Increase avatar download timeout and retries; add verification for uploaded avatars with fallback caching

This commit is contained in:
Leon Bösche
2026-01-31 18:44:52 +01:00
parent 33f977293d
commit 94e9036e87
2 changed files with 47 additions and 2 deletions

View File

@@ -39,8 +39,8 @@ func Load() *Config {
NextcloudBase: getEnv("NEXTCLOUD_BASEPATH", "/"),
AllowedOrigins: getEnv("ALLOWED_ORIGINS", "https://b0esche.cloud,https://www.b0esche.cloud,https://*.b0esche.cloud,http://localhost:8080"),
AvatarCacheDir: getEnv("AVATAR_CACHE_DIR", "/var/cache/b0esche/avatars"),
AvatarDownloadTimeoutSeconds: getEnvInt("AVATAR_DOWNLOAD_TIMEOUT_SECONDS", 10),
AvatarDownloadRetries: getEnvInt("AVATAR_DOWNLOAD_RETRIES", 2),
AvatarDownloadTimeoutSeconds: getEnvInt("AVATAR_DOWNLOAD_TIMEOUT_SECONDS", 20),
AvatarDownloadRetries: getEnvInt("AVATAR_DOWNLOAD_RETRIES", 3),
}
log.Printf("[CONFIG] Nextcloud URL: %q, User: %q, BasePath: %q, AvatarCacheDir: %q, AvatarDownloadTimeoutSeconds: %d, AvatarDownloadRetries: %d\n", cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudBase, cfg.AvatarCacheDir, cfg.AvatarDownloadTimeoutSeconds, cfg.AvatarDownloadRetries)
return cfg

View File

@@ -4216,6 +4216,29 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas
return
}
// Verify uploaded avatar is available on WebDAV (best-effort, retry)
verified := false
verifyRetries := 3
verifyTimeout := 5 // seconds
for i := 0; i < verifyRetries; i++ {
vctx, vcancel := context.WithTimeout(r.Context(), time.Duration(verifyTimeout)*time.Second)
resp, derr := client.Download(vctx, avatarPath, "")
vcancel()
if derr == nil && resp != nil {
resp.Body.Close()
verified = true
break
}
fmt.Printf("[WARN] avatar verification attempt %d/%d failed for %s: %v\n", i+1, verifyRetries, avatarPath, derr)
time.Sleep(time.Duration(300*(1<<i)) * time.Millisecond) // 300ms, 600ms, 1.2s
}
// If verification failed, log detailed message (but proceed to respond with preview/cache)
if !verified {
fmt.Printf("[ERROR] avatar verification failed after %d attempts for %s\n", verifyRetries, avatarPath)
} else {
fmt.Printf("[INFO] avatar verification succeeded for %s\n", avatarPath)
}
// Build public URL including version based on updated_at and include token if available
var version int64 = time.Now().Unix()
// Try to use updated_at from DB to be more accurate
@@ -4229,9 +4252,31 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas
cached := false
if err := writeAvatarCache(cfg, userID.String(), versionStr, ext, fileBytes); err != nil {
fmt.Printf("[WARN] failed to write avatar cache for user=%s version=%s: %v\n", userID.String(), versionStr, err)
// Attempt to write to an additional local data dir as a fallback
fallbackDir := "./data/avatars"
if err2 := os.MkdirAll(fallbackDir, 0755); err2 == nil {
fallbackPath := filepath.Join(fallbackDir, fmt.Sprintf("%s.%s%s", userID.String(), versionStr, ext))
if err3 := os.WriteFile(fallbackPath, fileBytes, 0644); err3 == nil {
fmt.Printf("[INFO] Wrote avatar cache to fallback path %s\n", fallbackPath)
cached = true
} else {
fmt.Printf("[WARN] failed to write avatar cache to fallback path %s: %v\n", fallbackPath, err3)
}
} else {
fmt.Printf("[WARN] failed to create fallback avatar dir %s: %v\n", fallbackDir, err2)
}
} else {
cached = true
}
// Verify cache is readable
if cached {
if b, ct, cerr := readAvatarCache(cfg, userID.String(), versionStr); cerr != nil {
fmt.Printf("[WARN] cache write verification failed for user=%s v=%s: %v\n", userID.String(), versionStr, cerr)
cached = false
} else {
fmt.Printf("[INFO] cache write verified for user=%s v=%s (content-type=%s size=%d)\n", userID.String(), versionStr, ct, len(b))
}
}
// Audit log
auditLogger.Log(r.Context(), audit.Entry{