payload-cloudinary
Version:
A Cloudinary storage plugin for Payload CMS
262 lines • 11.8 kB
JavaScript
import path from "path";
import stream from "stream";
import { getResourceType } from "./utils";
const getUploadOptions = (filename, versioning) => {
const ext = path.extname(filename).toLowerCase();
const resourceType = getResourceType(ext);
// Debug: Log PDF detection
if (ext === ".pdf") {
console.log(`[payload-cloudinary] PDF detected: ${filename}, resourceType: ${resourceType}`);
}
const baseOptions = {
resource_type: resourceType,
use_filename: true,
unique_filename: true,
// If versioning is enabled, add invalidate option
...(versioning?.autoInvalidate && { invalidate: true }),
};
switch (resourceType) {
case "video":
return {
...baseOptions,
chunk_size: 6000000,
eager: [{ format: ext.slice(1), quality: "auto" }],
eager_async: true,
};
case "image":
// For PDFs, add a pages parameter to count the pages and create a thumbnail
if (ext === ".pdf") {
const pdfOptions = {
...baseOptions,
resource_type: "image", // Force image type for PDFs
use_filename: true,
// When uploading PDFs, add a parameter to extract page count
pages: true,
// Set an eager transformation to create a thumbnail of first page
// For PDFs uploaded as images, use page parameter (not pg_)
eager: [{ format: "jpg", page: 1, quality: "auto" }],
eager_async: true,
};
console.log("[payload-cloudinary] PDF upload options:", JSON.stringify(pdfOptions, null, 2));
return pdfOptions;
}
return {
...baseOptions,
eager: [{ quality: "auto" }],
eager_async: true,
};
case "raw":
return {
...baseOptions,
resource_type: "raw",
use_filename: true,
};
default:
return baseOptions;
}
};
/**
* Sanitize a string to be used as part of a public ID
* @param str String to sanitize
* @returns Sanitized string
*/
const sanitizeForPublicID = (str) => {
return str
.toLowerCase()
.replace(/[^a-z0-9]/g, "-") // Replace any character that's not a letter or number with a hyphen
.replace(/-+/g, "-") // Replace consecutive hyphens with a single hyphen
.replace(/^-|-$/g, ""); // Remove leading or trailing hyphens
};
/**
* Generate a public ID based on the publicID options
* @param filename Original filename
* @param folderPath Folder path
* @param publicIDOptions Public ID options
* @returns Generated public ID
*/
const generatePublicID = (filename, folderPath, publicIDOptions) => {
// If a custom generator function is provided, use it
if (publicIDOptions?.generatePublicID) {
return publicIDOptions.generatePublicID(filename, path.dirname(folderPath), path.basename(folderPath));
}
// Get file extension and resource type
const ext = path.extname(filename).toLowerCase();
const resourceType = getResourceType(ext);
const isRawFile = resourceType === "raw";
// If publicID is disabled, just return the path with sanitization
if (publicIDOptions?.enabled === false) {
const filenameWithoutExt = path.basename(filename, path.extname(filename));
const sanitizedFilename = sanitizeForPublicID(filenameWithoutExt);
// For raw files, preserve the extension
const finalFilename = isRawFile
? `${sanitizedFilename}${ext}`
: sanitizedFilename;
return path.posix.join(folderPath, finalFilename);
}
// Default behavior - use filename (if enabled) and make it unique (if enabled)
const useFilename = publicIDOptions?.useFilename !== false;
const uniqueFilename = publicIDOptions?.uniqueFilename !== false;
const timestamp = uniqueFilename ? `_${Date.now()}` : "";
if (useFilename) {
// Use the filename as part of the public ID (sanitized)
const filenameWithoutExt = path.basename(filename, path.extname(filename));
const sanitizedFilename = sanitizeForPublicID(filenameWithoutExt);
// For raw files, preserve the extension
const finalFilename = isRawFile
? `${sanitizedFilename}${timestamp}${ext}`
: `${sanitizedFilename}${timestamp}`;
return path.posix.join(folderPath, finalFilename);
}
// Generate a timestamp-based ID if not using filename
// For raw files, we need to preserve the extension even with generated IDs
const finalFilename = isRawFile
? `media${timestamp}${ext}`
: `media${timestamp}`;
return path.posix.join(folderPath, finalFilename);
};
/**
* Check if a file is a PDF based on its file extension
*/
const isPDF = (filename) => {
const ext = path.extname(filename).toLowerCase();
return ext === ".pdf";
};
/**
* Get PDF page count from Cloudinary
* This is a separate function to avoid async/await linter issues
*/
const getPDFPageCount = async (cloudinary, publicId, defaultCount = 1) => {
try {
// PDFs are stored as images in Cloudinary, not raw
const pdfInfo = await cloudinary.api.resource(publicId, {
resource_type: "image",
pages: true,
});
if (pdfInfo && pdfInfo.pages) {
return pdfInfo.pages;
}
}
catch (error) {
console.error("Error getting PDF page count:", error);
}
return defaultCount;
};
export const getHandleUpload = ({ cloudinary, folder, prefix = "", versioning, publicID, }) => async ({ data, file }) => {
// Construct the folder path with proper handling of prefix
const folderPath = data.prefix
? path.posix.join(folder, data.prefix)
: path.posix.join(folder, prefix);
// Generate the public ID based on options
const publicIdValue = generatePublicID(file.filename, folderPath, publicID);
// Basic upload options
const uploadOptions = {
...getUploadOptions(file.filename, versioning),
public_id: publicIdValue,
// folder: path.dirname(publicIdValue), // Extract folder from public_id
use_filename: publicID?.useFilename !== false,
unique_filename: publicID?.uniqueFilename !== false,
asset_folder: folderPath,
};
return new Promise((resolve, reject) => {
try {
const uploadStream = cloudinary.uploader.upload_stream(uploadOptions, async (error, result) => {
if (error) {
console.error("Error uploading to Cloudinary:", error);
reject(error);
return;
}
if (result) {
const isPDFFile = isPDF(file.filename);
const baseMetadata = {
public_id: result.public_id,
resource_type: result.resource_type,
// For PDFs, explicitly set format to "pdf" if not provided
format: isPDFFile ? result.format || "pdf" : result.format,
secure_url: result.secure_url,
bytes: result.bytes,
created_at: result.created_at,
// Ensure version is always stored as string to match field type
version: result.version
? String(result.version)
: result.version,
version_id: result.version_id,
};
// Add metadata based on resource type
let typeSpecificMetadata = {};
if (result.resource_type === "video") {
typeSpecificMetadata = {
duration: result.duration,
width: result.width,
height: result.height,
eager: result.eager,
};
}
else if (isPDFFile) {
// Handle PDF specific metadata
let pageCount = 1;
// Try to get page count from result, otherwise call the API
if (result.pages) {
pageCount = result.pages;
}
else {
// Use the separate async function to get page count
pageCount = await getPDFPageCount(cloudinary, result.public_id);
}
// Ensure public_id has .pdf extension (but don't duplicate it)
const publicId = result.public_id.endsWith(".pdf")
? result.public_id
: `${result.public_id}.pdf`;
typeSpecificMetadata = {
pages: pageCount,
selected_page: 1, // Default to first page for thumbnails
width: result.width,
height: result.height,
format: result.format || "pdf", // Explicitly set format to pdf
// Generate a thumbnail URL for the PDF using Cloudinary's PDF thumbnail feature
thumbnail_url: `https://res.cloudinary.com/${cloudinary.config().cloud_name}/image/upload/pg_1,f_jpg,q_auto/${publicId}`,
};
}
else if (result.resource_type === "image") {
typeSpecificMetadata = {
width: result.width,
height: result.height,
};
}
// Combine base and type-specific metadata
// For PDFs, ensure format is explicitly set to "pdf"
const finalMetadata = {
...baseMetadata,
...typeSpecificMetadata,
};
if (isPDFFile) {
finalMetadata.format = "pdf";
}
data.cloudinary = finalMetadata;
// If versioning and history storage is enabled, store version info
if (versioning?.enabled && versioning?.storeHistory) {
data.versions = data.versions || [];
data.versions.push({
// Store version as a string to match the field type expectation
version: result.version ? String(result.version) : "",
version_id: result.version_id || "",
created_at: result.created_at || new Date().toISOString(),
secure_url: result.secure_url || "",
});
}
}
resolve(data);
});
// Create readable stream from buffer or file
const readableStream = new stream.Readable();
readableStream.push(file.buffer);
readableStream.push(null);
// Pipe the readable stream to the upload stream
readableStream.pipe(uploadStream);
}
catch (error) {
console.error("Error in upload process:", error);
reject(error);
}
});
};
//# sourceMappingURL=handleUpload.js.map