{"version":3,"sources":["../../../src/uploads/endpoints/getFile.ts"],"sourcesContent":["import type { Stats } from 'fs'\n\nimport { fileTypeFromFile } from 'file-type'\nimport fsPromises from 'fs/promises'\nimport { status as httpStatus } from 'http-status'\nimport path from 'path'\n\nimport type { PayloadHandler } from '../../config/types.js'\n\nimport { APIError } from '../../errors/APIError.js'\nimport { checkFileAccess } from '../../uploads/checkFileAccess.js'\nimport { streamFile } from '../../uploads/fetchAPI-stream-file/index.js'\nimport { getFileTypeFallback } from '../../uploads/getFileTypeFallback.js'\nimport { parseRangeHeader } from '../../uploads/parseRangeHeader.js'\nimport { getRequestCollection } from '../../utilities/getRequestEntity.js'\nimport { headersWithCors } from '../../utilities/headersWithCors.js'\n\nexport const getFileHandler: PayloadHandler = async (req) => {\n  const collection = getRequestCollection(req)\n\n  const filename = req.routeParams?.filename as string\n  const prefix = req.searchParams?.get('prefix') ?? undefined\n\n  if (!collection.config.upload) {\n    throw new APIError(\n      `This collection is not an upload collection: ${collection.config.slug}`,\n      httpStatus.BAD_REQUEST,\n    )\n  }\n\n  const accessResult = (await checkFileAccess({\n    collection,\n    filename,\n    prefix,\n    req,\n  }))!\n\n  if (accessResult instanceof Response) {\n    return accessResult\n  }\n\n  if (collection.config.upload.handlers?.length) {\n    let customResponse: null | Response | void = null\n    const headers = new Headers()\n\n    for (const handler of collection.config.upload.handlers) {\n      customResponse = await handler(req, {\n        doc: accessResult,\n        headers,\n        params: {\n          collection: collection.config.slug,\n          filename,\n          prefix,\n        },\n      })\n      if (customResponse && customResponse instanceof Response) {\n        break\n      }\n    }\n\n    if (customResponse instanceof Response) {\n      return customResponse\n    }\n  }\n\n  // Local filesystem fallback — cloud storage handlers return a Response above\n  // and have their own filename validation via sanitizeFilename.\n  const fileDir = collection.config.upload?.staticDir || collection.config.slug\n  const resolvedDir = path.resolve(fileDir)\n  const filePath = path.resolve(resolvedDir, filename)\n\n  if (!filePath.startsWith(resolvedDir + path.sep)) {\n    throw new APIError('Invalid filename.', httpStatus.BAD_REQUEST)\n  }\n\n  let stats: Stats\n\n  try {\n    stats = await fsPromises.stat(filePath)\n  } catch (err) {\n    if ((err as { code?: string }).code === 'ENOENT') {\n      req.payload.logger.error(\n        `File ${filename} for collection ${collection.config.slug} is missing on the disk. Expected path: ${filePath}`,\n      )\n\n      // Omit going to the routeError handler by returning response instead of\n      // throwing an error to cut down log noise. The response still matches what you get with APIError to not leak details to the user.\n      return Response.json(\n        {\n          errors: [\n            {\n              message: 'Something went wrong.',\n            },\n          ],\n        },\n        {\n          headers: headersWithCors({\n            headers: new Headers(),\n            req,\n          }),\n          status: 500,\n        },\n      )\n    }\n\n    throw err\n  }\n\n  const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath)\n  let mimeType = fileTypeResult.mime\n\n  if (filePath.endsWith('.svg') && fileTypeResult.mime === 'application/xml') {\n    mimeType = 'image/svg+xml'\n  }\n\n  // Parse Range header for byte range requests\n  const rangeHeader = req.headers.get('range')\n  const rangeResult = parseRangeHeader({\n    fileSize: stats.size,\n    rangeHeader,\n  })\n\n  if (rangeResult.type === 'invalid') {\n    let headers = new Headers()\n    headers.set('Content-Range', `bytes */${stats.size}`)\n    headers = collection.config.upload?.modifyResponseHeaders\n      ? collection.config.upload.modifyResponseHeaders({ headers }) || headers\n      : headers\n\n    return new Response(null, {\n      headers: headersWithCors({\n        headers,\n        req,\n      }),\n      status: httpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n    })\n  }\n\n  let headers = new Headers()\n  headers.set('Content-Type', mimeType)\n  headers.set('Accept-Ranges', 'bytes')\n\n  if (mimeType === 'image/svg+xml') {\n    headers.set('Content-Security-Policy', \"script-src 'none'\")\n  }\n\n  let data: ReadableStream\n  let status: number\n  const isPartial = rangeResult.type === 'partial'\n  const range = rangeResult.range\n\n  if (isPartial && range) {\n    const contentLength = range.end - range.start + 1\n    headers.set('Content-Length', String(contentLength))\n    headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`)\n    data = streamFile({ filePath, options: { end: range.end, start: range.start } })\n    status = httpStatus.PARTIAL_CONTENT\n  } else {\n    headers.set('Content-Length', String(stats.size))\n    data = streamFile({ filePath })\n    status = httpStatus.OK\n  }\n\n  headers = collection.config.upload?.modifyResponseHeaders\n    ? collection.config.upload.modifyResponseHeaders({ headers }) || headers\n    : headers\n\n  return new Response(data, {\n    headers: headersWithCors({\n      headers,\n      req,\n    }),\n    status,\n  })\n}\n"],"names":["fileTypeFromFile","fsPromises","status","httpStatus","path","APIError","checkFileAccess","streamFile","getFileTypeFallback","parseRangeHeader","getRequestCollection","headersWithCors","getFileHandler","req","collection","filename","routeParams","prefix","searchParams","get","undefined","config","upload","slug","BAD_REQUEST","accessResult","Response","handlers","length","customResponse","headers","Headers","handler","doc","params","fileDir","staticDir","resolvedDir","resolve","filePath","startsWith","sep","stats","stat","err","code","payload","logger","error","json","errors","message","fileTypeResult","mimeType","mime","endsWith","rangeHeader","rangeResult","fileSize","size","type","set","modifyResponseHeaders","REQUESTED_RANGE_NOT_SATISFIABLE","data","isPartial","range","contentLength","end","start","String","options","PARTIAL_CONTENT","OK"],"mappings":"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"}