diff --git a/src/main/kotlin/center/timemachine/cloud/Sync.kt b/src/main/kotlin/center/timemachine/cloud/Sync.kt index cb3c33d..bd425d3 100644 --- a/src/main/kotlin/center/timemachine/cloud/Sync.kt +++ b/src/main/kotlin/center/timemachine/cloud/Sync.kt @@ -29,10 +29,24 @@ fun readCredentials(tokenFile: Path): Pair { return parts[0].trim() to parts[1].trim() } -/** Build the restic --repo URL: rest:// */ -private fun resticRepo(baseUrl: String, discordId: String): String { - val base = baseUrl.trimEnd('/').let { if (it.startsWith("rest:")) it else "rest:$it" } - return "$base/$discordId/" +/** + * Build the restic --repo URL: rest:://:@// + * + * URL-embedded basic auth is universally supported by restic; the alternative + * (RESTIC_REST_USERNAME / RESTIC_REST_PASSWORD env vars) requires restic 0.16+. + * Same password is used for HTTP basic auth and restic repo encryption — + * cloud-svc provisions one password per user covering both. + */ +private fun resticRepo(baseUrl: String, discordId: String, password: String): String { + val raw = baseUrl.trimStart().removePrefix("rest:").trimEnd('/') + val schemeEnd = raw.indexOf("://") + require(schemeEnd > 0) { "--url must include a scheme (http:// or https://): $baseUrl" } + val scheme = raw.substring(0, schemeEnd + 3) + val hostAndPath = raw.substring(schemeEnd + 3) + // URL-encode credentials to handle special chars in the password + val u = java.net.URLEncoder.encode(discordId, Charsets.UTF_8) + val p = java.net.URLEncoder.encode(password, Charsets.UTF_8) + return "rest:$scheme$u:$p@$hostAndPath/$discordId/" } /** Common env applied to every restic invocation. */ @@ -52,7 +66,7 @@ private fun resticEnv(password: String): Map = mapOf( fun pull(args: Args): Int { val (discordId, password) = readCredentials(args.tokenFile) val binary = Restic.resolveBinary(args) - val repo = resticRepo(args.url, discordId) + val repo = resticRepo(args.url, discordId, password) val env = resticEnv(password) // Check whether any snapshots exist @@ -103,7 +117,7 @@ fun pull(args: Args): Int { fun push(args: Args): Int { val (discordId, password) = readCredentials(args.tokenFile) val binary = Restic.resolveBinary(args) - val repo = resticRepo(args.url, discordId) + val repo = resticRepo(args.url, discordId, password) val env = resticEnv(password) val scope = Scope_.load(args.packFolder)