87 lines
2.8 KiB
TypeScript
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));
|
|
}
|