mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
assets: go 15GB limit
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
// controllers/admin/assets.controller.js
|
// controllers/admin/assets.controller.js
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
|
||||||
const sequelize = require("../../config/db.config");
|
const sequelize = require("../../config/db.config");
|
||||||
const Asset = require("../../models/assets/assets.mdl");
|
const Asset = require("../../models/assets/assets.mdl");
|
||||||
const chibi = require("../../services/chibisafe.service");
|
const chibi = require("../../services/chibisafe.service");
|
||||||
@@ -57,10 +56,6 @@ function resolveExtension(originalName = "") {
|
|||||||
return path.extname(originalName).replace(".", "").toLowerCase() || null;
|
return path.extname(originalName).replace(".", "").toLowerCase() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveChecksum(buffer) {
|
|
||||||
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
function streamToBuffer(stream) {
|
function streamToBuffer(stream) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -112,32 +107,33 @@ async function rollbackUploads(uploads = []) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── uploadToProvider ─────────────────────────────────────────────────────────
|
// ─── finalizeReplacementUpload ─────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Uploads a single file to the resolved provider.
|
// Used by updateAsset() when replacing an asset's file (or a video's
|
||||||
// Returns { file_url, storage_key, mime_type, extension, checksum, file_type }
|
// thumbnail): the browser already PUT the new file straight to storage via a
|
||||||
|
// presigned URL (see presignAssetUpload) — this reads back what actually
|
||||||
|
// landed there (HeadObjectCommand, no download) instead of ever buffering the
|
||||||
|
// file through this backend, the same strategy finalizeAssetFromStorage()
|
||||||
|
// uses for brand-new assets.
|
||||||
|
// Returns { file_url, storage_key, mime_type, extension, checksum, file_type, originalname }
|
||||||
//
|
//
|
||||||
async function uploadToProvider(file, ownerType, storageProvider) {
|
async function finalizeReplacementUpload(storage_key, original_name, mimetype, storageProvider) {
|
||||||
const mime_type = file.mimetype;
|
const svc = getProvider(storageProvider);
|
||||||
const extension = resolveExtension(file.originalname);
|
if (!svc || !svc.getFileMetadata) {
|
||||||
const checksum = resolveChecksum(file.buffer);
|
throw Object.assign(new Error("Presigned uploads are only supported for S3 storage."), { status: 400 });
|
||||||
const file_type = resolveFileType(mime_type);
|
}
|
||||||
|
|
||||||
const svc = getProvider(storageProvider);
|
const meta = await svc.getFileMetadata(storage_key);
|
||||||
const result = await svc.uploadFile({
|
const mime_type = meta.mimetype || mimetype || "application/octet-stream";
|
||||||
buffer: file.buffer,
|
|
||||||
originalname: file.originalname,
|
|
||||||
mimetype: mime_type,
|
|
||||||
ownerType,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
file_url: result.url,
|
file_url: await svc.buildPublicUrl(storage_key),
|
||||||
storage_key: result.uuid, // chibisafe UUID or S3 key — both stored as storage_key in DB
|
storage_key,
|
||||||
mime_type,
|
mime_type,
|
||||||
extension,
|
extension: resolveExtension(original_name || storage_key),
|
||||||
checksum,
|
checksum: meta.checksum,
|
||||||
file_type,
|
file_type: resolveFileType(mime_type),
|
||||||
|
originalname: original_name || storage_key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,13 +653,15 @@ exports.updateAsset = async (req, res) => {
|
|||||||
const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted } });
|
const asset = await Asset.findOne({ where: { asset_id: assetId, ...notDeleted } });
|
||||||
if (!asset) return R.error(res, "Asset not found.", 404);
|
if (!asset) return R.error(res, "Asset not found.", 404);
|
||||||
|
|
||||||
const file = req.files?.file?.[0] ?? req.file ?? null;
|
// Browser already PUT the replacement file straight to storage via a
|
||||||
|
// presigned URL (see presignAssetUpload) — this is plain JSON, no
|
||||||
|
// multer/file buffer involved, same pattern as POST /admin/assets.
|
||||||
|
const { storage_key, original_name, mimetype } = req.body;
|
||||||
const isVideo = asset.file_type === "video";
|
const isVideo = asset.file_type === "video";
|
||||||
const isDocument = asset.file_type === "document";
|
const isDocument = asset.file_type === "document";
|
||||||
|
|
||||||
if (isDocument && file) return R.error(res, "Document files cannot be replaced.", 400);
|
if (isDocument && storage_key) return R.error(res, "Document files cannot be replaced.", 400);
|
||||||
if (isVideo && file && !req.body.is_thumbnail) return R.error(res, "Video files cannot be replaced. Upload a new asset instead.", 400);
|
if (isVideo && storage_key && !req.body.is_thumbnail) return R.error(res, "Video files cannot be replaced. Upload a new asset instead.", 400);
|
||||||
if (file && !file.buffer) return R.error(res, "File buffer is required.", 400);
|
|
||||||
|
|
||||||
const storageProvider = asset.storage_provider;
|
const storageProvider = asset.storage_provider;
|
||||||
const usesProvider = ["chibisafe", "s3"].includes(storageProvider);
|
const usesProvider = ["chibisafe", "s3"].includes(storageProvider);
|
||||||
@@ -673,9 +671,8 @@ exports.updateAsset = async (req, res) => {
|
|||||||
|
|
||||||
let uploaded = null;
|
let uploaded = null;
|
||||||
|
|
||||||
if (file && usesProvider) {
|
if (storage_key && usesProvider) {
|
||||||
const ownerType = isVideo ? "thumbnail" : resolveFileType(file.mimetype);
|
uploaded = await finalizeReplacementUpload(storage_key, original_name, mimetype, storageProvider);
|
||||||
uploaded = await uploadToProvider(file, ownerType, storageProvider);
|
|
||||||
newUpload = { key: uploaded.storage_key, provider: storageProvider };
|
newUpload = { key: uploaded.storage_key, provider: storageProvider };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,7 +680,7 @@ exports.updateAsset = async (req, res) => {
|
|||||||
|
|
||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await applyAssetUpdate(asset, uploaded ? { ...file, ...uploaded } : null, req.body);
|
await applyAssetUpdate(asset, uploaded, req.body);
|
||||||
await asset.save({ transaction: t });
|
await asset.save({ transaction: t });
|
||||||
await t.commit();
|
await t.commit();
|
||||||
} catch (dbErr) {
|
} catch (dbErr) {
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
/***********************************************************************************************************************************************************************
|
|
||||||
* File Name: asset_upload.middleware.js
|
|
||||||
* Type of Program: Middleware
|
|
||||||
* Description: Multer config for admin asset uploads (image/video/audio/document).
|
|
||||||
* Uses memory storage — buffer is passed directly to S3.
|
|
||||||
*
|
|
||||||
* Previously this route had no `limits.fileSize` at all, so an
|
|
||||||
* oversized upload wasn't rejected cleanly — it either exhausted
|
|
||||||
* server memory buffering the whole file or blew up with a raw
|
|
||||||
* multer/Node error that never reached R.error's JSON envelope.
|
|
||||||
* The frontend's fallback then had nothing to read a `message`
|
|
||||||
* out of, so every failure surfaced as the same generic
|
|
||||||
* "Something went wrong" regardless of cause.
|
|
||||||
*
|
|
||||||
* Limits:
|
|
||||||
* fileSize: 500 MB (matches task_upload.middleware.js)
|
|
||||||
*
|
|
||||||
* Author: Kenneth Obsequio (@lash0000)
|
|
||||||
* Date Created: Jul. 13, 2026
|
|
||||||
***********************************************************************************************************************************************************************/
|
|
||||||
const multer = require('multer');
|
|
||||||
const R = require('../utils/response.util');
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage: multer.memoryStorage(),
|
|
||||||
limits: { fileSize: MAX_FILE_SIZE },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wraps any multer middleware (fields/array/single) so oversized or malformed
|
|
||||||
// uploads always come back as a clean R.error JSON response the frontend can
|
|
||||||
// read a `message` out of, instead of an unhandled exception or Express's
|
|
||||||
// default HTML error page.
|
|
||||||
function handleUpload(multerMiddleware) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
multerMiddleware(req, res, (err) => {
|
|
||||||
if (!err) return next();
|
|
||||||
|
|
||||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
|
||||||
return R.error(res, `File exceeds the ${MAX_FILE_SIZE / (1024 * 1024)} MB size limit.`, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[MULTER ERROR][ASSETS]', err);
|
|
||||||
return R.error(res, err.message ?? 'File upload failed.', 400);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { upload, handleUpload, MAX_FILE_SIZE };
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// routes/admin/assets.routes.js
|
// routes/admin/assets.routes.js
|
||||||
|
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const { upload, handleUpload } = require('../../middleware/asset_upload.middleware');
|
|
||||||
const controller = require('../../controllers/admin/assets.controller');
|
const controller = require('../../controllers/admin/assets.controller');
|
||||||
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
const { sensitiveOpsLimiter } = require('../../middleware/rateLimiter.middleware');
|
||||||
|
|
||||||
@@ -29,7 +28,9 @@ router.post('/', controller.uploadAsset);
|
|||||||
// ─── Dynamic routes last ──────────────────────────────────────────────────────
|
// ─── Dynamic routes last ──────────────────────────────────────────────────────
|
||||||
router.get('/:assetId', controller.getAsset);
|
router.get('/:assetId', controller.getAsset);
|
||||||
router.post('/:assetId/convert-to-markdown', sensitiveOpsLimiter, controller.convertAssetToMarkdown);
|
router.post('/:assetId/convert-to-markdown', sensitiveOpsLimiter, controller.convertAssetToMarkdown);
|
||||||
router.patch('/:assetId', sensitiveOpsLimiter, handleUpload(upload.fields([{ name: 'file', maxCount: 1 }])), controller.updateAsset);
|
// Replace flow also goes through presign + direct PUT (browser -> storage) —
|
||||||
|
// this body is plain JSON (storage_key etc.), same as POST /. No multer here.
|
||||||
|
router.patch('/:assetId', sensitiveOpsLimiter, controller.updateAsset);
|
||||||
router.patch('/:assetId/restore', sensitiveOpsLimiter, controller.restoreAsset);
|
router.patch('/:assetId/restore', sensitiveOpsLimiter, controller.restoreAsset);
|
||||||
router.delete('/:assetId', sensitiveOpsLimiter, controller.archiveAsset);
|
router.delete('/:assetId', sensitiveOpsLimiter, controller.archiveAsset);
|
||||||
router.delete('/:assetId/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAsset);
|
router.delete('/:assetId/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAsset);
|
||||||
|
|||||||
Reference in New Issue
Block a user