UNPKG

payload

Version:

Node, React, Headless CMS and Application Framework built on Next.js

135 lines (134 loc) 5.06 kB
import { fileTypeFromFile } from 'file-type'; import fsPromises from 'fs/promises'; import { status as httpStatus } from 'http-status'; import path from 'path'; import { APIError } from '../../errors/APIError.js'; import { checkFileAccess } from '../../uploads/checkFileAccess.js'; import { streamFile } from '../../uploads/fetchAPI-stream-file/index.js'; import { getFileTypeFallback } from '../../uploads/getFileTypeFallback.js'; import { parseRangeHeader } from '../../uploads/parseRangeHeader.js'; import { getRequestCollection } from '../../utilities/getRequestEntity.js'; import { headersWithCors } from '../../utilities/headersWithCors.js'; export const getFileHandler = async (req)=>{ const collection = getRequestCollection(req); const filename = req.routeParams?.filename; if (!collection.config.upload) { throw new APIError(`This collection is not an upload collection: ${collection.config.slug}`, httpStatus.BAD_REQUEST); } const accessResult = await checkFileAccess({ collection, filename, req }); if (accessResult instanceof Response) { return accessResult; } if (collection.config.upload.handlers?.length) { let customResponse = null; const headers = new Headers(); for (const handler of collection.config.upload.handlers){ customResponse = await handler(req, { doc: accessResult, headers, params: { collection: collection.config.slug, filename } }); if (customResponse && customResponse instanceof Response) { break; } } if (customResponse instanceof Response) { return customResponse; } } const fileDir = collection.config.upload?.staticDir || collection.config.slug; const filePath = path.resolve(`${fileDir}/${filename}`); let stats; try { stats = await fsPromises.stat(filePath); } catch (err) { if (err.code === 'ENOENT') { req.payload.logger.error(`File ${filename} for collection ${collection.config.slug} is missing on the disk. Expected path: ${filePath}`); // Omit going to the routeError handler by returning response instead of // throwing an error to cut down log noise. The response still matches what you get with APIError to not leak details to the user. return Response.json({ errors: [ { message: 'Something went wrong.' } ] }, { headers: headersWithCors({ headers: new Headers(), req }), status: 500 }); } throw err; } const fileTypeResult = await fileTypeFromFile(filePath) || getFileTypeFallback(filePath); let mimeType = fileTypeResult.mime; if (filePath.endsWith('.svg') && fileTypeResult.mime === 'application/xml') { mimeType = 'image/svg+xml'; } // Parse Range header for byte range requests const rangeHeader = req.headers.get('range'); const rangeResult = parseRangeHeader({ fileSize: stats.size, rangeHeader }); if (rangeResult.type === 'invalid') { let headers = new Headers(); headers.set('Content-Range', `bytes */${stats.size}`); headers = collection.config.upload?.modifyResponseHeaders ? collection.config.upload.modifyResponseHeaders({ headers }) || headers : headers; return new Response(null, { headers: headersWithCors({ headers, req }), status: httpStatus.REQUESTED_RANGE_NOT_SATISFIABLE }); } let headers = new Headers(); headers.set('Content-Type', mimeType); headers.set('Accept-Ranges', 'bytes'); let data; let status; const isPartial = rangeResult.type === 'partial'; const range = rangeResult.range; if (isPartial && range) { const contentLength = range.end - range.start + 1; headers.set('Content-Length', String(contentLength)); headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`); data = streamFile({ filePath, options: { end: range.end, start: range.start } }); status = httpStatus.PARTIAL_CONTENT; } else { headers.set('Content-Length', String(stats.size)); data = streamFile({ filePath }); status = httpStatus.OK; } headers = collection.config.upload?.modifyResponseHeaders ? collection.config.upload.modifyResponseHeaders({ headers }) || headers : headers; return new Response(data, { headers: headersWithCors({ headers, req }), status }); }; //# sourceMappingURL=getFile.js.map