@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 +31 -0
- package/package.json +3 -3
- package/src/handleUpload.ts +5 -2
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.
|
|
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.
|
|
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.
|
|
34
|
+
"@quark-fw/core": "0.1.28"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/express": "^5.0.6",
|
package/src/handleUpload.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
130
|
+
log.error("upload failed", { err: e });
|
|
128
131
|
if (!res.headersSent) {
|
|
129
132
|
res.status(500).json({ error: "Не удалось сохранить файл" });
|
|
130
133
|
}
|