R2 storage from env + filename normalization

This commit is contained in:
2026-08-27 12:45:47 +02:00
parent ed4a78b109
commit 5630765215
13 changed files with 145 additions and 11 deletions
+57
View File
@@ -0,0 +1,57 @@
import { s3Storage } from '@payloadcms/storage-s3';
/**
* Cloudflare R2 media storage — configured from environment variables (agency
* infrastructure, not per-project panel data). R2 is S3-compatible, so we use
* @payloadcms/storage-s3 pointed at the R2 endpoint.
*
* Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at
* boot, and its credentials are agency-owned — so it lives in .env, not the
* panel. See docs/storage.md for the required variables.
*
* Returns the storage plugin when all R2 vars are present; otherwise returns a
* no-op passthrough so the project falls back to Payload's default local disk
* storage (useful in dev without R2). This mirrors how mailAdapter degrades
* gracefully when a transport isn't configured.
*
* @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])
*/ export const buildR2Storage = (collections = [
'media'
])=>{
const bucket = process.env.R2_BUCKET;
const endpoint = process.env.R2_ENDPOINT;
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
// Any missing → skip R2, fall back to local disk. Warn so it's not silent.
if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {
return (config)=>{
// Only warn when SOME vars are set (partial config = likely a mistake).
if (bucket || endpoint || accessKeyId || secretAccessKey) {
console.warn('[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' + 'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.');
}
return config;
};
}
// s3Storage wants Record<string, true> (the literal true, per collection),
// not Record<string, boolean>. Object.fromEntries widens true → boolean, so
// build the map with an explicitly-typed accumulator to keep the literal.
const collectionsConfig = {};
for (const slug of collections){
collectionsConfig[slug] = true;
}
return s3Storage({
bucket,
collections: collectionsConfig,
config: {
credentials: {
accessKeyId,
secretAccessKey
},
endpoint,
region: 'auto',
// R2 requires path-style addressing for S3 compatibility.
forcePathStyle: true
}
});
};
//# sourceMappingURL=buildR2Storage.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/buildR2Storage.ts"],"sourcesContent":["import type { Plugin } from 'payload'\n\nimport { s3Storage } from '@payloadcms/storage-s3'\n\n/**\n * Cloudflare R2 media storage — configured from environment variables (agency\n * infrastructure, not per-project panel data). R2 is S3-compatible, so we use\n * @payloadcms/storage-s3 pointed at the R2 endpoint.\n *\n * Storage is infrastructure (like the database or PAYLOAD_SECRET): it binds at\n * boot, and its credentials are agency-owned — so it lives in .env, not the\n * panel. See docs/storage.md for the required variables.\n *\n * Returns the storage plugin when all R2 vars are present; otherwise returns a\n * no-op passthrough so the project falls back to Payload's default local disk\n * storage (useful in dev without R2). This mirrors how mailAdapter degrades\n * gracefully when a transport isn't configured.\n *\n * @param collections - slugs of upload collections to offload to R2 (e.g. ['media'])\n */\nexport const buildR2Storage = (collections: string[] = ['media']): Plugin => {\n const bucket = process.env.R2_BUCKET\n const endpoint = process.env.R2_ENDPOINT\n const accessKeyId = process.env.R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY\n\n // Any missing → skip R2, fall back to local disk. Warn so it's not silent.\n if (!bucket || !endpoint || !accessKeyId || !secretAccessKey) {\n return (config) => {\n // Only warn when SOME vars are set (partial config = likely a mistake).\n if (bucket || endpoint || accessKeyId || secretAccessKey) {\n console.warn(\n '[ipal] R2 storage: incomplete env (need R2_BUCKET, R2_ENDPOINT, ' +\n 'R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY). Falling back to local disk.',\n )\n }\n return config\n }\n }\n\n // s3Storage wants Record<string, true> (the literal true, per collection),\n // not Record<string, boolean>. Object.fromEntries widens true → boolean, so\n // build the map with an explicitly-typed accumulator to keep the literal.\n const collectionsConfig: Record<string, true> = {}\n for (const slug of collections) {\n collectionsConfig[slug] = true\n }\n\n return s3Storage({\n bucket,\n collections: collectionsConfig,\n config: {\n credentials: { accessKeyId, secretAccessKey },\n endpoint,\n region: 'auto', // R2 uses 'auto'\n // R2 requires path-style addressing for S3 compatibility.\n forcePathStyle: true,\n },\n })\n}\n"],"names":["s3Storage","buildR2Storage","collections","bucket","process","env","R2_BUCKET","endpoint","R2_ENDPOINT","accessKeyId","R2_ACCESS_KEY_ID","secretAccessKey","R2_SECRET_ACCESS_KEY","config","console","warn","collectionsConfig","slug","credentials","region","forcePathStyle"],"mappings":"AAEA,SAASA,SAAS,QAAQ,yBAAwB;AAElD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,iBAAiB,CAACC,cAAwB;IAAC;CAAQ;IAC9D,MAAMC,SAASC,QAAQC,GAAG,CAACC,SAAS;IACpC,MAAMC,WAAWH,QAAQC,GAAG,CAACG,WAAW;IACxC,MAAMC,cAAcL,QAAQC,GAAG,CAACK,gBAAgB;IAChD,MAAMC,kBAAkBP,QAAQC,GAAG,CAACO,oBAAoB;IAExD,2EAA2E;IAC3E,IAAI,CAACT,UAAU,CAACI,YAAY,CAACE,eAAe,CAACE,iBAAiB;QAC5D,OAAO,CAACE;YACN,wEAAwE;YACxE,IAAIV,UAAUI,YAAYE,eAAeE,iBAAiB;gBACxDG,QAAQC,IAAI,CACV,qEACE;YAEN;YACA,OAAOF;QACT;IACF;IAEA,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAMG,oBAA0C,CAAC;IACjD,KAAK,MAAMC,QAAQf,YAAa;QAC9Bc,iBAAiB,CAACC,KAAK,GAAG;IAC5B;IAEA,OAAOjB,UAAU;QACfG;QACAD,aAAac;QACbH,QAAQ;YACNK,aAAa;gBAAET;gBAAaE;YAAgB;YAC5CJ;YACAY,QAAQ;YACR,0DAA0D;YAC1DC,gBAAgB;QAClB;IACF;AACF,EAAC"}
+3
View File
@@ -0,0 +1,3 @@
export { buildR2Storage } from './buildR2Storage.js';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/modules/storage/index.ts"],"sourcesContent":["export { buildR2Storage } from './buildR2Storage.js'\n"],"names":["buildR2Storage"],"mappings":"AAAA,SAASA,cAAc,QAAQ,sBAAqB"}