auth-app-api / cloudinaryUpload.js
AndreaNotes1's picture
Upload cloudinaryUpload.js
7154447 verified
Raw History Blame Contribute Delete
3.49 kB
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 };