Payload CMS - Cloudinary Support #8881
Replies: 1 comment
-
Hey! I actually recently created an adapter for Due to the fact that plugin-cloud-storage doesn't provide mimetype information by default, the verification of whether a file is a video currently relies on checking the file extension.
More info: https://cloudinary.com/documentation/node_integration File StructureHere’s how I organized the files for this adapter:
generateURL.tsimport type { GenerateURL } from "@payloadcms/plugin-cloud-storage/types";
import { v2 as cloudinary } from "cloudinary";
import { videoExtensions } from ".";
import path from "path";
interface GenerateURLArgs {
folderSrc: string;
getStorageClient: () => typeof cloudinary;
}
export const getGenerateURL = ({ folderSrc, getStorageClient }: GenerateURLArgs): GenerateURL => {
return async ({ filename, prefix = "" }) => {
const publicId = path.posix.join(folderSrc, prefix, filename);
const extension = filename.toLowerCase().split(".").pop() as string;
const isVideo = videoExtensions.includes(extension);
const resource = await getStorageClient().api.resource(publicId, {
resource_type: isVideo ? "video" : "image"
});
return resource.secure_url;
};
}; handleDelete.tsimport type { HandleDelete } from "@payloadcms/plugin-cloud-storage/types";
import { v2 as cloudinary } from "cloudinary";
import { videoExtensions } from ".";
import path from "path";
interface HandleDeleteArgs {
folderSrc: string;
getStorageClient: () => typeof cloudinary;
}
export const getHandleDelete = ({ folderSrc, getStorageClient }: HandleDeleteArgs): HandleDelete => {
return async ({ doc: { prefix = "" }, filename }) => {
const publicId = path.posix.join(folderSrc, prefix, filename);
const extension = filename.toLowerCase().split(".").pop() as string;
const isVideo = videoExtensions.includes(extension);
await getStorageClient().uploader.destroy(publicId, { resource_type: isVideo ? "video" : "image" });
};
}; handleUpload.tsimport type { HandleUpload } from "@payloadcms/plugin-cloud-storage/types";
import type { CollectionConfig } from "payload";
import { UploadApiOptions, v2 as cloudinary } from "cloudinary";
import type stream from "stream";
import path from "path";
import fs from "fs";
interface HandleUploadArgs {
folderSrc: string;
collection: CollectionConfig;
getStorageClient: () => typeof cloudinary;
prefix?: string;
}
const multipartThreshold = 1024 * 1024 * 99; // 99MB
export const getHandleUpload = ({ folderSrc, getStorageClient, prefix = "" }: HandleUploadArgs): HandleUpload => {
return async ({ data, file }) => {
const fileKey = path.posix.join(data.prefix || prefix, file.filename);
const config: UploadApiOptions = {
resource_type: "auto",
public_id: fileKey,
folder: folderSrc
};
const fileBufferOrStream: Buffer | stream.Readable = file.tempFilePath
? fs.createReadStream(file.tempFilePath)
: file.buffer;
if (file.buffer.length > 0 && file.buffer.length < multipartThreshold) {
await new Promise((resolve, reject) => {
getStorageClient()
.uploader.upload_stream(config, (error, result) => {
if (error) {
reject(error);
}
resolve(result);
})
.end(fileBufferOrStream);
});
return data;
}
await new Promise((resolve, reject) => {
getStorageClient()
.uploader.upload_chunked_stream(config, (error, result) => {
if (error) {
reject(error);
}
resolve(result);
})
.end(fileBufferOrStream);
});
return data;
};
}; index.tsimport type { Adapter, GeneratedAdapter } from "@payloadcms/plugin-cloud-storage/types";
import { ConfigOptions, v2 as cloudinary } from "cloudinary";
import { getGenerateURL } from "./generateURL";
import { getHandleDelete } from "./handleDelete";
import { getHandleUpload } from "./handleUpload";
import { getHandler } from "./staticHandler";
export interface Args {
folder?: string;
config: ConfigOptions;
}
// Video extensions
export const videoExtensions = ["mp2", "mp3", "mp4", "mov", "avi", "mkv", "flv", "wmv", "webm", "mpg", "mpe", "mpeg"];
export const cloudinaryAdapter =
({ folder, config = {} }: Args): Adapter =>
({ collection, prefix }): GeneratedAdapter => {
if (!cloudinary) {
throw new Error(
"The package cloudinary is not installed, but is required for the plugin-cloud-storage Cloudinary adapter. Please install it."
);
}
let storageClient: null | typeof cloudinary = null;
const folderSrc = folder ? folder.replace(/^\/|\/$/g, "") + "/" : "";
const getStorageClient = (): typeof cloudinary => {
if (storageClient) return storageClient;
cloudinary.config(config);
storageClient = cloudinary;
return storageClient;
};
return {
name: "cloudinary",
generateURL: getGenerateURL({ folderSrc, getStorageClient }),
handleDelete: getHandleDelete({ folderSrc, getStorageClient }),
handleUpload: getHandleUpload({
folderSrc,
collection,
getStorageClient,
prefix
}),
staticHandler: getHandler({ folderSrc, collection, getStorageClient })
};
}; staticHandler.tsimport type { StaticHandler } from "@payloadcms/plugin-cloud-storage/types";
import { getFilePrefix } from "@payloadcms/plugin-cloud-storage/utilities";
import type { CollectionConfig } from "payload";
import { v2 as cloudinary } from "cloudinary";
import { videoExtensions } from ".";
import path from "path";
interface StaticHandlerArgs {
folderSrc: string;
collection: CollectionConfig;
getStorageClient: () => typeof cloudinary;
}
export const getHandler = ({ folderSrc, collection, getStorageClient }: StaticHandlerArgs): StaticHandler => {
return async (req, { params: { filename } }) => {
try {
const prefix = await getFilePrefix({ collection, filename, req });
const publicId = path.posix.join(folderSrc, prefix, filename);
const extension = filename.toLowerCase().split(".").pop() as string;
const isVideo = videoExtensions.includes(extension);
const resource = await getStorageClient().api.resource(publicId, {
resource_type: isVideo ? "video" : "image"
});
const response = await fetch(resource.secure_url);
if (!response.ok) {
req.payload.logger.error(`Failed to fetch Cloudinary resource for ${filename}`);
return new Response("Not Found", { status: 404 });
}
const headers = new Headers({
"Content-Type": response.headers.get("content-type") || "application/octet-stream",
"Content-Length": response.headers.get("content-length") || "0"
});
return new Response(response.body, {
headers,
status: 200
});
} catch (err) {
req.payload.logger.error(err);
return new Response("Internal Server Error", { status: 500 });
}
};
}; payload.config.tsimport { cloudStoragePlugin } from "@payloadcms/plugin-cloud-storage";
import { cloudinaryAdapter } from "../CloudinaryAdapter";
// other import ...
const storageAdapter = cloudinaryAdapter({
folder: "folder-name", // if u want upload file to folder in Cloudinary
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
}
});
export default buildConfig({
...
collections: [Media],
plugins: [
cloudStoragePlugin({
enabled: process.env.CLOUDIONARY_ENABLED === "true",
collections: {
media: {
adapter: storageAdapter
}
}
})
]
}); Edit: Improve logic |
Beta Was this translation helpful? Give feedback.
-
Do we have adapters to extend payload assets management like Images, Audiom, Video, PDF, PPT to be hosted on cloudinary (may be tenant wise support) so that we can have better control wrt cloudinary features and capabilities includes in overall deliverables.
Beta Was this translation helpful? Give feedback.
All reactions