armand0e commited on
Commit
78120e2
·
1 Parent(s): 6572963

fix: resolve writable storage dir for logs/stats on Spaces

Browse files
src/app/api/chat/route.ts CHANGED
@@ -68,19 +68,50 @@ async function executeWebSearch(query: string): Promise<string> {
68
  }
69
  }
70
 
71
- function getWritableBaseDir() {
72
- const envDir = process.env.DATA_DIR;
73
- if (envDir) return envDir;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- // Hugging Face Spaces provides a writable /data volume; /app is typically read-only.
76
- if (process.env.NODE_ENV === "production") return "/data";
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- return process.cwd();
 
 
 
79
  }
80
 
81
- const BASE_DIR = getWritableBaseDir();
82
- const LOG_FILE = process.env.CHAT_LOG_PATH || path.join(BASE_DIR, "chat-logs.jsonl");
83
- const USAGE_FILE = process.env.USAGE_STATS_PATH || path.join(BASE_DIR, "usage-stats.json");
 
 
84
 
85
  type UsageStats = {
86
  totalRequests: number;
@@ -90,7 +121,8 @@ type UsageStats = {
90
 
91
  async function readUsageStats(): Promise<UsageStats> {
92
  try {
93
- const raw = await fs.readFile(USAGE_FILE, "utf8");
 
94
  const parsed = JSON.parse(raw) as Partial<UsageStats>;
95
  return {
96
  totalRequests: typeof parsed.totalRequests === "number" ? parsed.totalRequests : 0,
@@ -113,8 +145,9 @@ async function writeUsageStats(stats: UsageStats) {
113
  lastUpdated: stats.lastUpdated,
114
  };
115
  try {
116
- await fs.mkdir(path.dirname(USAGE_FILE), { recursive: true });
117
- await fs.writeFile(USAGE_FILE, JSON.stringify(payload, null, 2), "utf8");
 
118
  } catch (error) {
119
  // Don't fail chat if stats persistence isn't writable
120
  console.error("Failed to write usage stats:", error);
@@ -137,9 +170,10 @@ async function incrementUsageStats() {
137
 
138
  async function appendChatLog(entry: unknown) {
139
  try {
 
140
  const line = JSON.stringify(entry) + "\n";
141
- await fs.mkdir(path.dirname(LOG_FILE), { recursive: true });
142
- await fs.appendFile(LOG_FILE, line, "utf8");
143
  } catch (error) {
144
  // Don't fail chat if logging isn't writable
145
  console.error("Failed to write chat log:", error);
 
68
  }
69
  }
70
 
71
+ const storageDirPromise = (async () => {
72
+ const candidates: Array<string | undefined> = [
73
+ process.env.DATA_DIR,
74
+ process.env.HF_HOME,
75
+ process.env.HOME,
76
+ "/tmp",
77
+ os.tmpdir(),
78
+ process.cwd(),
79
+ ];
80
+
81
+ const seen = new Set<string>();
82
+ const unique = candidates.filter((c): c is string => {
83
+ if (!c) return false;
84
+ if (seen.has(c)) return false;
85
+ seen.add(c);
86
+ return true;
87
+ });
88
 
89
+ for (const dir of unique) {
90
+ try {
91
+ await fs.mkdir(dir, { recursive: true });
92
+ const probe = path.join(dir, ".write-probe");
93
+ await fs.writeFile(probe, "ok", "utf8");
94
+ await fs.unlink(probe);
95
+ return dir;
96
+ } catch {
97
+ // try next
98
+ }
99
+ }
100
+
101
+ return os.tmpdir();
102
+ })();
103
 
104
+ async function getLogFilePath() {
105
+ if (process.env.CHAT_LOG_PATH) return process.env.CHAT_LOG_PATH;
106
+ const baseDir = await storageDirPromise;
107
+ return path.join(baseDir, "chat-logs.jsonl");
108
  }
109
 
110
+ async function getUsageFilePath() {
111
+ if (process.env.USAGE_STATS_PATH) return process.env.USAGE_STATS_PATH;
112
+ const baseDir = await storageDirPromise;
113
+ return path.join(baseDir, "usage-stats.json");
114
+ }
115
 
116
  type UsageStats = {
117
  totalRequests: number;
 
121
 
122
  async function readUsageStats(): Promise<UsageStats> {
123
  try {
124
+ const usageFile = await getUsageFilePath();
125
+ const raw = await fs.readFile(usageFile, "utf8");
126
  const parsed = JSON.parse(raw) as Partial<UsageStats>;
127
  return {
128
  totalRequests: typeof parsed.totalRequests === "number" ? parsed.totalRequests : 0,
 
145
  lastUpdated: stats.lastUpdated,
146
  };
147
  try {
148
+ const usageFile = await getUsageFilePath();
149
+ await fs.mkdir(path.dirname(usageFile), { recursive: true });
150
+ await fs.writeFile(usageFile, JSON.stringify(payload, null, 2), "utf8");
151
  } catch (error) {
152
  // Don't fail chat if stats persistence isn't writable
153
  console.error("Failed to write usage stats:", error);
 
170
 
171
  async function appendChatLog(entry: unknown) {
172
  try {
173
+ const logFile = await getLogFilePath();
174
  const line = JSON.stringify(entry) + "\n";
175
+ await fs.mkdir(path.dirname(logFile), { recursive: true });
176
+ await fs.appendFile(logFile, line, "utf8");
177
  } catch (error) {
178
  // Don't fail chat if logging isn't writable
179
  console.error("Failed to write chat log:", error);
src/app/api/logs/route.ts CHANGED
@@ -1,18 +1,35 @@
1
  import { NextRequest } from "next/server";
2
  import { promises as fs } from "fs";
 
3
  import path from "path";
4
 
5
  export const runtime = "nodejs";
6
 
7
- function getWritableBaseDir() {
8
- const envDir = process.env.DATA_DIR;
9
- if (envDir) return envDir;
10
- if (process.env.NODE_ENV === "production") return "/data";
11
- return process.cwd();
12
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- const BASE_DIR = getWritableBaseDir();
15
- const LOG_FILE = process.env.CHAT_LOG_PATH || path.join(BASE_DIR, "chat-logs.jsonl");
 
16
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
17
 
18
  function isAuthorized(req: NextRequest) {
@@ -45,7 +62,8 @@ export async function GET(req: NextRequest) {
45
  const raw = rawParam === "1" || rawParam === "true";
46
 
47
  try {
48
- const file = await fs.readFile(LOG_FILE, "utf8");
 
49
  const lines = file.split(/\r?\n/).filter(Boolean);
50
  const sliced = lines.slice(Math.max(0, lines.length - tail));
51
 
@@ -69,12 +87,11 @@ export async function GET(req: NextRequest) {
69
  headers: { "Content-Type": "application/json" },
70
  });
71
  } catch (error) {
72
- return new Response(
73
- JSON.stringify({
74
- error: "Failed to read logs",
75
- message: error instanceof Error ? error.message : String(error),
76
- }),
77
- { status: 500, headers: { "Content-Type": "application/json" } }
78
- );
79
  }
80
  }
 
1
  import { NextRequest } from "next/server";
2
  import { promises as fs } from "fs";
3
+ import os from "os";
4
  import path from "path";
5
 
6
  export const runtime = "nodejs";
7
 
8
+ async function resolveLogFilePath() {
9
+ if (process.env.CHAT_LOG_PATH) return process.env.CHAT_LOG_PATH;
10
+
11
+ const candidates = [
12
+ process.env.DATA_DIR,
13
+ process.env.HF_HOME,
14
+ process.env.HOME,
15
+ "/tmp",
16
+ os.tmpdir(),
17
+ process.cwd(),
18
+ ].filter(Boolean) as string[];
19
+
20
+ for (const dir of candidates) {
21
+ const candidate = path.join(dir, "chat-logs.jsonl");
22
+ try {
23
+ await fs.access(candidate);
24
+ return candidate;
25
+ } catch {
26
+ // try next
27
+ }
28
+ }
29
 
30
+ // Default location if not present yet
31
+ return path.join(os.tmpdir(), "chat-logs.jsonl");
32
+ }
33
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
34
 
35
  function isAuthorized(req: NextRequest) {
 
62
  const raw = rawParam === "1" || rawParam === "true";
63
 
64
  try {
65
+ const logFile = await resolveLogFilePath();
66
+ const file = await fs.readFile(logFile, "utf8");
67
  const lines = file.split(/\r?\n/).filter(Boolean);
68
  const sliced = lines.slice(Math.max(0, lines.length - tail));
69
 
 
87
  headers: { "Content-Type": "application/json" },
88
  });
89
  } catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ const status = message.includes("ENOENT") ? 404 : 500;
92
+ return new Response(JSON.stringify({ error: "Failed to read logs", message }), {
93
+ status,
94
+ headers: { "Content-Type": "application/json" },
95
+ });
 
96
  }
97
  }
src/app/api/stats/route.ts CHANGED
@@ -1,18 +1,35 @@
1
  import { NextRequest } from "next/server";
2
  import { promises as fs } from "fs";
 
3
  import path from "path";
4
 
5
  export const runtime = "nodejs";
6
 
7
- function getWritableBaseDir() {
8
- const envDir = process.env.DATA_DIR;
9
- if (envDir) return envDir;
10
- if (process.env.NODE_ENV === "production") return "/data";
11
- return process.cwd();
12
- }
 
 
 
 
 
13
 
14
- const BASE_DIR = getWritableBaseDir();
15
- const USAGE_FILE = process.env.USAGE_STATS_PATH || path.join(BASE_DIR, "usage-stats.json");
 
 
 
 
 
 
 
 
 
 
 
16
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
17
 
18
  type UsageStats = {
@@ -34,7 +51,8 @@ function isAuthorized(req: NextRequest) {
34
 
35
  async function readUsageStats(): Promise<UsageStats> {
36
  try {
37
- const raw = await fs.readFile(USAGE_FILE, "utf8");
 
38
  const parsed = JSON.parse(raw) as Partial<UsageStats>;
39
  return {
40
  totalRequests: typeof parsed.totalRequests === "number" ? parsed.totalRequests : 0,
 
1
  import { NextRequest } from "next/server";
2
  import { promises as fs } from "fs";
3
+ import os from "os";
4
  import path from "path";
5
 
6
  export const runtime = "nodejs";
7
 
8
+ async function resolveUsageFilePath() {
9
+ if (process.env.USAGE_STATS_PATH) return process.env.USAGE_STATS_PATH;
10
+
11
+ const candidates = [
12
+ process.env.DATA_DIR,
13
+ process.env.HF_HOME,
14
+ process.env.HOME,
15
+ "/tmp",
16
+ os.tmpdir(),
17
+ process.cwd(),
18
+ ].filter(Boolean) as string[];
19
 
20
+ for (const dir of candidates) {
21
+ const candidate = path.join(dir, "usage-stats.json");
22
+ try {
23
+ await fs.access(candidate);
24
+ return candidate;
25
+ } catch {
26
+ // try next
27
+ }
28
+ }
29
+
30
+ // Default location if not present yet
31
+ return path.join(os.tmpdir(), "usage-stats.json");
32
+ }
33
  const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
34
 
35
  type UsageStats = {
 
51
 
52
  async function readUsageStats(): Promise<UsageStats> {
53
  try {
54
+ const usageFile = await resolveUsageFilePath();
55
+ const raw = await fs.readFile(usageFile, "utf8");
56
  const parsed = JSON.parse(raw) as Partial<UsageStats>;
57
  return {
58
  totalRequests: typeof parsed.totalRequests === "number" ? parsed.totalRequests : 0,