Enhance avatar upload handling by providing immediate preview data and improving cache write logic
This commit is contained in:
@@ -113,6 +113,19 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
|
|||||||
_avatarUploadProgress = 0.0;
|
_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
|
// Fetch latest profile from backend so we get the canonical avatar URL and version
|
||||||
try {
|
try {
|
||||||
final apiClient = GetIt.I<ApiClient>();
|
final apiClient = GetIt.I<ApiClient>();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -84,7 +85,11 @@ func writeAvatarCache(cfg *config.Config, userID string, version string, ext str
|
|||||||
dir = fallback
|
dir = fallback
|
||||||
}
|
}
|
||||||
p := avatarCachePath(&config.Config{AvatarCacheDir: dir}, userID, version, ext)
|
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).
|
// 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
|
// Also check fallback tmp dir
|
||||||
checkDirs = append(checkDirs, filepath.Join(os.TempDir(), "b0esche_avatars"))
|
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 {
|
for _, dir := range checkDirs {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("[DEBUG] readAvatarCache cannot read dir %s: %v\n", dir, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if version != "" {
|
if version != "" {
|
||||||
@@ -107,8 +115,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
|
|||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if strings.HasPrefix(e.Name(), prefix) {
|
if strings.HasPrefix(e.Name(), prefix) {
|
||||||
p := filepath.Join(dir, e.Name())
|
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)
|
b, err := os.ReadFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
ext := filepath.Ext(e.Name())
|
ext := filepath.Ext(e.Name())
|
||||||
@@ -120,6 +130,7 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// not found
|
// not found
|
||||||
|
fmt.Printf("[DEBUG] no exact cached avatar match in %s for prefix=%s\n", dir, prefix)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// no version specified: return latest by modtime of files starting with userID.
|
// 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 {
|
if latest != nil {
|
||||||
p := filepath.Join(dir, latestName)
|
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)
|
b, err := os.ReadFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
ext := filepath.Ext(latestName)
|
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
|
// Save avatar bytes to local cache keyed by version so it survives restarts and avoids unnecessary re-downloads
|
||||||
versionStr := fmt.Sprintf("%d", version)
|
versionStr := fmt.Sprintf("%d", version)
|
||||||
|
cached := false
|
||||||
if err := writeAvatarCache(cfg, userID.String(), versionStr, ext, fileBytes); err != nil {
|
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)
|
fmt.Printf("[WARN] failed to write avatar cache for user=%s version=%s: %v\n", userID.String(), versionStr, err)
|
||||||
|
} else {
|
||||||
|
cached = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audit log
|
// 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)
|
publicURL := fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", version)
|
||||||
if token, ok := middleware.GetToken(r.Context()); ok && token != "" {
|
if token, ok := middleware.GetToken(r.Context()); ok && token != "" {
|
||||||
publicURL = fmt.Sprintf("%s&token=%s", publicURL, token)
|
publicURL = fmt.Sprintf("%s&token=%s", publicURL, token)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"message": "Avatar uploaded successfully",
|
"message": "Avatar uploaded successfully",
|
||||||
"avatarUrl": publicURL,
|
"avatarUrl": publicURL,
|
||||||
|
"cached": cached,
|
||||||
|
"avatarData": avatarData,
|
||||||
|
"contentType": contentType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user