AndreaNotes1 commited on
Commit
7154447
·
verified ·
1 Parent(s): 6e13570

Upload cloudinaryUpload.js

Browse files
Files changed (1) hide show
  1. 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 };