2 Commits
v1.9 ... v1.11

Author SHA1 Message Date
sakuradairong
3813f49a12 release: v1.11
fix: restic REST URI 含 repo 前缀 (/backup/config), 桥接器未剥离导致
     type/name 解析错误, remoteBase + URI 双重拼接产生路径嵌套

fix: 在 handleRequest 中剥离 repoPath 前缀后再解析 segments,
     使 type/name 指向正确的 restic REST API 资源 (config/keys/data/...)
     "backup" 段不会再被拼入 SMB 路径
2026-06-06 00:31:42 +08:00
sakuradairong
b2ea0c7960 release: v1.10
fix: config/blob GET handler 提前删文件导致 restic 读到零字节
fix: NanoHTTPD Response 在 handler 返回后才发送,finally 删除过早
fix: 改为 readBytes() 后关闭文件,再返回 InputStream
2026-06-06 00:25:20 +08:00
3 changed files with 23 additions and 10 deletions

View File

@@ -26,8 +26,8 @@ android {
applicationId "com.example.androidbackupgui"
minSdk 24
targetSdk 34
versionCode 10
versionName "1.9"
versionCode 12
versionName "1.11"
}
buildFeatures {
viewBinding true

View File

@@ -65,7 +65,7 @@ class RestBridgeRunner {
val transport = cachedTransport!!
val remoteBase = buildRemoteBase(backend, backendUrl, backendShare, repoPath)
val bridge = ResticRestBridge(transport, remoteBase, cacheDir)
val bridge = ResticRestBridge(transport, remoteBase, repoPath, cacheDir)
try {
bridge.start(0)

View File

@@ -6,7 +6,6 @@ import fi.iki.elonen.NanoHTTPD.IHTTPSession
import kotlinx.coroutines.runBlocking
import java.io.File
import java.util.UUID
/**
* NanoHTTPD-based REST bridge implementing the restic REST backend API.
*
@@ -14,10 +13,15 @@ import java.util.UUID
* can read/write blobs directly to SMB/WebDAV without a local staging repo.
*
* Port is auto-assigned (0); use [listeningPort] after start().
*
* @param repoPath repository path from the bridge URL (e.g. "backup").
* Stripped from incoming URIs so that the remoteBase SMB path
* does not get double-nested with the repo prefix.
*/
class ResticRestBridge(
private val transport: RemoteTransport,
private val remoteBase: String,
private val repoPath: String,
private val cacheDir: File
) : NanoHTTPD(0) {
@@ -56,6 +60,14 @@ class ResticRestBridge(
session: IHTTPSession
): Response {
val path = uri.trimEnd('/')
// Strip the repoPath prefix (/backup/...) from the URI so that type/name
// parsing sees only the restic REST API segment.
val stripPrefix = if (repoPath.isNotEmpty()) "/${repoPath.trim('/')}" else ""
val strippedPath = if (stripPrefix.isNotEmpty() && path.startsWith(stripPrefix)) {
path.removePrefix(stripPrefix).ifEmpty { "/" }
} else {
path
}
// POST {path}?create=true -> mkdirs
if (method == NanoHTTPD.Method.POST && params["create"] == "true") {
@@ -71,7 +83,7 @@ class ResticRestBridge(
}
}
val segments = path.split("/").filter { it.isNotEmpty() }
val segments = strippedPath.split("/").filter { it.isNotEmpty() }
if (segments.isEmpty()) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "Invalid path")
@@ -174,7 +186,8 @@ class ResticRestBridge(
try {
when (transport.download(remotePath, tempFile.absolutePath)) {
is AppResult.Success -> {
newChunkedResponse(Response.Status.OK, "application/octet-stream", tempFile.inputStream())
val data = tempFile.readBytes()
newFixedLengthResponse(Response.Status.OK, "application/octet-stream", data.inputStream(), data.size.toLong())
}
is AppResult.Failure -> newFixedLengthResponse(
Response.Status.NOT_FOUND, "text/plain", ""
@@ -305,14 +318,14 @@ class ResticRestBridge(
response.addHeader("Content-Length", chunkSize.toString())
return@runBlocking response
}
// Full file — stream directly without loading into memory
// Full file — read into memory (blobs are typically small)
val data = tempFile.readBytes()
val response = newChunkedResponse(
Response.Status.OK,
"application/octet-stream",
tempFile.inputStream()
data.inputStream()
)
response.addHeader("Content-Length", tempFile.length().toString())
response.addHeader("Content-Length", data.size.toString())
response
}
is AppResult.Failure -> newFixedLengthResponse(