Spaces:
Running
Running
File size: 3,489 Bytes
7154447 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | const fs = require("fs");
const cloudinary = require("../config/cloudinary");
const FOLDER = "aynwerd-courses";
// Cloudinary's chunked `upload_large` is meant for genuinely large
// files (their own docs point to it for anything that risks a single
// request timing out or exceeding size limits — 100MB+ in practice).
// Using it unconditionally for every video was a bug: for small files
// its resolved result can come back missing fields (public_id,
// resource_type) that the plain, non-chunked upload always includes,
// which surfaced as a confusing Mongoose "required" validation error
// rather than a clear upload error. Only use it above this threshold.
const LARGE_UPLOAD_THRESHOLD_BYTES = 100 * 1024 * 1024; // 100MB
/**
* Uploads a file from a local temp path to Cloudinary under private,
* signed-only access. `type: "authenticated"` means the asset can
* NEVER be fetched by its plain URL or by guessing the public ID —
* every request must carry a valid signature that we only ever mint
* for a verified, paying learner (see getSignedPlaybackUrl below).
*/
async function uploadFromPath(filePath, { resourceType, filenameHint }) {
const options = {
folder: FOLDER,
resource_type: resourceType, // "video" | "image" | "raw"
type: "authenticated",
use_filename: true,
unique_filename: true,
filename_override: filenameHint,
};
const { size } = fs.statSync(filePath);
const useLargeUpload = resourceType === "video" && size > LARGE_UPLOAD_THRESHOLD_BYTES;
const result = useLargeUpload
? await cloudinary.uploader.upload_large(filePath, { ...options, chunk_size: 20 * 1024 * 1024 })
: await cloudinary.uploader.upload(filePath, options);
// Fail loudly and specifically here rather than letting an
// incomplete result silently reach the database, where it would
// otherwise surface as an opaque Mongoose validation error instead
// of a clear "the upload didn't actually succeed" message.
if (!result || !result.public_id || !result.resource_type) {
throw new Error(
`Cloudinary upload did not return the expected result (got: ${JSON.stringify(result)}). ` +
"The file was not saved — please try again."
);
}
return result;
}
function deleteAsset(publicId, resourceType) {
return cloudinary.uploader.destroy(publicId, { resource_type: resourceType, type: "authenticated" });
}
/**
* Mints a short-lived, signed URL for a specific lesson asset. Valid for
* a couple of hours only, so a leaked link stops working quickly and
* cannot be redistributed as a permanent download.
*/
function getSignedPlaybackUrl(publicId, resourceType, { format, expiresInSeconds = 2 * 60 * 60 } = {}) {
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
const url = cloudinary.url(publicId, {
resource_type: resourceType,
type: "authenticated",
sign_url: true,
secure: true,
expires_at: expiresAt,
format: format || undefined,
});
return { url, expiresAt };
}
/** Maps an uploaded file's mimetype to the Cloudinary resource_type bucket it belongs in. */
function resourceTypeForMimetype(mimetype) {
if (mimetype.startsWith("video/")) return "video";
if (mimetype.startsWith("image/")) return "image";
return "raw"; // PDFs, docs, slides, zips, etc.
}
module.exports = { uploadFromPath, deleteAsset, getSignedPlaybackUrl, resourceTypeForMimetype };
|