Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
138 lines
5.1 KiB
Plaintext
138 lines
5.1 KiB
Plaintext
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');
|
|
if (mimeType === 'image/svg+xml') {
|
|
headers.set('Content-Security-Policy', "script-src 'none'");
|
|
}
|
|
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 |