-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstorage.ts
More file actions
141 lines (124 loc) · 4.35 KB
/
Copy pathstorage.ts
File metadata and controls
141 lines (124 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { put, del } from "@vercel/blob";
import { kv } from "@vercel/kv";
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
import path from "path";
export interface Problem {
id: string;
imageUrl: string;
explanation: string;
createdAt: string;
studentName?: string;
questionNumber?: number;
}
export interface CurrentProblem {
id: string;
imageUrl: string;
explanation: string;
createdAt: string;
status: "solving" | "done";
studentName?: string;
questionNumber?: number;
}
export async function getNextQuestionNumber(): Promise<number> {
const problems = await getProblems();
return problems.length + 1;
}
// ---------------------------------------------------------------------------
// Backend selection
// KV_REST_API_URL set → Vercel KV (production / preview)
// otherwise → local filesystem (local dev without KV)
// ---------------------------------------------------------------------------
const isProd = process.env.NODE_ENV === "production";
export const useKV = isProd && !!process.env.KV_REST_API_URL;
export const useBlob = isProd && !!process.env.BLOB_READ_WRITE_TOKEN;
// Local paths
const DATA_DIR = path.join(process.cwd(), "data");
export const UPLOADS_DIR = path.join(process.cwd(), "public", "uploads");
const PROBLEMS_FILE = path.join(DATA_DIR, "problems.json");
const CURRENT_FILE = path.join(DATA_DIR, "current.json");
async function ensureDirs() {
await mkdir(DATA_DIR, { recursive: true });
await mkdir(UPLOADS_DIR, { recursive: true });
}
// ---------------------------------------------------------------------------
// Problems
// ---------------------------------------------------------------------------
export async function getProblems(): Promise<Problem[]> {
if (useKV) return (await kv.get<Problem[]>("problems")) ?? [];
try {
return JSON.parse(await readFile(PROBLEMS_FILE, "utf-8"));
} catch {
return [];
}
}
export async function saveProblem(problem: Problem): Promise<void> {
const problems = await getProblems();
problems.unshift(problem);
if (useKV) {
await kv.set("problems", problems);
} else {
await ensureDirs();
await writeFile(PROBLEMS_FILE, JSON.stringify(problems, null, 2));
}
}
export async function getProblemById(id: string): Promise<Problem | undefined> {
return (await getProblems()).find((p) => p.id === id);
}
export async function deleteProblem(id: string): Promise<void> {
const all = await getProblems();
const target = all.find((p) => p.id === id);
const problems = all.filter((p) => p.id !== id);
if (useKV) {
await kv.set("problems", problems);
} else {
await writeFile(PROBLEMS_FILE, JSON.stringify(problems, null, 2));
}
if (target?.imageUrl) await deleteImage(target.imageUrl);
}
// ---------------------------------------------------------------------------
// Current problem (real-time state)
// ---------------------------------------------------------------------------
export async function getCurrentProblem(): Promise<CurrentProblem | null> {
if (useKV) return kv.get<CurrentProblem>("current");
try {
return JSON.parse(await readFile(CURRENT_FILE, "utf-8"));
} catch {
return null;
}
}
export async function setCurrentProblem(problem: CurrentProblem): Promise<void> {
if (useKV) {
await kv.set("current", problem);
} else {
await ensureDirs();
await writeFile(CURRENT_FILE, JSON.stringify(problem));
}
}
// ---------------------------------------------------------------------------
// Image upload (always Vercel Blob in production, local otherwise)
// ---------------------------------------------------------------------------
export async function deleteImage(imageUrl: string): Promise<void> {
try {
if (imageUrl.startsWith("/uploads/")) {
// path.basename guards against traversal in a stored URL
await unlink(path.join(UPLOADS_DIR, path.basename(imageUrl)));
} else if (useBlob) {
await del(imageUrl);
}
} catch {
// Best-effort: a missing file/blob shouldn't block record deletion
}
}
export async function uploadImage(
buffer: Buffer,
filename: string,
contentType: string
): Promise<string> {
if (useBlob) {
const blob = await put(filename, buffer, { access: "private", contentType });
return blob.url;
}
await ensureDirs();
await writeFile(path.join(UPLOADS_DIR, filename), buffer);
return `/uploads/${filename}`;
}