This commit is contained in:
rgrgogu
2026-05-14 12:59:42 +08:00
parent 06c4286c4a
commit c3663572ae
12 changed files with 570 additions and 536 deletions
+40 -76
View File
@@ -12,7 +12,7 @@
// CHIBISAFE_ALBUM_ARCHIVED – album UUID used as the "trash" / archived album
const FormData = require("form-data");
const fetch = require("node-fetch"); // npm i node-fetch@2 (CJS-compatible)
const axios = require("axios");
// ─── Config ───────────────────────────────────────────────────────────────────
@@ -24,38 +24,23 @@ const ALBUMS = {
videos: process.env.CHIBISAFE_ALBUM_VIDEOS || null,
documents: process.env.CHIBISAFE_ALBUM_DOCUMENTS || null,
thumbnails: process.env.CHIBISAFE_ALBUM_THUMBNAILS || null,
images: process.env.CHIBISAFE_ALBUM_IMAGES || null, // ← new
archived: process.env.CHIBISAFE_ALBUM_ARCHIVED || null,
};
// ─── Internal helpers ─────────────────────────────────────────────────────────
/**
* Resolve which Chibisafe album UUID should receive a file based on owner_type.
* owner_type is the single source of truth for album routing:
*
* "avatar" → avatars album (profile pictures)
* "video" → videos album (course/content videos)
* "document" → documents album (pdf, docx, ppt, txt…)
* "thumbnail" → thumbnails album (video cover images)
* "image" → no album (general-purpose images)
* anything else / null → no album
*
* @param {string} ownerType – value of the asset's owner_type field
* @returns {string|null}
*/
function resolveAlbumUuid(ownerType = "") {
switch (ownerType) {
case "avatar": return ALBUMS.avatars;
case "video": return ALBUMS.videos;
case "document": return ALBUMS.documents;
case "thumbnail": return ALBUMS.thumbnails;
default: return null; // "image" and unknowns → no album
case "image": return ALBUMS.images;
default: return null;
}
}
/**
* Build default headers for every Chibisafe request.
*/
function baseHeaders(extra = {}) {
return {
"x-api-key": API_KEY,
@@ -63,44 +48,41 @@ function baseHeaders(extra = {}) {
};
}
// ─── Axios instance ───────────────────────────────────────────────────────────
const chibi = axios.create({
baseURL: BASE_URL,
maxBodyLength: Infinity, // ← required for large file uploads
maxContentLength: Infinity,
});
/**
* Thin fetch wrapper that throws a descriptive error on non-2xx.
* Thin axios wrapper that throws a descriptive error on non-2xx.
*/
async function chibiRequest(path, options = {}) {
const url = `${BASE_URL}${path}`;
const res = await fetch(url, options);
let body;
async function chibiRequest(path, { method = "GET", headers = {}, data } = {}) {
try {
body = await res.json();
} catch {
body = {};
const res = await chibi.request({
url: path,
method,
headers,
data,
});
return res.data;
} catch (err) {
const status = err.response?.status;
const body = err.response?.data ?? {};
const msg = body?.message || body?.error || err.message;
const friendly = new Error(`[Chibisafe] ${status ?? "?"} – ${msg}`);
friendly.status = status;
friendly.chibiBody = body;
throw friendly;
}
if (!res.ok) {
const msg = body?.message || body?.error || res.statusText;
const err = new Error(`[Chibisafe] ${res.status} – ${msg}`);
err.status = res.status;
err.chibiBody = body;
throw err;
}
return body;
}
// ─── Public API ───────────────────────────────────────────────────────────────
/**
* Upload a file to Chibisafe, optionally straight into a typed album.
*
* @param {object} opts
* @param {Buffer} opts.buffer – raw file bytes
* @param {string} opts.originalname – original filename (for Content-Disposition)
* @param {string} opts.mimetype – MIME type
* @param {string} opts.ownerType – asset owner_type value used to resolve the album
* ("avatar" | "video" | "document" | "thumbnail" | "image")
*
* @returns {Promise<{ uuid: string, url: string, name: string }>}
*/
async function uploadFile({ buffer, originalname, mimetype, ownerType = "" }) {
if (!BASE_URL || !API_KEY) {
@@ -115,21 +97,16 @@ async function uploadFile({ buffer, originalname, mimetype, ownerType = "" }) {
contentType: mimetype,
});
const headers = {
...baseHeaders(),
...form.getHeaders(),
// Pass the album UUID at upload time so the file lands in the right album
// in a single round-trip (official Chibisafe header).
...(albumUuid ? { albumuuid: albumUuid } : {}),
};
const data = await chibiRequest("/api/upload", {
method: "POST",
headers,
body: form,
headers: {
...baseHeaders(),
...form.getHeaders(), // ← axios needs these
...(albumUuid ? { albumuuid: albumUuid } : {}),
},
data: form,
});
// Chibisafe returns: { name, uuid, url, ... }
return {
uuid: data.uuid,
url: data.url,
@@ -139,10 +116,6 @@ async function uploadFile({ buffer, originalname, mimetype, ownerType = "" }) {
/**
* Permanently delete one file from Chibisafe by its UUID.
* Used for rollback cleanup when a DB transaction fails after a successful upload.
*
* @param {string} uuid – Chibisafe file UUID
* @returns {Promise<void>}
*/
async function deleteFile(uuid) {
await chibiRequest(`/api/file/${uuid}`, {
@@ -152,11 +125,7 @@ async function deleteFile(uuid) {
}
/**
* Move one or more files into the "archived" album (soft-delete equivalent).
* Preserves the file on Chibisafe but keeps it out of active albums.
*
* @param {string|string[]} uuids – Chibisafe file UUID(s)
* @returns {Promise<void>}
* Move one or more files into the "archived" album.
*/
async function archiveFiles(uuids) {
if (!ALBUMS.archived) {
@@ -172,20 +141,15 @@ async function archiveFiles(uuids) {
...baseHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
files: ids,
albumUuid: ALBUMS.archived,
}),
},
});
}
/**
* Move one or more files into a specific album by UUID.
* Used internally; you can also call it directly for custom album operations.
*
* @param {string|string[]} uuids
* @param {string} albumUuid
* @returns {Promise<void>}
*/
async function addFilesToAlbum(uuids, albumUuid) {
const ids = Array.isArray(uuids) ? uuids : [uuids];
@@ -196,10 +160,10 @@ async function addFilesToAlbum(uuids, albumUuid) {
...baseHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
files: ids,
albumUuid,
}),
},
});
}