Refactor WebDAV backup path and file name handling

This commit is contained in:
2dust
2026-01-15 11:52:18 +08:00
parent c99fda9839
commit c08147c362
4 changed files with 30 additions and 47 deletions
@@ -9,7 +9,9 @@ object AppConfig {
/** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets"
const val DIR_BACKUPS = "backups"
const val WEBDAV_BACKUP_DIR = "backups"
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
/** Legacy configuration keys. */
const val ANG_CONFIG = "ang_config"
@@ -35,18 +35,18 @@ object WebDavManager {
}
/**
* Upload a local file to a remote relative path under the configured remoteBasePath.
* The provided `remoteRelativePath` should be relative (e.g. "backup_ng.zip").
* Upload a local file to a remote file name under the configured remoteBasePath.
* The provided `remoteFileName` should be a file name (e.g. "backup_ng.zip").
* The method will attempt to create parent directories via MKCOL before PUT.
*
* @param localFile File to upload.
* @param remoteRelativePath Remote path relative to configured remoteBasePath.
* @param remoteFileName Remote file name relative to configured remoteBasePath.
* @return true if upload succeeded (HTTP 2xx), false otherwise.
*/
suspend fun uploadFile(localFile: File, remoteRelativePath: String): Boolean = withContext(Dispatchers.IO) {
suspend fun uploadFile(localFile: File, remoteFileName: String): Boolean = withContext(Dispatchers.IO) {
val remote = buildRemoteUrl(remoteFileName)
try {
val cl = client ?: return@withContext false
val remote = buildRemoteUrl(remoteRelativePath)
// Ensure parent directories exist
val dirPath = remote.substringBeforeLast('/')
@@ -67,14 +67,14 @@ object WebDavManager {
cl.newCall(req).execute().use { resp ->
val success = resp.isSuccessful
if (success) {
Log.i(AppConfig.TAG, "WebDAV upload success: $remoteRelativePath")
Log.i(AppConfig.TAG, "WebDAV upload success: $remote")
} else {
Log.e(AppConfig.TAG, "WebDAV upload failed: $remoteRelativePath (HTTP ${resp.code})")
Log.e(AppConfig.TAG, "WebDAV upload failed: $remote (HTTP ${resp.code})")
}
return@withContext success
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV upload exception: $remoteRelativePath", e)
Log.e(AppConfig.TAG, "WebDAV upload exception: $remote", e)
return@withContext false
}
}
@@ -82,18 +82,18 @@ object WebDavManager {
/**
* Download a remote file (relative to configured remoteBasePath) into a local file.
*
* @param remoteRelativePath Remote path relative to configured remoteBasePath.
* @param remoteFileName Remote file name relative to configured remoteBasePath.
* @param destFile Local destination file to write to.
* @return true if download and write succeeded, false otherwise.
*/
suspend fun downloadFile(remoteRelativePath: String, destFile: File): Boolean = withContext(Dispatchers.IO) {
suspend fun downloadFile(remoteFileName: String, destFile: File): Boolean = withContext(Dispatchers.IO) {
val remote = buildRemoteUrl(remoteFileName)
try {
val cl = client ?: return@withContext false
val remote = buildRemoteUrl(remoteRelativePath)
val req = applyAuth(Request.Builder().url(remote).get()).build()
cl.newCall(req).execute().use { resp ->
if (!resp.isSuccessful) {
Log.e(AppConfig.TAG, "WebDAV download failed: $remoteRelativePath (HTTP ${resp.code})")
Log.e(AppConfig.TAG, "WebDAV download failed: $remote (HTTP ${resp.code})")
return@withContext false
}
@@ -104,29 +104,31 @@ object WebDavManager {
}
}
Log.i(AppConfig.TAG, "WebDAV download success: $remoteRelativePath")
Log.i(AppConfig.TAG, "WebDAV download success: $remote")
return@withContext true
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV download exception: $remoteRelativePath", e)
Log.e(AppConfig.TAG, "WebDAV download exception: $remote", e)
return@withContext false
}
}
/**
* Build a full remote URL by combining the configured base URL, the configured
* remote base path and a relative path provided by the caller.
* remote base path and a file name provided by the caller.
*
* Example: baseUrl="https://example.com/remote.php/dav", remoteBasePath="backups",
* remoteRelativePath="backup_ng.zip" => "https://example.com/remote.php/dav/backups/backup_ng.zip"
* remoteFileName="backup_ng.zip" => "https://example.com/remote.php/dav/backups/backup_ng.zip"
*
* @param remoteRelativePath A path relative to the configured remoteBasePath (no leading slash required).
* @param remoteFileName A file name relative to the configured remoteBasePath (no leading slash required).
* @return Full URL string used for HTTP operations.
*/
private fun buildRemoteUrl(remoteRelativePath: String): String {
private fun buildRemoteUrl(remoteFileName: String): String {
val base = cfg?.baseUrl?.trimEnd('/') ?: ""
val basePath = cfg?.remoteBasePath?.trim('/') ?: ""
val rel = remoteRelativePath.trimStart('/')
// Use configured remoteBasePath when not empty; otherwise fallback to AppConfig.WEBDAV_BACKUP_DIR
val basePathConfigured = cfg?.remoteBasePath?.trim('/')?.takeIf { it.isNotEmpty() }
val basePath = basePathConfigured ?: AppConfig.WEBDAV_BACKUP_DIR
val rel = remoteFileName.trimStart('/')
return if (basePath.isEmpty()) "$base/$rel" else "$base/$basePath/$rel"
}
@@ -170,7 +172,7 @@ object WebDavManager {
Log.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}")
}
}
} catch (ignored: Exception) {
} catch (_: Exception) {
// best-effort, continue
}
}
@@ -14,6 +14,7 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.WEBDAV_BACKUP_FILE_NAME
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBackupBinding
@@ -40,10 +41,6 @@ class BackupActivity : BaseActivity() {
resources.getStringArray(R.array.config_backup_options)
}
companion object {
private const val BACKUP_FILE_NAME = "backup_ng.zip"
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
@@ -263,7 +260,7 @@ class BackupActivity : BaseActivity() {
WebDavManager.init(saved)
val ok = try {
WebDavManager.uploadFile(tempFile, BACKUP_FILE_NAME)
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV upload error", e)
false
@@ -303,7 +300,7 @@ class BackupActivity : BaseActivity() {
try {
target = File(cacheDir, "download_${System.currentTimeMillis()}.zip")
WebDavManager.init(saved)
val ok = WebDavManager.downloadFile(BACKUP_FILE_NAME, target)
val ok = WebDavManager.downloadFile(WEBDAV_BACKUP_FILE_NAME, target)
if (!ok) {
withContext(Dispatchers.Main) {
toastError(R.string.toast_failure)
@@ -351,7 +348,7 @@ class BackupActivity : BaseActivity() {
val url = dialogBinding.etWebdavUrl.text.toString().trim()
val user = dialogBinding.etWebdavUser.text.toString().trim().ifEmpty { null }
val pass = dialogBinding.etWebdavPass.text.toString()
val remotePath = dialogBinding.etWebdavRemotePath.text.toString().trim().ifEmpty { "/" }
val remotePath = dialogBinding.etWebdavRemotePath.text.toString().trim().ifEmpty { AppConfig.WEBDAV_BACKUP_DIR }
val cfg = WebDavConfig(baseUrl = url, username = user, password = pass, remoteBasePath = remotePath)
MmkvManager.encodeWebDavConfig(cfg)
toastSuccess(R.string.toast_success)
@@ -375,24 +375,6 @@ object Utils {
}
}
/**
* Get the path to the backup directory.
*
* @param context The context to use.
* @return The path to the backup directory.
*/
fun backupPath(context: Context?): String {
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get backup path", e)
""
}
}
/**
* Get the device ID for XUDP base key.
*