luckfun233 commited on
Commit ·
534def0
1
Parent(s): 745c481
Fix config.json permission denied on HF Spaces persistent storage
Browse filesOn HuggingFace Spaces, the persistent /data volume may retain files owned by root or a different UID after container rebuilds, causing permission denied when ds2api tries to save config changes.
Two-layer fix:
- entrypoint.sh: always chown+chmod config files on startup, not just when creating them for the first time
- Go writeConfigBytes: atomic write (temp file + rename) with fallback that removes the old file and retries if permission denied
- entrypoint.sh +7 -2
- internal/config/store.go +1 -1
- internal/config/store_env_writeback.go +52 -2
entrypoint.sh
CHANGED
|
@@ -9,14 +9,19 @@ fi
|
|
| 9 |
# Create config file if not exists or is empty (with empty JSON object)
|
| 10 |
if [ ! -f /data/config.json ] || [ ! -s /data/config.json ]; then
|
| 11 |
echo {} > /data/config.json
|
| 12 |
-
chown ds2api:ds2api /data/config.json 2>/dev/null || true
|
| 13 |
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
# Create chat history file if not exists or is empty
|
| 16 |
if [ ! -f /data/chat_history.json ] || [ ! -s /data/chat_history.json ]; then
|
| 17 |
echo {} > /data/chat_history.json
|
| 18 |
-
chown ds2api:ds2api /data/chat_history.json 2>/dev/null || true
|
| 19 |
fi
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# Start the application as ds2api user
|
| 22 |
exec gosu ds2api /usr/local/bin/ds2api
|
|
|
|
| 9 |
# Create config file if not exists or is empty (with empty JSON object)
|
| 10 |
if [ ! -f /data/config.json ] || [ ! -s /data/config.json ]; then
|
| 11 |
echo {} > /data/config.json
|
|
|
|
| 12 |
fi
|
| 13 |
+
# Always ensure config.json is writable by ds2api user, even if the file
|
| 14 |
+
# was created by a previous container with different ownership (common on
|
| 15 |
+
# HuggingFace Spaces persistent storage after rebuild).
|
| 16 |
+
chown ds2api:ds2api /data/config.json 2>/dev/null || true
|
| 17 |
+
chmod 644 /data/config.json 2>/dev/null || true
|
| 18 |
|
| 19 |
# Create chat history file if not exists or is empty
|
| 20 |
if [ ! -f /data/chat_history.json ] || [ ! -s /data/chat_history.json ]; then
|
| 21 |
echo {} > /data/chat_history.json
|
|
|
|
| 22 |
fi
|
| 23 |
+
chown ds2api:ds2api /data/chat_history.json 2>/dev/null || true
|
| 24 |
+
chmod 644 /data/chat_history.json 2>/dev/null || true
|
| 25 |
|
| 26 |
# Start the application as ds2api user
|
| 27 |
exec gosu ds2api /usr/local/bin/ds2api
|
internal/config/store.go
CHANGED
|
@@ -136,7 +136,7 @@ func loadConfigFromFile(path string) (Config, error) {
|
|
| 136 |
cfg.DropInvalidAccounts()
|
| 137 |
if strings.Contains(string(content), `"test_status"`) && !IsVercel() {
|
| 138 |
if b, err := json.MarshalIndent(cfg, "", " "); err == nil {
|
| 139 |
-
_ =
|
| 140 |
}
|
| 141 |
}
|
| 142 |
return cfg, nil
|
|
|
|
| 136 |
cfg.DropInvalidAccounts()
|
| 137 |
if strings.Contains(string(content), `"test_status"`) && !IsVercel() {
|
| 138 |
if b, err := json.MarshalIndent(cfg, "", " "); err == nil {
|
| 139 |
+
_ = overwriteFile(path, b, 0o644)
|
| 140 |
}
|
| 141 |
}
|
| 142 |
return cfg, nil
|
internal/config/store_env_writeback.go
CHANGED
|
@@ -36,13 +36,63 @@ func writeConfigFile(path string, cfg Config) error {
|
|
| 36 |
return writeConfigBytes(path, b)
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
func writeConfigBytes(path string, b []byte) error {
|
| 40 |
dir := filepath.Dir(path)
|
| 41 |
if dir == "." || dir == "" {
|
| 42 |
-
return
|
| 43 |
}
|
| 44 |
if err := os.MkdirAll(dir, 0o755); err != nil {
|
| 45 |
return fmt.Errorf("mkdir config dir: %w", err)
|
| 46 |
}
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
|
|
|
| 36 |
return writeConfigBytes(path, b)
|
| 37 |
}
|
| 38 |
|
| 39 |
+
// overwriteFile writes data to path. If the write fails due to permission
|
| 40 |
+
// issues (common on HuggingFace Spaces where the file may be owned by root),
|
| 41 |
+
// it removes the old file and retries.
|
| 42 |
+
func overwriteFile(path string, b []byte, perm os.FileMode) error {
|
| 43 |
+
if err := os.WriteFile(path, b, perm); err != nil {
|
| 44 |
+
if removeErr := os.Remove(path); removeErr == nil {
|
| 45 |
+
return os.WriteFile(path, b, perm)
|
| 46 |
+
}
|
| 47 |
+
return err
|
| 48 |
+
}
|
| 49 |
+
return nil
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
func writeConfigBytes(path string, b []byte) error {
|
| 53 |
dir := filepath.Dir(path)
|
| 54 |
if dir == "." || dir == "" {
|
| 55 |
+
return overwriteFile(path, b, 0o644)
|
| 56 |
}
|
| 57 |
if err := os.MkdirAll(dir, 0o755); err != nil {
|
| 58 |
return fmt.Errorf("mkdir config dir: %w", err)
|
| 59 |
}
|
| 60 |
+
// Write to a temp file first, then rename atomically to avoid
|
| 61 |
+
// leaving a corrupted/truncated config on crash or disk-full.
|
| 62 |
+
tmp, err := os.CreateTemp(dir, ".ds2api-config-*")
|
| 63 |
+
if err != nil {
|
| 64 |
+
return fmt.Errorf("create temp config file: %w", err)
|
| 65 |
+
}
|
| 66 |
+
tmpName := tmp.Name()
|
| 67 |
+
wrote := false
|
| 68 |
+
defer func() {
|
| 69 |
+
if !wrote {
|
| 70 |
+
_ = tmp.Close()
|
| 71 |
+
_ = os.Remove(tmpName)
|
| 72 |
+
}
|
| 73 |
+
}()
|
| 74 |
+
if _, err := tmp.Write(b); err != nil {
|
| 75 |
+
return fmt.Errorf("write temp config: %w", err)
|
| 76 |
+
}
|
| 77 |
+
if err := tmp.Sync(); err != nil {
|
| 78 |
+
return fmt.Errorf("sync temp config: %w", err)
|
| 79 |
+
}
|
| 80 |
+
if err := tmp.Close(); err != nil {
|
| 81 |
+
return fmt.Errorf("close temp config: %w", err)
|
| 82 |
+
}
|
| 83 |
+
wrote = true
|
| 84 |
+
if err := os.Rename(tmpName, path); err != nil {
|
| 85 |
+
// Rename may fail because the existing file has wrong ownership/permissions
|
| 86 |
+
// (common on HuggingFace Spaces where /data/config.json may be owned by root).
|
| 87 |
+
// Try removing the old file and renaming again.
|
| 88 |
+
if removeErr := os.Remove(path); removeErr == nil {
|
| 89 |
+
if renameErr := os.Rename(tmpName, path); renameErr == nil {
|
| 90 |
+
return nil
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
// Rename still failing (e.g. cross-filesystem); fall back to direct write.
|
| 94 |
+
_ = os.Remove(tmpName)
|
| 95 |
+
return overwriteFile(path, b, 0o644)
|
| 96 |
+
}
|
| 97 |
+
return nil
|
| 98 |
}
|