Enhance avatar upload handling by providing immediate preview data and improving cache write logic

This commit is contained in:
Leon Bösche
2026-01-31 18:09:07 +01:00
parent 6085409bad
commit ff4c9bb26c
2 changed files with 38 additions and 3 deletions

View File

@@ -113,6 +113,19 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
_avatarUploadProgress = 0.0;
});
// Immediately show a preview if the server returned the avatar bytes
try {
final avatarData = response['avatarData'] as String?;
final contentType = response['contentType'] as String?;
if (avatarData != null && contentType != null) {
setState(() {
_avatarUrl = 'data:$contentType;base64,$avatarData';
});
}
} catch (_) {
// ignore
}
// Fetch latest profile from backend so we get the canonical avatar URL and version
try {
final apiClient = GetIt.I<ApiClient>();

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -84,7 +85,11 @@ func writeAvatarCache(cfg *config.Config, userID string, version string, ext str
dir = fallback
}
p := avatarCachePath(&config.Config{AvatarCacheDir: dir}, userID, version, ext)
return os.WriteFile(p, data, 0644)
if err := os.WriteFile(p, data, 0644); err != nil {
return err
}
fmt.Printf("[INFO] Wrote avatar cache for user=%s v=%s path=%s size=%d\n", userID, version, p, len(data))
return nil
}
// readAvatarCache attempts to read a cached avatar for a user and optional version. If version is provided, it looks for an exact match; otherwise it returns the latest available cached avatar (if any).
@@ -96,9 +101,12 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
// Also check fallback tmp dir
checkDirs = append(checkDirs, filepath.Join(os.TempDir(), "b0esche_avatars"))
fmt.Printf("[DEBUG] readAvatarCache checking dirs=%v for user=%s version=%s\n", checkDirs, userID, version)
for _, dir := range checkDirs {
entries, err := os.ReadDir(dir)
if err != nil {
fmt.Printf("[DEBUG] readAvatarCache cannot read dir %s: %v\n", dir, err)
continue
}
if version != "" {
@@ -107,8 +115,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
for _, e := range entries {
if strings.HasPrefix(e.Name(), prefix) {
p := filepath.Join(dir, e.Name())
fmt.Printf("[INFO] Found cached avatar for user=%s v=%s path=%s\n", userID, version, p)
b, err := os.ReadFile(p)
if err != nil {
fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
return nil, "", err
}
ext := filepath.Ext(e.Name())
@@ -120,6 +130,7 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
}
}
// not found
fmt.Printf("[DEBUG] no exact cached avatar match in %s for prefix=%s\n", dir, prefix)
continue
}
// no version specified: return latest by modtime of files starting with userID.
@@ -139,8 +150,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
}
if latest != nil {
p := filepath.Join(dir, latestName)
fmt.Printf("[INFO] Found latest cached avatar for user=%s path=%s\n", userID, p)
b, err := os.ReadFile(p)
if err != nil {
fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
return nil, "", err
}
ext := filepath.Ext(latestName)
@@ -4213,8 +4226,11 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas
}
// Save avatar bytes to local cache keyed by version so it survives restarts and avoids unnecessary re-downloads
versionStr := fmt.Sprintf("%d", version)
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)
} else {
cached = true
}
// Audit log
@@ -4228,14 +4244,20 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas
},
})
// Provide avatarData for immediate preview (small images only)
avatarData := base64.StdEncoding.EncodeToString(fileBytes)
publicURL := fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", version)
if token, ok := middleware.GetToken(r.Context()); ok && token != "" {
publicURL = fmt.Sprintf("%s&token=%s", publicURL, token)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Avatar uploaded successfully",
"avatarUrl": publicURL,
"message": "Avatar uploaded successfully",
"avatarUrl": publicURL,
"cached": cached,
"avatarData": avatarData,
"contentType": contentType,
})
}