@quark-fw/plugin-uploads 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @quark-fw/plugin-uploads
2
+
3
+ File uploads for Quark apps: `POST /api/files` (multipart, local disk or S3-compatible storage), optional image variants via sharp, `GET /img/<id>/<variant>.<ext>` served with an immutable cache, and `authorize` / `keyFor` / `onSaved` hooks for the app.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i @quark-fw/plugin-uploads
9
+ ```
10
+
11
+ Peer: `@quark-fw/core`. `sharp` and `busboy` are dependencies; the S3 driver additionally needs `@aws-sdk/client-s3` (`npm i @aws-sdk/client-s3`, imported only when `UPLOAD_DRIVER=s3`).
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import "@quark-fw/plugin-uploads"; // provides quark.uploads, POST /api/files, GET /img, /uploads
17
+
18
+ // optional hooks (quark.uploads is read per request); the todo and tasktracker
19
+ // apps run with the defaults
20
+ quark.uploads.authorize = (req) => !!req.user; // default: any logged-in user
21
+ quark.uploads.keyFor = ({ mimeType, originalName }) => `img/${crypto.randomUUID()}`; // default YYYY-MM/<uuid><ext>
22
+ quark.uploads.onSaved = async (file) => file; // replace each response item, e.g. with an Image row
23
+ ```
24
+
25
+ Response: `{ files: [{ key, url, size, mimeType, originalName, width?, height?, variants? }] }`; `403` when not authorized, `400` for a bad multipart body, `413` when a file exceeds `maxFileSize` (everything saved in that request is deleted).
26
+
27
+ How it works: busboy streams each part (up to 20 files per request) into `quark.uploads.storage`. With `UPLOAD_VARIANTS` set, images are buffered, EXIF-rotated and resized to each width (`withoutEnlargement`, default webp, quality 80) and stored next to the original as `<key>/<name>.<ext>`. The `/img` route maps `/img/<id>/<name>.<ext>` to `<imageKeyPrefix><id>/<name>.<ext>` in storage and answers with `Cache-Control: public, max-age=31536000, immutable`; only extensions with a known inline type (png, jpg, gif, webp, avif, pdf) are served, originals without an extension are not. With the local driver, `uploads/` is served at `/uploads/<key>` with `nosniff`, and anything not in the inline list is sent as `Content-Disposition: attachment` (uploaded html/svg must never run as a page of the app). `storage.ts`, `images.ts` (`saveImage`, `processImage`, `deleteImage`) and `helpers.ts` (`createStorage`) are importable without quark, for scripts that run in a separate process.
28
+
29
+ Env: `UPLOAD_MAX_SIZE` (bytes, default 10 MiB), `UPLOAD_DRIVER` (`s3` or the local disk `uploads/` by default), `UPLOAD_S3_BUCKET`, `UPLOAD_S3_REGION` (default `us-east-1`), `UPLOAD_S3_ENDPOINT` (path-style for MinIO / R2 / Yandex), `UPLOAD_S3_ACCESS_KEY`, `UPLOAD_S3_SECRET_KEY`, `UPLOAD_S3_PUBLIC_URL` (public base URL of the bucket), `UPLOAD_S3_ACL` (e.g. `public-read`), `UPLOAD_VARIANTS` (`400,800` or JSON `{"400":{"width":400},"800":{"width":800,"format":"webp"}}`), `UPLOAD_IMAGE_PREFIX` (key prefix the `/img` route serves, default `img/`).
30
+
31
+ Part of the [Quark framework](https://www.npmjs.com/package/@quark-fw/core).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark-fw/plugin-uploads",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "type": "module",
@@ -24,14 +24,14 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "@quark-fw/plugin": "0.1.26",
27
+ "@quark-fw/plugin": "0.1.28",
28
28
  "@types/busboy": "^1.5.4",
29
29
  "busboy": "^1.6.0",
30
30
  "express": "^5.2.1",
31
31
  "sharp": "^0.34.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@quark-fw/core": "0.1.26"
34
+ "@quark-fw/core": "0.1.28"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/express": "^5.0.6",
@@ -5,6 +5,9 @@ import { safeExtension } from "./helpers.js";
5
5
  import { isImageMime, saveImage, type StoredImage } from "./images.js";
6
6
  import type { StoredFile } from "./storage.js";
7
7
  import {} from "@quark-fw/plugin-uploads/src/global.js";
8
+ import { log as rootLog } from "@quark-fw/core/src/log.js";
9
+
10
+ const log = rootLog.child({ scope: "uploads" });
8
11
 
9
12
  export const handleUpload = async (req: Request, res: Response) => {
10
13
  const uploads = quark.uploads;
@@ -112,7 +115,7 @@ export const handleUpload = async (req: Request, res: Response) => {
112
115
  return;
113
116
  }
114
117
  if (failure) {
115
- console.error("[uploads]", failure);
118
+ log.error("upload failed", { err: failure });
116
119
  res.status(500).json({ error: "Не удалось сохранить файл" });
117
120
  return;
118
121
  }
@@ -124,7 +127,7 @@ export const handleUpload = async (req: Request, res: Response) => {
124
127
  res.json({ files });
125
128
  } catch (e) {
126
129
  // сбой удаления хранилища — не необработанный reject в close
127
- console.error("[uploads]", e);
130
+ log.error("upload failed", { err: e });
128
131
  if (!res.headersSent) {
129
132
  res.status(500).json({ error: "Не удалось сохранить файл" });
130
133
  }