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