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 };