Files
boss/src/lib/boss-storage-server-file.ts
2026-03-29 17:06:54 +08:00

87 lines
2.8 KiB
TypeScript

import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import type { StoreAttachmentParams, StoredAttachmentRecord } from "@/lib/boss-storage";
import { sanitizeFileName } from "@/lib/boss-attachments";
function detectRuntimeRoot(startDir: string) {
let current = startDir;
while (true) {
if (
path.basename(current) === "boss" &&
path.basename(path.dirname(current)) === "code"
) {
return current;
}
const parent = path.dirname(current);
if (parent === current) {
return startDir;
}
current = parent;
}
}
function resolveRuntimeRoot() {
if (process.env.BOSS_RUNTIME_ROOT?.trim()) {
return path.resolve(process.env.BOSS_RUNTIME_ROOT);
}
if (process.env.BOSS_STATE_FILE?.trim()) {
return path.dirname(path.dirname(path.resolve(process.env.BOSS_STATE_FILE)));
}
return detectRuntimeRoot(process.cwd());
}
function getUploadsRoot() {
return path.resolve(resolveRuntimeRoot(), "data", "uploads");
}
function accountStorageSegment(account: string) {
const normalized = account.normalize("NFKC").trim();
const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
return `acct-${digest}`;
}
function normalizeUploadsRelativePath(storagePath: string) {
const normalized = storagePath.replace(/\\/g, "/").replace(/^\/+/, "");
return normalized.replace(/^data\/uploads\/+/, "");
}
function resolvePathWithinUploadsRoot(storagePath: string) {
const uploadsRoot = getUploadsRoot();
const candidate = path.resolve(uploadsRoot, normalizeUploadsRelativePath(storagePath));
const relative = path.relative(uploadsRoot, candidate);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error("ATTACHMENT_STORAGE_PATH_OUTSIDE_UPLOADS_ROOT");
}
return candidate;
}
export async function storeServerFileAttachment(
params: StoreAttachmentParams,
): Promise<StoredAttachmentRecord> {
const now = new Date();
const relativePath = path.posix.join(
"data",
"uploads",
accountStorageSegment(params.account),
String(now.getUTCFullYear()),
String(now.getUTCMonth() + 1).padStart(2, "0"),
`${params.messageId}-${sanitizeFileName(params.fileName)}`,
);
const absolutePath = resolvePathWithinUploadsRoot(relativePath);
await mkdir(path.dirname(absolutePath), { recursive: true });
await writeFile(absolutePath, params.buffer);
return {
storageBackend: "server_file",
storagePath: relativePath,
};
}
export function resolveServerFileAttachmentAbsolutePath(storagePath: string) {
return resolvePathWithinUploadsRoot(storagePath);
}
export async function readServerFileAttachmentBuffer(storagePath: string) {
return readFile(resolveServerFileAttachmentAbsolutePath(storagePath));
}