mirror of
https://github.com/hanzo-apps/cms.git
synced 2026-10-30 14:47:33 +00:00
Complete de-brand of the fork the org owns (unpublished, empty DB — so no external consumers and no data migration). 1308 files. Applied Payload→CMS / payload→cms / PAYLOAD→CMS to: type/identifier names (PayloadRequest→CMSRequest, getPayload→getCMS, BasePayload→BaseCMS, the request property req.payload→req.cms, PayloadComponent→ CMSComponent, usePayloadAPI→useCMSAPI, …); collection/DB slugs (payload-jobs→cms-jobs, payload-kv, payload-migrations, payload-preferences, payload-locked-documents); env vars (PAYLOAD_*→CMS_*); i18n key + values (payloadSettings→cmsSettings, 'Payload Settings'→'CMS Settings' across 44 languages); CSS @layer/classes; comments/docs/UI. PRESERVED (deliberate — renaming breaks resolution or law): - MIT LICENSE + 'Copyright (c) 2018-2025 Payload CMS, Inc.' (legal attribution) - every import/require/export specifier + path literal; on-disk filenames (payload.config.ts, payload-types.ts) + the packages/payload dir; @payload-config alias - package.json "name" fields (already @hanzo/cms*) - URLs (payloadcms.com); Redux action.payload (a different, unrelated field) CONSISTENCY VERIFIED repo-wide: req.payload=0, from 'payload'=0, payloadSettings=0 (all now cms*), Redux action.payload intact, license intact. Individual files transpile clean. NOT YET VERIFIED: full type-compilation — requires a 44-package monorepo build (node_modules absent). Green build is the gate before publish; not claimed here. Co-authored-by: Hanzo Dev <[email protected]>
154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
import type { CollectionConfig, CMSRequest } from '@hanzo/cms'
|
|
|
|
import {
|
|
getFilePrefix as getDocPrefix,
|
|
getFileKey,
|
|
} from '@hanzo/cms-plugin-cloud-storage/utilities'
|
|
import { getRangeRequestInfo } from '@hanzo/cms/internal'
|
|
|
|
import type { R2Bucket } from './types.js'
|
|
|
|
interface GetFileArgs {
|
|
bucket: R2Bucket
|
|
clientUploadContext?: unknown
|
|
collection: CollectionConfig
|
|
filename: string
|
|
incomingHeaders?: Headers
|
|
prefix: string
|
|
prefixQueryParam?: string
|
|
req: CMSRequest
|
|
useCompositePrefixes?: boolean
|
|
}
|
|
|
|
const isMiniflare = process.env.NODE_ENV === 'development'
|
|
|
|
export async function getFile({
|
|
bucket,
|
|
clientUploadContext,
|
|
collection,
|
|
filename,
|
|
incomingHeaders,
|
|
prefix = '',
|
|
prefixQueryParam,
|
|
req,
|
|
useCompositePrefixes = false,
|
|
}: GetFileArgs): Promise<Response> {
|
|
try {
|
|
const docPrefix = await getDocPrefix({
|
|
clientUploadContext,
|
|
collection,
|
|
filename,
|
|
prefixQueryParam,
|
|
req,
|
|
})
|
|
|
|
const { fileKey } = getFileKey({
|
|
collectionPrefix: prefix,
|
|
docPrefix,
|
|
filename,
|
|
useCompositePrefixes,
|
|
})
|
|
|
|
// Get file size for range validation
|
|
const headObj = await bucket?.head(fileKey)
|
|
if (!headObj) {
|
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
|
}
|
|
|
|
const fileSize = headObj.size
|
|
|
|
// Don't return large file uploads back to the client, or the Worker will run out of memory
|
|
if (fileSize > 50 * 1024 * 1024 && clientUploadContext) {
|
|
return new Response(null, { status: 200 })
|
|
}
|
|
|
|
// Handle range request
|
|
const rangeHeader = req.headers.get('range')
|
|
const rangeResult = getRangeRequestInfo({ fileSize, rangeHeader })
|
|
|
|
if (rangeResult.type === 'invalid') {
|
|
return new Response(null, {
|
|
headers: new Headers(rangeResult.headers),
|
|
status: rangeResult.status,
|
|
})
|
|
}
|
|
|
|
// Get object with range if needed
|
|
// Due to https://github.com/cloudflare/workers-sdk/issues/6047
|
|
// We cannot send a Headers instance to Miniflare
|
|
const obj =
|
|
rangeResult.type === 'partial' && !isMiniflare
|
|
? await bucket?.get(fileKey, {
|
|
range: {
|
|
length: rangeResult.rangeEnd - rangeResult.rangeStart + 1,
|
|
offset: rangeResult.rangeStart,
|
|
},
|
|
})
|
|
: await bucket?.get(fileKey)
|
|
|
|
if (!obj || obj.body == undefined) {
|
|
return new Response(null, { status: 404, statusText: 'Not Found' })
|
|
}
|
|
|
|
let headers = new Headers(incomingHeaders)
|
|
|
|
// Add range-related headers from the result
|
|
for (const [headerKey, value] of Object.entries(rangeResult.headers)) {
|
|
headers.append(headerKey, value)
|
|
}
|
|
|
|
// Add R2-specific headers
|
|
if (isMiniflare) {
|
|
// In development with Miniflare, manually set headers from httpMetadata
|
|
const metadata = obj.httpMetadata
|
|
if (metadata?.cacheControl) {
|
|
headers.set('Cache-Control', metadata.cacheControl)
|
|
}
|
|
if (metadata?.contentDisposition) {
|
|
headers.set('Content-Disposition', metadata.contentDisposition)
|
|
}
|
|
if (metadata?.contentEncoding) {
|
|
headers.set('Content-Encoding', metadata.contentEncoding)
|
|
}
|
|
if (metadata?.contentLanguage) {
|
|
headers.set('Content-Language', metadata.contentLanguage)
|
|
}
|
|
if (metadata?.contentType) {
|
|
headers.set('Content-Type', metadata.contentType)
|
|
}
|
|
} else {
|
|
obj.writeHttpMetadata(headers)
|
|
}
|
|
|
|
// Add Content-Security-Policy header for SVG files to prevent executable code
|
|
const contentType = headers.get('Content-Type')
|
|
if (contentType === 'image/svg+xml') {
|
|
headers.set('Content-Security-Policy', "script-src 'none'")
|
|
}
|
|
|
|
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
|
|
|
|
if (
|
|
collection.upload &&
|
|
typeof collection.upload === 'object' &&
|
|
typeof collection.upload.modifyResponseHeaders === 'function'
|
|
) {
|
|
headers = collection.upload.modifyResponseHeaders({ headers }) || headers
|
|
}
|
|
|
|
if (etagFromHeaders && etagFromHeaders === obj.etag) {
|
|
return new Response(null, {
|
|
headers,
|
|
status: 304,
|
|
})
|
|
}
|
|
|
|
return new Response(obj.body, {
|
|
headers,
|
|
status: rangeResult.status,
|
|
})
|
|
} catch (_err: unknown) {
|
|
return new Response('Internal Server Error', { status: 500 })
|
|
}
|
|
}
|