feat: add route for static file GET requests (#5065)
This commit is contained in:
BIN
packages/dev/media/dep-comparison.jpg
Normal file
BIN
packages/dev/media/dep-comparison.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
@@ -0,0 +1,4 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
|
||||
export { GET } from '@payloadcms/next/routes/[collection]/file/[filename]/route'
|
||||
@@ -1,4 +1,4 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
|
||||
export { GET } from '@payloadcms/next/routes/graphql/playground'
|
||||
export { GET_GraphQLPlayground as GET } from '@payloadcms/next/routes/graphql'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
|
||||
export { POST } from '@payloadcms/next/routes/graphql/handler'
|
||||
export { POST_GraphQLHandler as POST } from '@payloadcms/next/routes/graphql'
|
||||
|
||||
@@ -3,7 +3,12 @@ import { BeforeInput } from './Pages/BeforeInput'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: true,
|
||||
upload: {
|
||||
staticDir: 'media',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
|
||||
54
packages/next/bin/install.ts
Normal file
54
packages/next/bin/install.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { copyRecursiveSync } from './utilities/copyRecursiveSync'
|
||||
import { copyFile } from './utilities/copyFile'
|
||||
|
||||
const install = () => {
|
||||
const useSrc = fs.existsSync(path.resolve(process.cwd(), './src'))
|
||||
const hasAppFolder = fs.existsSync(path.resolve(process.cwd(), `./${useSrc ? 'src/' : 'app'}`))
|
||||
|
||||
if (!hasAppFolder) {
|
||||
console.error(
|
||||
`You need to have a ${
|
||||
useSrc ? 'src/' : 'app/'
|
||||
} folder in your project before running this command.`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const basePath = useSrc ? './src' : '.'
|
||||
|
||||
// Copy handlers into /api
|
||||
copyRecursiveSync(
|
||||
path.resolve(__dirname, './templates/pages/api'),
|
||||
path.resolve(process.cwd(), `${basePath}/pages/api`),
|
||||
)
|
||||
|
||||
// Copy admin into /app
|
||||
copyRecursiveSync(
|
||||
path.resolve(__dirname, './templates/app'),
|
||||
path.resolve(process.cwd(), `${basePath}/app`),
|
||||
)
|
||||
|
||||
const payloadConfigPath = path.resolve(process.cwd(), `${basePath}/payload`)
|
||||
|
||||
if (!fs.existsSync(payloadConfigPath)) {
|
||||
fs.mkdirSync(payloadConfigPath)
|
||||
}
|
||||
|
||||
// Copy payload initialization
|
||||
copyFile(
|
||||
path.resolve(__dirname, './templates/payloadClient.ts'),
|
||||
path.resolve(process.cwd(), `${basePath}/payload/payloadClient.ts`),
|
||||
)
|
||||
|
||||
// Copy base payload config
|
||||
copyFile(
|
||||
path.resolve(__dirname, './templates/payload.config.ts'),
|
||||
path.resolve(process.cwd(), `${basePath}/payload/payload.config.ts`),
|
||||
)
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
export default install()
|
||||
7
packages/next/bin/utilities/copyFile.ts
Normal file
7
packages/next/bin/utilities/copyFile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import fs from 'fs'
|
||||
|
||||
export const copyFile = (source, target) => {
|
||||
if (!fs.existsSync(target)) {
|
||||
fs.writeFileSync(target, fs.readFileSync(source))
|
||||
}
|
||||
}
|
||||
16
packages/next/bin/utilities/copyRecursiveSync.ts
Normal file
16
packages/next/bin/utilities/copyRecursiveSync.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function copyRecursiveSync(src, dest) {
|
||||
var exists = fs.existsSync(src)
|
||||
var stats = exists && fs.statSync(src)
|
||||
var isDirectory = exists && stats && stats.isDirectory()
|
||||
if (isDirectory) {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
fs.readdirSync(src).forEach(function (childItemName) {
|
||||
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
|
||||
})
|
||||
} else {
|
||||
fs.copyFileSync(src, dest)
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,9 @@
|
||||
"import": "./src/routes/index.ts",
|
||||
"require": "./src/routes/index.ts"
|
||||
},
|
||||
"./graphql": {
|
||||
"import": "./src/routes/graphql/handler.ts",
|
||||
"require": "./src/routes/graphql/handler.ts"
|
||||
},
|
||||
"./graphql-playground": {
|
||||
"import": "./src/routes/graphql/playground.ts",
|
||||
"require": "./src/routes/graphql/playground.ts"
|
||||
"./routes/*": {
|
||||
"import": "./src/routes/*.ts",
|
||||
"require": "./src/routes/*.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -96,15 +92,10 @@
|
||||
"require": "./dist/routes/index.js",
|
||||
"types": "./dist/routes/index.d.ts"
|
||||
},
|
||||
"./graphql": {
|
||||
"import": "./dist/routes/graphql/handler.js",
|
||||
"require": "./dist/routes/graphql/handler.js",
|
||||
"types": "./dist/routes/graphql/handler.ts"
|
||||
},
|
||||
"./graphql-playground": {
|
||||
"import": "./dist/routes/graphql/playground.js",
|
||||
"require": "./dist/routes/graphql/playground.js",
|
||||
"types": "./dist/routes/graphql/playground.ts"
|
||||
"./routes/*": {
|
||||
"import": "./dist/routes/*.js",
|
||||
"require": "./dist/routes/*.js",
|
||||
"types": "./dist/routes/*.d.ts"
|
||||
}
|
||||
},
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
|
||||
export { GET } from '@payloadcms/next/routes/[collection]/file/[filename]/route'
|
||||
@@ -1,4 +1,4 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
|
||||
export { GET } from '@payloadcms/next/routes/graphql/playground'
|
||||
export { GET_GraphQLPlayground as GET } from '@payloadcms/next/routes/graphql'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
|
||||
export { POST } from '@payloadcms/next/routes/graphql/handler'
|
||||
export { POST_GraphQLHandler as POST } from '@payloadcms/next/routes/graphql'
|
||||
|
||||
26
packages/next/src/next-stream-file/index.ts
Normal file
26
packages/next/src/next-stream-file/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import fs from 'fs'
|
||||
|
||||
function iteratorToStream(iterator) {
|
||||
return new ReadableStream({
|
||||
async pull(controller) {
|
||||
const { value, done } = await iterator.next()
|
||||
if (done) {
|
||||
controller.close()
|
||||
} else {
|
||||
controller.enqueue(value)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function* nodeStreamToIterator(stream: fs.ReadStream) {
|
||||
for await (const chunk of stream) {
|
||||
yield new Uint8Array(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
export function streamFile(path: string): ReadableStream {
|
||||
const nodeStream = fs.createReadStream(path)
|
||||
const data: ReadableStream = iteratorToStream(nodeStreamToIterator(nodeStream))
|
||||
return data
|
||||
}
|
||||
119
packages/next/src/routes/[collection]/file/[filename]/route.ts
Normal file
119
packages/next/src/routes/[collection]/file/[filename]/route.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import path from 'path'
|
||||
import config from 'payload-config'
|
||||
import { streamFile } from '../../../../next-stream-file'
|
||||
import fsPromises from 'fs/promises'
|
||||
import { Collection, PayloadRequest, Where } from 'payload/types'
|
||||
import executeAccess from 'payload/dist/auth/executeAccess'
|
||||
import { APIError, Forbidden } from 'payload/errors'
|
||||
import { RouteError } from '../../../RouteError'
|
||||
import { createPayloadRequest } from '../../../../utilities/createPayloadRequest'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
async function checkFileAccess({
|
||||
req,
|
||||
filename,
|
||||
collection,
|
||||
}: {
|
||||
req: PayloadRequest
|
||||
filename: string
|
||||
collection: Collection
|
||||
}) {
|
||||
const { config } = collection
|
||||
const accessResult = await executeAccess({ isReadingStaticFile: true, req }, config.access.read)
|
||||
|
||||
if (typeof accessResult === 'object') {
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
filename: {
|
||||
equals: filename,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
accessResult,
|
||||
],
|
||||
}
|
||||
|
||||
if (config.upload.imageSizes) {
|
||||
config.upload.imageSizes.forEach(({ name }) => {
|
||||
queryToBuild.and[0].or.push({
|
||||
[`sizes.${name}.filename`]: {
|
||||
equals: filename,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const doc = await req.payload.db.findOne({
|
||||
collection: config.slug,
|
||||
req,
|
||||
where: queryToBuild,
|
||||
})
|
||||
|
||||
if (!doc) {
|
||||
throw new Forbidden(req.t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = async (
|
||||
request: Request,
|
||||
{ params }: { params: { collection: string; filename: string } },
|
||||
) => {
|
||||
const { collection: collectionSlug, filename } = params
|
||||
let req: PayloadRequest
|
||||
let collection: Collection
|
||||
|
||||
try {
|
||||
req = await createPayloadRequest({
|
||||
request,
|
||||
config,
|
||||
params: { collection: collectionSlug },
|
||||
})
|
||||
collection = req.payload.collections?.[collectionSlug]
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`Media collection not found: ${collectionSlug}`, httpStatus.BAD_REQUEST)
|
||||
}
|
||||
|
||||
if (!collection.config.upload) {
|
||||
throw new APIError(
|
||||
`This collection is not an upload collection: ${collectionSlug}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
if (collection.config.upload.disableLocalStorage) {
|
||||
throw new APIError(
|
||||
`This collection has local storage disabled: ${collectionSlug}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
await checkFileAccess({
|
||||
req,
|
||||
filename,
|
||||
collection,
|
||||
})
|
||||
|
||||
const fileDir = collection.config.upload?.staticDir || collection.config.slug
|
||||
const filePath = path.resolve(`${fileDir}/${filename}`)
|
||||
const stats = await fsPromises.stat(filePath)
|
||||
const data = streamFile(filePath)
|
||||
return new Response(data, {
|
||||
status: httpStatus.OK,
|
||||
headers: new Headers({
|
||||
'content-length': stats.size + '',
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
req,
|
||||
collection,
|
||||
err: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
125
packages/next/src/routes/fileHandler/[filename]/route.ts
Normal file
125
packages/next/src/routes/fileHandler/[filename]/route.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import path from 'path'
|
||||
import config from 'payload-config'
|
||||
import { streamFile } from '../../../next-stream-file'
|
||||
import fsPromises from 'fs/promises'
|
||||
import { Collection, PayloadRequest, Where } from 'payload/types'
|
||||
import executeAccess from 'payload/dist/auth/executeAccess'
|
||||
import { APIError, Forbidden } from 'payload/errors'
|
||||
import { RouteError } from '../../RouteError'
|
||||
import { createPayloadRequest } from '../../../utilities/createPayloadRequest'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
async function checkFileAccess({
|
||||
req,
|
||||
filename,
|
||||
collection,
|
||||
}: {
|
||||
req: PayloadRequest
|
||||
filename: string
|
||||
collection: Collection
|
||||
}) {
|
||||
const { config } = collection
|
||||
const accessResult = await executeAccess({ isReadingStaticFile: true, req }, config.access.read)
|
||||
|
||||
if (typeof accessResult === 'object') {
|
||||
const queryToBuild: Where = {
|
||||
and: [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
filename: {
|
||||
equals: filename,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
accessResult,
|
||||
],
|
||||
}
|
||||
|
||||
if (config.upload.imageSizes) {
|
||||
config.upload.imageSizes.forEach(({ name }) => {
|
||||
queryToBuild.and[0].or.push({
|
||||
[`sizes.${name}.filename`]: {
|
||||
equals: filename,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const doc = await req.payload.db.findOne({
|
||||
collection: config.slug,
|
||||
req,
|
||||
where: queryToBuild,
|
||||
})
|
||||
|
||||
if (!doc) {
|
||||
throw new Forbidden(req.t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GET =
|
||||
(collectionSlug: string) =>
|
||||
async (request: Request, { params }: { params: { filename: string } }) => {
|
||||
let req: PayloadRequest
|
||||
let collection: Collection
|
||||
|
||||
try {
|
||||
req = await createPayloadRequest({
|
||||
request,
|
||||
config,
|
||||
params: { collection: collectionSlug },
|
||||
})
|
||||
collection = req.payload.collections?.[collectionSlug]
|
||||
|
||||
if (!collection) {
|
||||
throw new APIError(`Media collection not found: ${collectionSlug}`, httpStatus.BAD_REQUEST)
|
||||
}
|
||||
|
||||
if (!collection.config.upload) {
|
||||
throw new APIError(
|
||||
`This collection is not an upload collection: ${collectionSlug}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
if (collection.config.upload.disableLocalStorage) {
|
||||
throw new APIError(
|
||||
`This collection has local storage disabled: ${collectionSlug}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
const { filename } = params
|
||||
if (!filename) {
|
||||
throw new APIError(
|
||||
'No filename provided, ensure this route is within a [filename] folder, i.e. staticDir/[filename]/route.ts',
|
||||
httpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
await checkFileAccess({
|
||||
req,
|
||||
filename,
|
||||
collection,
|
||||
})
|
||||
|
||||
const fileDir = collection.config.upload?.staticDir || collection.config.slug
|
||||
const filePath = path.resolve(`${fileDir}/${filename}`)
|
||||
const stats = await fsPromises.stat(filePath)
|
||||
const data = streamFile(filePath)
|
||||
return new Response(data, {
|
||||
status: httpStatus.OK,
|
||||
headers: new Headers({
|
||||
'content-length': stats.size + '',
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
return RouteError({
|
||||
req,
|
||||
collection,
|
||||
err: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
2
packages/next/src/routes/graphql/index.ts
Normal file
2
packages/next/src/routes/graphql/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { POST as POST_GraphQLHandler } from './handler'
|
||||
export { GET as GET_GraphQLPlayground } from './playground'
|
||||
@@ -98,7 +98,11 @@ export const createPayloadRequest = async ({
|
||||
transactionID: undefined,
|
||||
payloadDataLoader: undefined,
|
||||
payloadUploadSizes: {},
|
||||
...urlPropertiesObject,
|
||||
host: urlProperties.host,
|
||||
protocol: urlProperties.protocol,
|
||||
pathname: urlProperties.pathname,
|
||||
searchParams: urlProperties.searchParams,
|
||||
origin: urlProperties.origin,
|
||||
}
|
||||
|
||||
const req: PayloadRequest = Object.assign(request, customRequest)
|
||||
|
||||
@@ -7,16 +7,18 @@ type OperationArgs = {
|
||||
data?: any
|
||||
disableErrors?: boolean
|
||||
id?: number | string
|
||||
isReadingStaticFile?: boolean
|
||||
req: PayloadRequest
|
||||
}
|
||||
const executeAccess = async (
|
||||
{ id, data, disableErrors, req }: OperationArgs,
|
||||
{ id, data, disableErrors, isReadingStaticFile = false, req }: OperationArgs,
|
||||
access: Access,
|
||||
): Promise<AccessResult> => {
|
||||
if (access) {
|
||||
const result = await access({
|
||||
id,
|
||||
data,
|
||||
isReadingStaticFile,
|
||||
req,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,6 +208,8 @@ export type AccessArgs<T = any, U = any> = {
|
||||
data?: T
|
||||
/** ID of the resource being accessed */
|
||||
id?: number | string
|
||||
/** If true, the request is for a static file */
|
||||
isReadingStaticFile?: boolean
|
||||
/** The original request that requires an access check */
|
||||
req: PayloadRequest<U>
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export type CustomPayloadRequest<U = any> = {
|
||||
transactionIDPromise?: Promise<void>
|
||||
/** The signed in user */
|
||||
user: (U & User) | null
|
||||
} & Partial<URL>
|
||||
} & Pick<URL, 'host' | 'origin' | 'pathname' | 'protocol' | 'searchParams'>
|
||||
export type PayloadRequest<U = any> = Partial<Request> &
|
||||
Required<Pick<Request, 'headers'>> &
|
||||
CustomPayloadRequest<U>
|
||||
|
||||
Reference in New Issue
Block a user