From 40d99e1e22aacae63c333743ea1b9e4131d2593c Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 17 Sep 2026 06:41:35 +0800 Subject: [PATCH] fix: make WebDAV upload retry reliable --- settings.go | 44 +++++++++++++++++++++++++++++++++++++++++++- user_webdav.go | 2 +- util.go | 10 ++++++++++ webdav.go | 6 +++--- webdav_resync.go | 4 ++-- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/settings.go b/settings.go index e33200e..3c34d48 100644 --- a/settings.go +++ b/settings.go @@ -1,6 +1,8 @@ package main import ( + "crypto/rand" + "fmt" "strings" "time" @@ -68,6 +70,9 @@ func (a *App) putSettings(c *gin.Context) { } else { a.storage = nil } + // Credentials/config changes should repair already-downloaded local files + // immediately instead of waiting for the periodic retry interval. + go a.resyncPendingUploads() c.JSON(200, gin.H{"ok": true}) } @@ -77,11 +82,48 @@ func (a *App) testWebDAV(c *gin.Context) { c.Status(400) return } + // An empty password means "use the saved password". The password is never + // returned to the browser, so this also makes a fresh settings page test + // the credential that is actually stored by the server. + if v.URL == "" { + v.URL = a.cfg.WebDAVURL + } + if v.User == "" { + v.User = a.cfg.WebDAVUser + } + if v.Pass == "" { + v.Pass = a.cfg.WebDAVPass + } + v.Path = strings.Trim(v.Path, "/") + if v.Path == "" { + v.Path = a.cfg.WebDAVPath + } + if v.URL == "" || v.User == "" || v.Pass == "" { + c.JSON(400, gin.H{"ok": false, "error": "WebDAV URL、用户名或密码未配置"}) + return + } client := gowebdav.NewClient(v.URL, v.User, v.Pass) client.SetTimeout(20 * time.Second) if err := client.Connect(); err != nil { - c.JSON(502, gin.H{"ok": false, "error": err.Error()}) + c.JSON(502, gin.H{"ok": false, "error": "WebDAV authentication failed: " + err.Error()}) return } + if v.Path != "" { + if err := client.MkdirAll(v.Path, 0755); err != nil { + c.JSON(502, gin.H{"ok": false, "error": "WebDAV target path is not writable: " + err.Error()}) + return + } + } + var token [12]byte + if _, err := rand.Read(token[:]); err != nil { + c.JSON(500, gin.H{"ok": false, "error": "failed to create test token"}) + return + } + testPath := strings.Trim(v.Path, "/") + "/.am-downloader-test-" + fmt.Sprintf("%x", token[:]) + if err := client.Write(testPath, []byte("ok"), 0644); err != nil { + c.JSON(502, gin.H{"ok": false, "error": "WebDAV write test failed: " + err.Error()}) + return + } + _ = client.Remove(testPath) c.JSON(200, gin.H{"ok": true, "path": v.Path}) } diff --git a/user_webdav.go b/user_webdav.go index 51e0549..921ae64 100644 --- a/user_webdav.go +++ b/user_webdav.go @@ -112,7 +112,7 @@ func (a *App) storageForTask(t *Task) (RemoteStorage, string, bool) { if a.db.First(&user, t.UserID).Error != nil { return nil, "", false } - if !a.hasPermission(user.ID, PermWebDAV) { + if user.Role != RoleAdmin && !a.hasPermission(user.ID, PermWebDAV) { return nil, "", false } // Regular users use the system/global WebDAV only. They cannot override diff --git a/util.go b/util.go index 643d52c..f48cb17 100644 --- a/util.go +++ b/util.go @@ -25,6 +25,16 @@ func (a *App) fail(t *Task, err error) { } } +func (a *App) failUpload(t *Task, err error) { + if err == nil { + err = errors.New("unknown WebDAV upload error") + } + t.RemotePath = "" + t.Status = "upload_pending" + t.Error = "WebDAV 上传失败,等待自动重试: " + err.Error() + a.db.Save(t) +} + var bad = regexp.MustCompile(`[/\\:*?"<>|]+`) func safe(s string) string { diff --git a/webdav.go b/webdav.go index 99aba9c..5990216 100644 --- a/webdav.go +++ b/webdav.go @@ -19,9 +19,8 @@ func (a *App) upload(t *Task) { } a.db.Model(t).Updates(map[string]any{"status": "uploading"}) remote := strings.TrimRight(root, "/") + "/" + safe(t.Artist) + "/" + safe(t.Album) + "/" + safe(t.Title) + "." + safe(t.Format) - t.RemotePath = remote if err := storage.MkdirAll(filepath.ToSlash(filepath.Dir(remote))); err != nil { - a.fail(t, err) + a.failUpload(t, err) return } if fi, err := os.Stat(t.LocalPath); err == nil { @@ -37,9 +36,10 @@ func (a *App) upload(t *Task) { } } if err := storage.Upload(t.LocalPath, remote); err != nil { - a.fail(t, err) + a.failUpload(t, err) return } + t.RemotePath = remote if a.cfg.WebDAVVersionPolicy == "replace_lower" { a.cleanupLowerQualityVersions(t, storage) } diff --git a/webdav_resync.go b/webdav_resync.go index c8c5b81..43c9726 100644 --- a/webdav_resync.go +++ b/webdav_resync.go @@ -6,7 +6,7 @@ import "time" // unavailable or the user's WebDAV permission was not yet configured. func (a *App) resyncPendingUploads() { var tasks []Task - if err := a.db.Where("status = ? AND local_path <> ? AND (remote_path = ? OR remote_path IS NULL)", "done", "", "").Order("id asc").Limit(100).Find(&tasks).Error; err != nil { + if err := a.db.Where("status IN ? AND local_path <> ? AND (remote_path = ? OR remote_path IS NULL)", []string{"done", "upload_pending", "error"}, "", "").Order("id asc").Limit(100).Find(&tasks).Error; err != nil { return } for i := range tasks { @@ -20,7 +20,7 @@ func (a *App) resyncPendingUploads() { } // startWebDAVResync runs a small periodic repair loop. It is intentionally -// conservative: only completed local tasks without a remote path are retried. +// conservative: only tasks with a completed local file and no confirmed remote path are retried. func (a *App) startWebDAVResync() { go func() { a.resyncPendingUploads()