fix: make WebDAV upload retry reliable

This commit is contained in:
OpenClaw Agent
2026-09-17 06:41:35 +08:00
parent efb2ff33b4
commit 40d99e1e22
5 changed files with 59 additions and 7 deletions
+43 -1
View File
@@ -1,6 +1,8 @@
package main package main
import ( import (
"crypto/rand"
"fmt"
"strings" "strings"
"time" "time"
@@ -68,6 +70,9 @@ func (a *App) putSettings(c *gin.Context) {
} else { } else {
a.storage = nil 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}) c.JSON(200, gin.H{"ok": true})
} }
@@ -77,11 +82,48 @@ func (a *App) testWebDAV(c *gin.Context) {
c.Status(400) c.Status(400)
return 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 := gowebdav.NewClient(v.URL, v.User, v.Pass)
client.SetTimeout(20 * time.Second) client.SetTimeout(20 * time.Second)
if err := client.Connect(); err != nil { 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 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}) c.JSON(200, gin.H{"ok": true, "path": v.Path})
} }
+1 -1
View File
@@ -112,7 +112,7 @@ func (a *App) storageForTask(t *Task) (RemoteStorage, string, bool) {
if a.db.First(&user, t.UserID).Error != nil { if a.db.First(&user, t.UserID).Error != nil {
return nil, "", false return nil, "", false
} }
if !a.hasPermission(user.ID, PermWebDAV) { if user.Role != RoleAdmin && !a.hasPermission(user.ID, PermWebDAV) {
return nil, "", false return nil, "", false
} }
// Regular users use the system/global WebDAV only. They cannot override // Regular users use the system/global WebDAV only. They cannot override
+10
View File
@@ -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(`[/\\:*?"<>|]+`) var bad = regexp.MustCompile(`[/\\:*?"<>|]+`)
func safe(s string) string { func safe(s string) string {
+3 -3
View File
@@ -19,9 +19,8 @@ func (a *App) upload(t *Task) {
} }
a.db.Model(t).Updates(map[string]any{"status": "uploading"}) 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) 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 { if err := storage.MkdirAll(filepath.ToSlash(filepath.Dir(remote))); err != nil {
a.fail(t, err) a.failUpload(t, err)
return return
} }
if fi, err := os.Stat(t.LocalPath); err == nil { 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 { if err := storage.Upload(t.LocalPath, remote); err != nil {
a.fail(t, err) a.failUpload(t, err)
return return
} }
t.RemotePath = remote
if a.cfg.WebDAVVersionPolicy == "replace_lower" { if a.cfg.WebDAVVersionPolicy == "replace_lower" {
a.cleanupLowerQualityVersions(t, storage) a.cleanupLowerQualityVersions(t, storage)
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import "time"
// unavailable or the user's WebDAV permission was not yet configured. // unavailable or the user's WebDAV permission was not yet configured.
func (a *App) resyncPendingUploads() { func (a *App) resyncPendingUploads() {
var tasks []Task 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 return
} }
for i := range tasks { for i := range tasks {
@@ -20,7 +20,7 @@ func (a *App) resyncPendingUploads() {
} }
// startWebDAVResync runs a small periodic repair loop. It is intentionally // 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() { func (a *App) startWebDAVResync() {
go func() { go func() {
a.resyncPendingUploads() a.resyncPendingUploads()