luckfun233 commited on
Commit
534def0
·
1 Parent(s): 745c481

Fix config.json permission denied on HF Spaces persistent storage

Browse files

On 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 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
- _ = os.WriteFile(path, b, 0o644)
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 os.WriteFile(path, b, 0o644)
43
  }
44
  if err := os.MkdirAll(dir, 0o755); err != nil {
45
  return fmt.Errorf("mkdir config dir: %w", err)
46
  }
47
- return os.WriteFile(path, b, 0o644)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  }