# Files and uploads (https://lazyfrog.dev/docs/guide/files-and-uploads)

{/* Generated by scripts/generate-docs.ts from services/gateway/src/guide.md. Do not edit. */}

- `/data` is the environment's file store: it survives restarts and publishes, and is in every
  snapshot. Files under `/data/public/` are served to everyone at `/files/<path>` (5-minute cache).
  Other files are private.
- An S3-compatible API over `/data` is at `S3_ENDPOINT` (`http://s3.local`), bucket `S3_BUCKET`,
  keys `S3_ACCESS_KEY`/`S3_SECRET_KEY`, region `S3_REGION`; the AWS SDK variables are set too, so
  `new S3Client({ forcePathStyle: true })` or `boto3.client("s3")` work with no options. An
  object's key is its path under `/data` (`public/avatars/1.png` is `/files/avatars/1.png`).
- Visitor uploads and private downloads: presign on the server with a second client whose
  `endpoint` is `S3_PUBLIC_ENDPOINT` (the site's own address + `/_lf/s3`) and `forcePathStyle:
  true`; the browser then PUTs or GETs that URL on the same origin (no CORS). A test or app on
  another origin (an e2e suite at `http://localhost:4173`) is refused until routing.json has a
  `cors` rule for `/_lf/s3/*` allowing that origin (a preview-only test origin is fine; the URL's
  signature stays the credential), or presign through the app's own origin. Presigned URLs last
  at most 15 minutes (`expiresIn` ≤ 900). Each request is capped at 100,000,000 bytes (95 MiB) at
  the edge: use multipart with parts of 64 MiB or less
  (`@aws-sdk/lib-storage` `Upload`, boto3 `upload_fileobj`) for larger files. Sign the type
  (`signableHeaders: new Set(["content-type"])`) and upload visitors' files to a private key,
  then check them on the server (type, size, re-encode images) and copy them to a new final key
  (under `public/` or private) that you serve: a presigned PUT can't cap the size, and the upload
  key stays writable until its URL expires, so never serve the key that was uploaded to. Files served from `/files` never run as the site (they are
  sandboxed, and HTML downloads).
- A deleted or replaced public file stays in the edge cache up to its max-age (5 minutes unless
  the object or routing sets another): to take a photo down at once, give it a new name and stop
  linking the old one. Content-hashed names can be cached for a year.
- `/files/*` and `/_lf/*` are the platform's; an app's own `/files/...` route still works for any
  path with no file in `/data/public/`.
- The user's photos, and images your app made in the chat, come in with `fs_import` (see "Their
  own photos and files"). An image your app's image generation made always takes `generated:
  true` (anything you created, not a file the user attached), or it is recorded as the user's own
  photo; the same file again with `generated: true` corrects that.
