131 lines
4.1 KiB
JavaScript
131 lines
4.1 KiB
JavaScript
import { NextResponse } from 'next/server';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
|
|
// 数据持久化 API — 将所有用户数据存储到本地文件系统
|
|
// 每个用户有独立的目录(通过 userId cookie 隔离)
|
|
|
|
const DATA_ROOT = process.env.DATA_DIR || path.join(process.cwd(), 'data');
|
|
|
|
// 从请求中提取或创建用户ID
|
|
function getUserId(request) {
|
|
// 优先从 cookie 读取
|
|
const cookies = request.headers.get('cookie') || '';
|
|
const match = cookies.match(/author-uid=([a-zA-Z0-9_-]+)/);
|
|
if (match) return match[1];
|
|
return null;
|
|
}
|
|
|
|
// 确保目录存在
|
|
async function ensureDir(dirPath) {
|
|
try {
|
|
await fs.mkdir(dirPath, { recursive: true });
|
|
} catch (e) {
|
|
if (e.code !== 'EEXIST') throw e;
|
|
}
|
|
}
|
|
|
|
// key → 文件路径映射(防止路径穿越)
|
|
function resolveFilePath(userId, key) {
|
|
// 仅允许字母、数字、连字符、下划线、斜杠、点
|
|
const safeKey = key.replace(/[^a-zA-Z0-9\-_./]/g, '');
|
|
if (!safeKey || safeKey.includes('..')) {
|
|
throw new Error('Invalid storage key');
|
|
}
|
|
const userDir = path.join(DATA_ROOT, userId);
|
|
const filePath = path.join(userDir, safeKey + '.json');
|
|
|
|
// 安全检查:确保路径在用户目录内
|
|
const resolvedPath = path.resolve(filePath);
|
|
const resolvedUserDir = path.resolve(userDir);
|
|
if (!resolvedPath.startsWith(resolvedUserDir)) {
|
|
throw new Error('Path traversal detected');
|
|
}
|
|
|
|
return filePath;
|
|
}
|
|
|
|
// GET /api/storage?key=xxx — 读取数据
|
|
export async function GET(request) {
|
|
try {
|
|
const userId = getUserId(request);
|
|
if (!userId) {
|
|
return NextResponse.json({ error: 'No user ID' }, { status: 401 });
|
|
}
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const key = searchParams.get('key');
|
|
if (!key) {
|
|
return NextResponse.json({ error: 'Missing key parameter' }, { status: 400 });
|
|
}
|
|
|
|
const filePath = resolveFilePath(userId, key);
|
|
|
|
try {
|
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
return NextResponse.json({ data: JSON.parse(content) });
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT') {
|
|
return NextResponse.json({ data: null });
|
|
}
|
|
throw e;
|
|
}
|
|
} catch (error) {
|
|
console.error('Storage GET error:', error);
|
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// POST /api/storage — 写入数据 { key, value }
|
|
export async function POST(request) {
|
|
try {
|
|
const userId = getUserId(request);
|
|
if (!userId) {
|
|
return NextResponse.json({ error: 'No user ID' }, { status: 401 });
|
|
}
|
|
|
|
const { key, value } = await request.json();
|
|
if (!key) {
|
|
return NextResponse.json({ error: 'Missing key' }, { status: 400 });
|
|
}
|
|
|
|
const filePath = resolveFilePath(userId, key);
|
|
await ensureDir(path.dirname(filePath));
|
|
await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf-8');
|
|
|
|
return NextResponse.json({ ok: true });
|
|
} catch (error) {
|
|
console.error('Storage POST error:', error);
|
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// DELETE /api/storage?key=xxx — 删除数据
|
|
export async function DELETE(request) {
|
|
try {
|
|
const userId = getUserId(request);
|
|
if (!userId) {
|
|
return NextResponse.json({ error: 'No user ID' }, { status: 401 });
|
|
}
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const key = searchParams.get('key');
|
|
if (!key) {
|
|
return NextResponse.json({ error: 'Missing key parameter' }, { status: 400 });
|
|
}
|
|
|
|
const filePath = resolveFilePath(userId, key);
|
|
|
|
try {
|
|
await fs.unlink(filePath);
|
|
} catch (e) {
|
|
if (e.code !== 'ENOENT') throw e;
|
|
}
|
|
|
|
return NextResponse.json({ ok: true });
|
|
} catch (error) {
|
|
console.error('Storage DELETE error:', error);
|
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
}
|
|
}
|