const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const { TextDecoder } = require("util"); const { db } = require("./db"); const { getSetting, setSetting } = require("./settings"); const { safeDownloadFilename } = require("./upload-security"); const DATA_DIR = path.join(__dirname, "..", "..", "data", "content-library"); const FILES_DIR = path.join(DATA_DIR, "files"); const INCOMING_DIR = path.join(DATA_DIR, ".incoming"); const TRASH_DIR = path.join(DATA_DIR, ".trash"); const ACCESS_LEVELS = Object.freeze(["locked", "exposed"]); const DEFAULT_STORAGE_RESERVE_BYTES = 512 * 1024 * 1024; const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024 * 1024; const HARD_MAX_FILE_BYTES = 8 * 1024 * 1024 * 1024; const DEFAULT_UPLOAD_MAX_FILES = 20; const HARD_UPLOAD_MAX_FILES = 50; const SIGNED_URL_MAX_TTL_SECONDS = 24 * 60 * 60; const TEXT_RESOURCE_MAX_BYTES = 64 * 1024 * 1024; let initialized = false; const FORMAT_DEFINITIONS = Object.freeze({ ".png": media("image/png", "image", "image", matchPng), ".apng": media("image/apng", "image", "image", matchPng), ".jpg": media("image/jpeg", "image", "image", matchJpeg), ".jpeg": media("image/jpeg", "image", "image", matchJpeg), ".jfif": media("image/jpeg", "image", "image", matchJpeg), ".webp": media("image/webp", "image", "image", matchWebp), ".gif": media("image/gif", "image", "image", matchGif), ".avif": media("image/avif", "image", "image", (buffer) => matchIsoBrand(buffer, ["avif", "avis"])), ".heic": media("image/heic", "image", "image", (buffer) => matchIsoBrand(buffer, ["heic", "heix", "hevc", "hevx", "mif1", "msf1"])), ".heif": media("image/heif", "image", "image", (buffer) => matchIsoBrand(buffer, ["heif", "heim", "heis", "mif1", "msf1"])), ".bmp": media("image/bmp", "image", "image", (buffer) => buffer.subarray(0, 2).toString("ascii") === "BM"), ".tif": media("image/tiff", "image", "image", matchTiff), ".tiff": media("image/tiff", "image", "image", matchTiff), ".ico": media("image/x-icon", "image", "image", (buffer) => startsWith(buffer, [0x00, 0x00, 0x01, 0x00])), ".psd": media("image/vnd.adobe.photoshop", "image", "none", (buffer) => buffer.subarray(0, 4).toString("ascii") === "8BPS"), ".svg": media("image/svg+xml; charset=utf-8", "image", "image", matchSafeSvg, true), ".mp3": media("audio/mpeg", "audio", "audio", matchMp3), ".wav": media("audio/wav", "audio", "audio", (buffer) => matchRiff(buffer, "WAVE")), ".wave": media("audio/wav", "audio", "audio", (buffer) => matchRiff(buffer, "WAVE")), ".ogg": media("audio/ogg", "audio", "audio", matchOgg), ".oga": media("audio/ogg", "audio", "audio", matchOgg), ".opus": media("audio/ogg", "audio", "audio", matchOgg), ".flac": media("audio/flac", "audio", "audio", (buffer) => buffer.subarray(0, 4).toString("ascii") === "fLaC"), ".m4a": media("audio/mp4", "audio", "audio", matchIsoMedia), ".aac": media("audio/aac", "audio", "audio", matchAac), ".weba": media("audio/webm", "audio", "audio", matchEbml), ".mka": media("audio/x-matroska", "audio", "audio", matchEbml), ".wma": media("audio/x-ms-wma", "audio", "audio", matchAsf), ".caf": media("audio/x-caf", "audio", "audio", (buffer) => buffer.subarray(0, 4).toString("ascii") === "caff"), ".aiff": media("audio/aiff", "audio", "audio", (buffer) => matchForm(buffer, ["AIFF", "AIFC"])), ".aif": media("audio/aiff", "audio", "audio", (buffer) => matchForm(buffer, ["AIFF", "AIFC"])), ".mp4": media("video/mp4", "video", "video", matchIsoMedia), ".m4v": media("video/x-m4v", "video", "video", matchIsoMedia), ".mov": media("video/quicktime", "video", "video", matchIsoMedia), ".webm": media("video/webm", "video", "video", matchEbml), ".ogv": media("video/ogg", "video", "video", matchOgg), ".mkv": media("video/x-matroska", "video", "video", matchEbml), ".avi": media("video/x-msvideo", "video", "video", (buffer) => matchRiff(buffer, "AVI ")), ".mpeg": media("video/mpeg", "video", "video", matchMpegVideo), ".mpg": media("video/mpeg", "video", "video", matchMpegVideo), ".ts": media("video/mp2t", "video", "video", matchTransportStream), ".mts": media("video/mp2t", "video", "video", matchTransportStream), ".m2ts": media("video/mp2t", "video", "video", matchTransportStream), ".3gp": media("video/3gpp", "video", "video", matchIsoMedia), ".flv": media("video/x-flv", "video", "video", (buffer) => buffer.subarray(0, 3).toString("ascii") === "FLV"), ".wmv": media("video/x-ms-wmv", "video", "video", matchAsf), ".mxf": media("application/mxf", "video", "video", (buffer) => startsWith(buffer, [0x06, 0x0e, 0x2b, 0x34])), ".vtt": media("text/vtt; charset=utf-8", "caption", "text", matchWebVtt, true), ".webvtt": media("text/vtt; charset=utf-8", "caption", "text", matchWebVtt, true), ".srt": media("application/x-subrip; charset=utf-8", "caption", "text", matchSubRip, true), ".ass": media("text/x-ssa; charset=utf-8", "caption", "text", matchSubStationAlpha, true), ".ssa": media("text/x-ssa; charset=utf-8", "caption", "text", matchSubStationAlpha, true), ".json": media("application/json; charset=utf-8", "data", "text", matchJson, true), ".lottie": media("application/json; charset=utf-8", "data", "text", matchJson, true), ".pdf": media("application/pdf", "document", "pdf", (buffer) => buffer.subarray(0, 5).toString("ascii") === "%PDF-"), ".woff": media("font/woff", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "wOFF"), ".woff2": media("font/woff2", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "wOF2"), ".ttf": media("font/ttf", "font", "font", matchTrueType), ".ttc": media("font/collection", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "ttcf"), ".otf": media("font/otf", "font", "font", (buffer) => buffer.subarray(0, 4).toString("ascii") === "OTTO") }); function media(mime, category, previewKind, matches, text = false) { return Object.freeze({ mime, category, previewKind, matches, text }); } function ensureContentLibrary() { if (initialized) return; fs.mkdirSync(FILES_DIR, { recursive: true }); fs.mkdirSync(INCOMING_DIR, { recursive: true }); fs.mkdirSync(TRASH_DIR, { recursive: true }); ensureSettingDefault("content_storage_limit_bytes", 0); ensureSettingDefault("content_storage_reserve_bytes", DEFAULT_STORAGE_RESERVE_BYTES); ensureSettingDefault("content_max_file_bytes", DEFAULT_MAX_FILE_BYTES); ensureSettingDefault("content_upload_max_files", DEFAULT_UPLOAD_MAX_FILES); cleanupStaleWorkingFiles(); initialized = true; } function ensureSettingDefault(key, value) { if (getSetting(key, null) === null) setSetting(key, value); } function cleanupStaleWorkingFiles() { const cutoff = Date.now() - 24 * 60 * 60 * 1000; for (const directory of [INCOMING_DIR, TRASH_DIR]) { let entries = []; try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { if (!entry.isFile()) continue; const target = path.join(directory, entry.name); try { if (fs.statSync(target).mtimeMs < cutoff) fs.rmSync(target, { force: true }); } catch {} } } } function supportedFormats() { return Object.entries(FORMAT_DEFINITIONS).map(([extension, definition]) => ({ extension, mime: definition.mime, category: definition.category, preview_kind: definition.previewKind })); } function supportedExtensions() { return Object.keys(FORMAT_DEFINITIONS); } function uploadLimits() { return { max_file_bytes: clampInteger( getSetting("content_max_file_bytes", DEFAULT_MAX_FILE_BYTES), 1 * 1024 * 1024, HARD_MAX_FILE_BYTES, DEFAULT_MAX_FILE_BYTES ), max_files: clampInteger( getSetting("content_upload_max_files", DEFAULT_UPLOAD_MAX_FILES), 1, HARD_UPLOAD_MAX_FILES, DEFAULT_UPLOAD_MAX_FILES ) }; } function getStorageStats() { ensureContentLibrary(); const usedRow = db.prepare("SELECT COALESCE(SUM(size), 0) AS used FROM content_resources").get(); const usedBytes = nonNegativeNumber(usedRow?.used); const limitBytes = nonNegativeNumber(getSetting("content_storage_limit_bytes", 0)); const reserveBytes = nonNegativeNumber( getSetting("content_storage_reserve_bytes", DEFAULT_STORAGE_RESERVE_BYTES) ); const disk = diskStats(DATA_DIR); const diskUsableBytes = disk.available ? Math.max(0, disk.available_bytes - reserveBytes) : Number.MAX_SAFE_INTEGER; const quotaRemainingBytes = limitBytes > 0 ? Math.max(0, limitBytes - usedBytes) : Number.MAX_SAFE_INTEGER; const effectiveAvailableBytes = Math.min(diskUsableBytes, quotaRemainingBytes); return { used_bytes: usedBytes, file_count: Number(db.prepare("SELECT COUNT(*) AS count FROM content_resources").get()?.count || 0), limit_bytes: limitBytes, quota_enabled: limitBytes > 0, quota_remaining_bytes: limitBytes > 0 ? quotaRemainingBytes : null, reserve_bytes: reserveBytes, disk_available: disk.available, disk_free_bytes: disk.available ? disk.available_bytes : null, disk_total_bytes: disk.available ? disk.total_bytes : null, disk_usable_bytes: disk.available ? diskUsableBytes : null, effective_available_bytes: Number.isFinite(effectiveAvailableBytes) ? effectiveAvailableBytes : null, usage_percent: limitBytes > 0 ? percent(usedBytes, limitBytes) : disk.available ? percent(disk.total_bytes - disk.available_bytes, disk.total_bytes) : 0, quota_usage_percent: limitBytes > 0 ? percent(usedBytes, limitBytes) : null, disk_usage_percent: disk.available ? percent(disk.total_bytes - disk.available_bytes, disk.total_bytes) : null, ...uploadLimits() }; } function diskStats(target) { try { if (typeof fs.statfsSync !== "function") return { available: false }; const stat = fs.statfsSync(target); return { available: true, available_bytes: Number(stat.bavail) * Number(stat.bsize), total_bytes: Number(stat.blocks) * Number(stat.bsize) }; } catch { return { available: false }; } } function preflightIncomingRequest(contentLength) { const bytes = nonNegativeNumber(contentLength); if (!bytes) return { ok: true }; const stats = getStorageStats(); if (stats.effective_available_bytes !== null && bytes > stats.effective_available_bytes) { return { ok: false, status: 507, reason: "The upload is larger than the space currently available to Lumi." }; } return { ok: true }; } async function importUploadedFiles(files, options = {}) { ensureContentLibrary(); const list = Array.isArray(files) ? files.filter(Boolean) : []; const limits = uploadLimits(); if (!list.length) throw new Error("Choose at least one file to upload."); if (list.length > limits.max_files) { throw new Error(`Upload no more than ${limits.max_files} files at once.`); } const accessLevel = normalizeAccessLevel(options.access_level); const prepared = []; try { for (const file of list) { prepared.push(await inspectUpload(file, limits.max_file_bytes)); } ensureImportCapacity(prepared.reduce((sum, item) => sum + item.size, 0)); const moved = []; try { for (const item of prepared) { const id = crypto.randomUUID(); const storedName = `${id}${item.extension}`; const destination = safeStoragePath(storedName); moveFile(item.temp_path, destination); moved.push(destination); const token = accessLevel === "exposed" ? createStoredToken() : null; item.row = { id, display_name: displayNameFromFilename(item.original_name), original_name: item.original_name, stored_name: storedName, mime: item.mime, extension: item.extension, category: item.category, preview_kind: item.preview_kind, size: item.size, checksum_sha256: item.checksum_sha256, access_level: accessLevel, public_token_hash: token?.hash || null, public_token_encrypted: token?.encrypted || null, uploaded_by: options.uploaded_by || null, created_at: Date.now(), updated_at: Date.now() }; } const insert = db.prepare(` INSERT INTO content_resources ( id, display_name, original_name, stored_name, mime, extension, category, preview_kind, size, checksum_sha256, access_level, public_token_hash, public_token_encrypted, uploaded_by, created_at, updated_at ) VALUES ( @id, @display_name, @original_name, @stored_name, @mime, @extension, @category, @preview_kind, @size, @checksum_sha256, @access_level, @public_token_hash, @public_token_encrypted, @uploaded_by, @created_at, @updated_at ) `); const transaction = db.transaction((items) => { items.forEach((item) => insert.run(item.row)); }); transaction(prepared); return prepared.map((item) => hydrateResource(item.row)); } catch (error) { moved.forEach((target) => { try { fs.rmSync(target, { force: true }); } catch {} }); throw error; } } finally { list.forEach((file) => { if (!file?.path) return; try { fs.rmSync(file.path, { force: true }); } catch {} }); } } async function inspectUpload(file, maxFileBytes) { if (!file?.path) throw new Error("One of the uploaded files could not be read."); const originalName = safeDownloadFilename(file.originalname, "resource"); const extension = path.extname(originalName).toLowerCase(); const definition = FORMAT_DEFINITIONS[extension]; if (!definition) { throw new Error(`${extension || "That file type"} is not supported by Lumi's media library.`); } const stat = fs.statSync(file.path); if (!stat.isFile() || stat.size < 1) throw new Error(`${originalName} is empty.`); if (stat.size > maxFileBytes) { throw new Error(`${originalName} is larger than the configured per-file limit (${formatBytes(maxFileBytes)}).`); } if (definition.text && stat.size > TEXT_RESOURCE_MAX_BYTES) { throw new Error(`${originalName} is too large for a text-based resource (${formatBytes(TEXT_RESOURCE_MAX_BYTES)} maximum).`); } const sample = definition.text ? fs.readFileSync(file.path) : readPrefix(file.path, 128 * 1024); if (!definition.matches(sample)) { throw new Error(`${originalName} does not match its filename extension or is not a valid supported media file.`); } return { temp_path: file.path, original_name: originalName, extension, mime: definition.mime, category: definition.category, preview_kind: definition.previewKind, size: stat.size, checksum_sha256: await hashFile(file.path) }; } function ensureImportCapacity(bytes) { const stats = getStorageStats(); if (stats.quota_enabled && bytes > stats.quota_remaining_bytes) { throw new Error("Uploading these files would exceed Lumi's configured content-library limit."); } if (stats.disk_available && stats.disk_free_bytes < stats.reserve_bytes) { throw new Error("The upload reached Lumi's reserved free-space boundary. Remove files or lower the reserve before retrying."); } if (stats.disk_available && bytes > stats.disk_usable_bytes) { throw new Error("Lumi cannot safely finish storing these files without crossing the reserved free-space boundary."); } } function listResources(filters = {}) { ensureContentLibrary(); const where = []; const params = {}; const category = String(filters.category || "").trim().toLowerCase(); const accessLevel = String(filters.access_level || "").trim().toLowerCase(); const search = String(filters.search || "").trim(); if (category) { where.push("category = @category"); params.category = category; } if (ACCESS_LEVELS.includes(accessLevel)) { where.push("access_level = @access_level"); params.access_level = accessLevel; } if (search) { where.push("(display_name LIKE @search ESCAPE '\\' OR original_name LIKE @search ESCAPE '\\' OR mime LIKE @search ESCAPE '\\')"); params.search = `%${search.replace(/[\\%_]/g, "\\$&")}%`; } const orderBy = filters.sort === "name" ? "display_name COLLATE NOCASE ASC" : filters.sort === "size" ? "size DESC" : "created_at DESC"; const sql = `SELECT * FROM content_resources${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY ${orderBy}`; return db.prepare(sql).all(params).map(hydrateResource); } function getResource(id) { ensureContentLibrary(); const row = db.prepare("SELECT * FROM content_resources WHERE id = ?").get(String(id || "")); return row ? hydrateResource(row) : null; } function getResourceByExposedToken(token) { ensureContentLibrary(); const row = db.prepare( "SELECT * FROM content_resources WHERE public_token_hash = ? AND access_level = 'exposed'" ).get(tokenHash(token)); return row ? hydrateResource(row) : null; } function updateResourceName(id, value) { const current = requireResource(id); const displayName = normalizeDisplayName(value, current.display_name); db.prepare("UPDATE content_resources SET display_name = ?, updated_at = ? WHERE id = ?") .run(displayName, Date.now(), current.id); return getResource(current.id); } function setResourceAccess(id, value) { const current = requireResource(id); const accessLevel = normalizeAccessLevel(value); if (current.access_level === accessLevel) return current; const token = accessLevel === "exposed" ? createStoredToken() : null; db.prepare(` UPDATE content_resources SET access_level = ?, public_token_hash = ?, public_token_encrypted = ?, updated_at = ? WHERE id = ? `).run(accessLevel, token?.hash || null, token?.encrypted || null, Date.now(), current.id); return getResource(current.id); } function deleteResource(id) { const current = requireResource(id); const source = safeStoragePath(current.stored_name); if (fs.existsSync(source)) { const stat = fs.lstatSync(source); if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("The resource storage entry is not a regular file."); // Deleting directly is more reliable than renaming to a temporary trash // path on Windows network drives. If the database write then fails, the // retained metadata can still be removed safely by retrying this action. fs.rmSync(source, { force: true }); } db.prepare("DELETE FROM content_resources WHERE id = ?").run(current.id); return current; } function updateStorageSettings(values = {}) { for (const field of ["limit_gib", "reserve_gib", "max_file_mib", "max_files"]) { if (values[field] === undefined || values[field] === null || String(values[field]).trim() === "") { throw new Error("Complete every storage-limit field before saving."); } } const limitBytes = gibToBytes(values.limit_gib, { allowZero: true, max: 1024 * 1024 }); const reserveBytes = gibToBytes(values.reserve_gib, { allowZero: true, max: 1024 }); const maxFileBytes = mibToBytes(values.max_file_mib, { min: 1, max: HARD_MAX_FILE_BYTES / (1024 * 1024), fallback: DEFAULT_MAX_FILE_BYTES }); const maxFiles = clampInteger(values.max_files, 1, HARD_UPLOAD_MAX_FILES, DEFAULT_UPLOAD_MAX_FILES); const usedBytes = getStorageStats().used_bytes; if (limitBytes > 0 && limitBytes < usedBytes) { throw new Error(`The storage limit cannot be lower than the ${formatBytes(usedBytes)} already stored.`); } db.transaction(() => { setSetting("content_storage_limit_bytes", limitBytes); setSetting("content_storage_reserve_bytes", reserveBytes); setSetting("content_max_file_bytes", maxFileBytes); setSetting("content_upload_max_files", maxFiles); })(); return getStorageStats(); } function resourceFilePath(resourceOrId) { const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId; if (!resource?.stored_name) throw new Error("Resource storage metadata is missing."); const target = safeStoragePath(resource.stored_name); const stat = fs.lstatSync(target); if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("The resource file is unavailable."); return target; } function openReadStream(id, options = {}) { const resource = requireResource(id); const filePath = resourceFilePath(resource); return { resource, file_path: filePath, stream: fs.createReadStream(filePath, options) }; } function publicUrl(resourceOrId, baseUrl = "") { const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId; if (resource.access_level !== "exposed" || !resource.public_token) return null; return joinBaseUrl(baseUrl, `/media/${encodeURIComponent(resource.public_token)}/${encodeURIComponent(resource.original_name)}`); } function adminUrl(resourceOrId, baseUrl = "") { const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId; return joinBaseUrl(baseUrl, `/admin/resources/${encodeURIComponent(resource.id)}/raw/${encodeURIComponent(resource.original_name)}`); } function downloadUrl(resourceOrId, baseUrl = "") { const resource = typeof resourceOrId === "string" ? requireResource(resourceOrId) : resourceOrId; return joinBaseUrl(baseUrl, `/admin/resources/${encodeURIComponent(resource.id)}/download/${encodeURIComponent(resource.original_name)}`); } function createSignedUrl(id, options = {}) { const resource = requireResource(id); const ttlSeconds = clampInteger( options.ttl_seconds, 1, SIGNED_URL_MAX_TTL_SECONDS, 5 * 60 ); const expires = Math.floor(Date.now() / 1000) + ttlSeconds; const signature = signedResourceSignature(resource.id, expires); const route = `/internal/media/${encodeURIComponent(resource.id)}/${encodeURIComponent(resource.original_name)}?expires=${expires}&sig=${encodeURIComponent(signature)}`; return joinBaseUrl(options.base_url || "", route); } function verifySignedResource(id, expires, signature) { const normalizedId = String(id || ""); const expiry = Number(expires); if (!normalizedId || !Number.isFinite(expiry) || expiry < Math.floor(Date.now() / 1000)) return null; if (expiry > Math.floor(Date.now() / 1000) + SIGNED_URL_MAX_TTL_SECONDS + 60) return null; const expected = Buffer.from(signedResourceSignature(normalizedId, expiry)); const received = Buffer.from(String(signature || "")); if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) return null; return getResource(normalizedId); } function serializeResource(resource, options = {}) { const baseUrl = options.base_url || ""; return { id: resource.id, display_name: resource.display_name, original_name: resource.original_name, mime: resource.mime, extension: resource.extension, category: resource.category, preview_kind: resource.preview_kind, size: resource.size, size_display: formatBytes(resource.size), checksum_sha256: resource.checksum_sha256, access_level: resource.access_level, created_at: resource.created_at, updated_at: resource.updated_at, admin_url: adminUrl(resource, baseUrl), download_url: downloadUrl(resource, baseUrl), public_url: publicUrl(resource, baseUrl) }; } function frameworkApi() { return Object.freeze({ list: (filters) => listResources(filters).map((resource) => serializeResource(resource)), get: (id) => { const resource = getResource(id); return resource ? serializeResource(resource) : null; }, resolvePath: resourceFilePath, openReadStream, publicUrl, createSignedUrl, storage: getStorageStats, supportedFormats }); } function hydrateResource(row) { let publicToken = ""; if (row.public_token_encrypted) { try { publicToken = decryptSecret(row.public_token_encrypted); } catch { publicToken = ""; } } return { ...row, size: Number(row.size), created_at: Number(row.created_at), updated_at: Number(row.updated_at), public_token: publicToken }; } function requireResource(id) { const resource = getResource(id); if (!resource) { const error = new Error("Resource not found."); error.status = 404; throw error; } return resource; } function safeStoragePath(storedName) { const base = path.resolve(FILES_DIR); const target = path.resolve(FILES_DIR, path.basename(String(storedName || ""))); if (path.dirname(target) !== base) throw new Error("Invalid resource storage path."); return target; } function normalizeAccessLevel(value) { const normalized = String(value || "locked").trim().toLowerCase(); if (!ACCESS_LEVELS.includes(normalized)) throw new Error("Choose locked or exposed access."); return normalized; } function normalizeDisplayName(value, fallback = "Resource") { const normalized = String(value || "") .normalize("NFKC") .replace(/[\u0000-\u001f\u007f]/g, "") .trim() .slice(0, 160); return normalized || fallback; } function displayNameFromFilename(filename) { const extension = path.extname(filename); return normalizeDisplayName(path.basename(filename, extension), filename); } function moveFile(source, destination) { try { fs.renameSync(source, destination); } catch (error) { if (!["EXDEV", "EPERM", "EACCES"].includes(error.code)) throw error; fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL); fs.rmSync(source, { force: true }); } } function readPrefix(filePath, length) { const descriptor = fs.openSync(filePath, "r"); try { const buffer = Buffer.alloc(length); const bytesRead = fs.readSync(descriptor, buffer, 0, length, 0); return buffer.subarray(0, bytesRead); } finally { fs.closeSync(descriptor); } } function hashFile(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash("sha256"); const stream = fs.createReadStream(filePath); stream.on("data", (chunk) => hash.update(chunk)); stream.on("error", reject); stream.on("end", () => resolve(hash.digest("hex"))); }); } function encryptionKey() { const secret = getSetting("session_secret", ""); if (!secret) throw new Error("Lumi's session secret is not initialized."); return crypto.createHash("sha256").update(`lumi-content-library:${secret}`).digest(); } function createStoredToken() { const token = crypto.randomBytes(32).toString("base64url"); return { token, hash: tokenHash(token), encrypted: encryptSecret(token) }; } function tokenHash(token) { return crypto.createHash("sha256").update(String(token || "")).digest("hex"); } function encryptSecret(value) { const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey(), iv); const encrypted = Buffer.concat([cipher.update(String(value), "utf8"), cipher.final()]); const tag = cipher.getAuthTag(); return `v1.${iv.toString("base64url")}.${tag.toString("base64url")}.${encrypted.toString("base64url")}`; } function decryptSecret(value) { const [version, ivValue, tagValue, encryptedValue] = String(value || "").split("."); if (version !== "v1" || !ivValue || !tagValue || !encryptedValue) throw new Error("Invalid stored token."); const decipher = crypto.createDecipheriv( "aes-256-gcm", encryptionKey(), Buffer.from(ivValue, "base64url") ); decipher.setAuthTag(Buffer.from(tagValue, "base64url")); return Buffer.concat([ decipher.update(Buffer.from(encryptedValue, "base64url")), decipher.final() ]).toString("utf8"); } function signedResourceSignature(id, expires) { return crypto.createHmac("sha256", encryptionKey()) .update(`resource:${id}:${expires}`) .digest("base64url"); } function joinBaseUrl(baseUrl, route) { return `${String(baseUrl || "").replace(/\/$/, "")}${route}`; } function matchPng(buffer) { return startsWith(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); } function matchJpeg(buffer) { return startsWith(buffer, [0xff, 0xd8, 0xff]); } function matchWebp(buffer) { return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP"; } function matchGif(buffer) { const header = buffer.subarray(0, 6).toString("ascii"); return header === "GIF87a" || header === "GIF89a"; } function matchTiff(buffer) { return startsWith(buffer, [0x49, 0x49, 0x2a, 0x00]) || startsWith(buffer, [0x4d, 0x4d, 0x00, 0x2a]); } function matchMp3(buffer) { return buffer.subarray(0, 3).toString("ascii") === "ID3" || (buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0); } function matchAac(buffer) { return buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xf6) === 0xf0; } function matchOgg(buffer) { return buffer.subarray(0, 4).toString("ascii") === "OggS"; } function matchRiff(buffer, formType) { return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === formType; } function matchForm(buffer, types) { return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "FORM" && types.includes(buffer.subarray(8, 12).toString("ascii")); } function matchIsoMedia(buffer) { return buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp"; } function matchIsoBrand(buffer, brands) { if (!matchIsoMedia(buffer)) return false; const brandBlock = buffer.subarray(8, Math.min(buffer.length, 64)).toString("ascii"); return brands.some((brand) => brandBlock.includes(brand)); } function matchEbml(buffer) { return startsWith(buffer, [0x1a, 0x45, 0xdf, 0xa3]); } function matchMpegVideo(buffer) { for (let index = 0; index < Math.min(buffer.length - 3, 4096); index += 1) { if (buffer[index] === 0x00 && buffer[index + 1] === 0x00 && buffer[index + 2] === 0x01 && [0xb3, 0xba].includes(buffer[index + 3])) return true; } return false; } function matchTransportStream(buffer) { const offsets = [0, 4]; return offsets.some((offset) => buffer.length > offset + 376 && buffer[offset] === 0x47 && buffer[offset + 188] === 0x47 && buffer[offset + 376] === 0x47); } function matchWebVtt(buffer) { const text = decodeText(buffer).replace(/^\uFEFF/, ""); return /^WEBVTT(?:[ \t]|\r?\n)/.test(text); } function matchSubRip(buffer) { const text = decodeText(buffer); return /^\s*\d+\s*\r?\n\s*\d{1,2}:\d{2}:\d{2}[,.]\d{3}\s*-->\s*\d{1,2}:\d{2}:\d{2}[,.]\d{3}/m.test(text); } function matchJson(buffer) { try { const value = JSON.parse(decodeText(buffer)); return value !== null && typeof value === "object"; } catch { return false; } } function matchAsf(buffer) { return startsWith(buffer, [0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c]); } function matchSubStationAlpha(buffer) { const text = decodeText(buffer); return /^\s*\[Script Info\]/im.test(text) && /^\s*\[Events\]/im.test(text); } function matchSafeSvg(buffer) { const source = decodeText(buffer).replace(/^\uFEFF/, "").trim(); if (!source || (!/^<\?xml\b[^>]*>\s*/i.test(source) && !/^= bytes.length && bytes.every((byte, index) => buffer[index] === byte); } function formatBytes(value) { const bytes = nonNegativeNumber(value); if (bytes < 1024) return `${bytes} B`; const units = ["KB", "MB", "GB", "TB"]; let amount = bytes; let unit = "B"; for (const next of units) { amount /= 1024; unit = next; if (amount < 1024) break; } return `${amount >= 10 ? amount.toFixed(1) : amount.toFixed(2)} ${unit}`; } function percent(value, total) { if (!total) return 0; return Math.max(0, Math.min(100, Math.round((Number(value) / Number(total)) * 1000) / 10)); } function nonNegativeNumber(value) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : 0; } function clampInteger(value, min, max, fallback) { const number = Math.floor(Number(value)); if (!Number.isFinite(number)) return Math.floor(fallback); return Math.max(min, Math.min(max, number)); } function gibToBytes(value, options = {}) { const number = Number(value); if (options.allowZero && number === 0) return 0; if (!Number.isFinite(number) || number < 0) throw new Error("Storage values must be positive numbers."); const bounded = Math.min(options.max || number, number); return Math.floor(bounded * 1024 * 1024 * 1024); } function mibToBytes(value, options = {}) { const number = Number(value); const fallbackBytes = Number(options.fallback || DEFAULT_MAX_FILE_BYTES); if (!Number.isFinite(number)) return fallbackBytes; const bounded = Math.max(options.min || 1, Math.min(options.max || number, number)); return Math.floor(bounded * 1024 * 1024); } module.exports = { ACCESS_LEVELS, DATA_DIR, FILES_DIR, FORMAT_DEFINITIONS, HARD_MAX_FILE_BYTES, HARD_UPLOAD_MAX_FILES, INCOMING_DIR, adminUrl, createSignedUrl, deleteResource, downloadUrl, ensureContentLibrary, formatBytes, frameworkApi, getResource, getResourceByExposedToken, getStorageStats, importUploadedFiles, listResources, normalizeAccessLevel, openReadStream, preflightIncomingRequest, publicUrl, resourceFilePath, serializeResource, setResourceAccess, supportedExtensions, supportedFormats, updateResourceName, updateStorageSettings, uploadLimits, verifySignedResource };