2 Commits
v1.10 ... v1.12

Author SHA1 Message Date
sakuradairong
1f3e1ceea8 release: v1.12
fix: HEAD /backup/config 响应 Content-Length 为 0,restic 以为 config 空文件
fix: 添加 RemoteTransport.fileSize() 方法,HEAD 返回实际文件大小
feat: SmbTransport/WebdavTransport 实现 fileSize
2026-06-06 01:07:12 +08:00
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
6 changed files with 56 additions and 8 deletions

View File

@@ -26,8 +26,8 @@ android {
applicationId "com.example.androidbackupgui"
minSdk 24
targetSdk 34
versionCode 11
versionName "1.10"
versionCode 13
versionName "1.12"
}
buildFeatures {
viewBinding true

View File

@@ -42,6 +42,8 @@ interface RemoteTransport {
suspend fun delete(remotePath: String): AppResult<Unit>
suspend fun exists(remotePath: String): AppResult<Boolean>
/** Get the size of a remote file in bytes. Returns [AppResult.Failure] if not found. */
suspend fun fileSize(remotePath: String): AppResult<Long>
companion object {

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

@@ -4,9 +4,9 @@ import android.util.Log
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.IHTTPSession
import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
import java.io.File
import java.util.UUID
/**
* NanoHTTPD-based REST bridge implementing the restic REST backend API.
*
@@ -14,10 +14,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 +61,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 +84,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")
@@ -156,10 +169,15 @@ class ResticRestBridge(
val remotePath = "$remoteBase/config"
when (method) {
NanoHTTPD.Method.HEAD -> {
when (val result = transport.exists(remotePath)) {
when (val exists = transport.exists(remotePath)) {
is AppResult.Success -> {
if (result.data) {
newFixedLengthResponse(Response.Status.OK, "application/octet-stream", "")
if (exists.data) {
val sizeResult = transport.fileSize(remotePath)
val fileSize = if (sizeResult is AppResult.Success) sizeResult.data else 0L
newFixedLengthResponse(
Response.Status.OK, "application/octet-stream",
ByteArrayInputStream(ByteArray(0)), fileSize
)
} else {
newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "")
}

View File

@@ -253,4 +253,17 @@ class SmbTransport(
err(AppError.Remote("SMB 检查失败", "exists", cause = e))
}
}
override suspend fun fileSize(remotePath: String): AppResult<Long> =
withContext(Dispatchers.IO) {
try {
val file = smbFile(remotePath)
if (!file.exists()) return@withContext err(AppError.Remote("文件不存在", "fileSize"))
AppResult.Success(file.length())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
err(AppError.Remote("SMB 获取文件大小失败", "fileSize", cause = e))
}
}
}

View File

@@ -180,4 +180,19 @@ class WebdavTransport(
err(AppError.Remote("WebDAV 检查失败", "exists", cause = e))
}
}
override suspend fun fileSize(remotePath: String): AppResult<Long> =
withContext(Dispatchers.IO) {
try {
val url = buildUrl(remotePath)
if (!sardine.exists(url)) return@withContext err(AppError.Remote("文件不存在", "fileSize"))
val resources = sardine.list(url)
val size = resources.firstOrNull()?.contentLength ?: 0L
AppResult.Success(size)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
err(AppError.Remote("WebDAV 获取文件大小失败", "fileSize", cause = e))
}
}
}