@fragno-dev/upload 0.1.1 → 0.1.3
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 +148 -12
- package/dist/browser/client/clients.js +17 -9
- package/dist/browser/client/clients.js.map +1 -1
- package/dist/browser/client/helpers.d.ts +15 -6
- package/dist/browser/client/helpers.d.ts.map +1 -1
- package/dist/browser/client/helpers.js +176 -30
- package/dist/browser/client/helpers.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{@nanostores_query@0.3.4_nanostores@1.1.0 → @nanostores_query@0.3.4_nanostores@1.2.0}/node_modules/@nanostores/query/dist/nanoquery.js +6 -6
- package/dist/browser/client/node_modules/.pnpm/{@nanostores_query@0.3.4_nanostores@1.1.0 → @nanostores_query@0.3.4_nanostores@1.2.0}/node_modules/@nanostores/query/dist/nanoquery.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{@nanostores_solid@1.1.1_nanostores@1.1.0_solid-js@1.9.10 → @nanostores_solid@1.1.1_nanostores@1.2.0_solid-js@1.9.10}/node_modules/@nanostores/solid/dist/index.js +2 -2
- package/dist/browser/client/node_modules/.pnpm/{@nanostores_solid@1.1.1_nanostores@1.1.0_solid-js@1.9.10 → @nanostores_solid@1.1.1_nanostores@1.2.0_solid-js@1.9.10}/node_modules/@nanostores/solid/dist/index.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/atom/index.js +2 -1
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.2.0/node_modules/nanostores/atom/index.js.map +1 -0
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.2.0/node_modules/nanostores/clean-stores/index.js +6 -0
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/clean-stores/index.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/computed/index.js +8 -5
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.2.0/node_modules/nanostores/computed/index.js.map +1 -0
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/lifecycle/index.js +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/lifecycle/index.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/listen-keys/index.js +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/listen-keys/index.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/map/index.js +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/map/index.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/task/index.js +1 -1
- package/dist/browser/client/node_modules/.pnpm/{nanostores@1.1.0 → nanostores@1.2.0}/node_modules/nanostores/task/index.js.map +1 -1
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.2.0/node_modules/nanostores/warn/index.js +16 -0
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.2.0/node_modules/nanostores/warn/index.js.map +1 -0
- package/dist/browser/client/packages/fragment-upload/src/definition.js +1 -42
- package/dist/browser/client/packages/fragment-upload/src/definition.js.map +1 -1
- package/dist/browser/client/packages/fragment-upload/src/routes/files.js +12 -5
- package/dist/browser/client/packages/fragment-upload/src/routes/files.js.map +1 -1
- package/dist/browser/client/packages/fragment-upload/src/routes/shared.js +3 -4
- package/dist/browser/client/packages/fragment-upload/src/routes/shared.js.map +1 -1
- package/dist/browser/client/packages/fragment-upload/src/routes/uploads.js +32 -21
- package/dist/browser/client/packages/fragment-upload/src/routes/uploads.js.map +1 -1
- package/dist/browser/client/packages/fragment-upload/src/schema.js +33 -3
- package/dist/browser/client/packages/fragment-upload/src/schema.js.map +1 -1
- package/dist/browser/client/packages/fragment-upload/src/types.d.ts +1 -2
- package/dist/browser/client/packages/fragment-upload/src/types.d.ts.map +1 -1
- package/dist/browser/client/packages/fragno/dist/client/client.js +28 -12
- package/dist/browser/client/packages/fragno/dist/client/client.js.map +1 -1
- package/dist/browser/client/packages/fragno/dist/client/client.svelte.js +11 -3
- package/dist/browser/client/packages/fragno/dist/client/client.svelte.js.map +1 -1
- package/dist/browser/client/packages/fragno/dist/client/react.js +104 -12
- package/dist/browser/client/packages/fragno/dist/client/react.js.map +1 -1
- package/dist/browser/client/packages/fragno/dist/client/solid.js +25 -11
- package/dist/browser/client/packages/fragno/dist/client/solid.js.map +1 -1
- package/dist/browser/client/packages/fragno/dist/client/vanilla.js +21 -1
- package/dist/browser/client/packages/fragno/dist/client/vanilla.js.map +1 -1
- package/dist/browser/client/packages/fragno/dist/client/vue.js +19 -11
- package/dist/browser/client/packages/fragno/dist/client/vue.js.map +1 -1
- package/dist/browser/client/react.d.ts +215 -192
- package/dist/browser/client/react.d.ts.map +1 -1
- package/dist/browser/client/react.js.map +1 -1
- package/dist/browser/client/solid.d.ts +218 -196
- package/dist/browser/client/solid.d.ts.map +1 -1
- package/dist/browser/client/solid.js.map +1 -1
- package/dist/browser/client/svelte.d.ts +216 -193
- package/dist/browser/client/svelte.d.ts.map +1 -1
- package/dist/browser/client/svelte.js.map +1 -1
- package/dist/browser/client/vanilla.d.ts +217 -195
- package/dist/browser/client/vanilla.d.ts.map +1 -1
- package/dist/browser/client/vanilla.js.map +1 -1
- package/dist/browser/client/vue.d.ts +217 -194
- package/dist/browser/client/vue.d.ts.map +1 -1
- package/dist/browser/client/vue.js.map +1 -1
- package/dist/cli/commands/files/delete.d.ts +4 -4
- package/dist/cli/commands/files/delete.d.ts.map +1 -1
- package/dist/cli/commands/files/delete.js +8 -10
- package/dist/cli/commands/files/delete.js.map +1 -1
- package/dist/cli/commands/files/download-url.d.ts +4 -4
- package/dist/cli/commands/files/download-url.d.ts.map +1 -1
- package/dist/cli/commands/files/download-url.js +8 -10
- package/dist/cli/commands/files/download-url.js.map +1 -1
- package/dist/cli/commands/files/download.d.ts +4 -4
- package/dist/cli/commands/files/download.d.ts.map +1 -1
- package/dist/cli/commands/files/download.js +10 -12
- package/dist/cli/commands/files/download.js.map +1 -1
- package/dist/cli/commands/files/get.d.ts +4 -4
- package/dist/cli/commands/files/get.d.ts.map +1 -1
- package/dist/cli/commands/files/get.js +8 -10
- package/dist/cli/commands/files/get.js.map +1 -1
- package/dist/cli/commands/files/list.d.ts +4 -4
- package/dist/cli/commands/files/list.d.ts.map +1 -1
- package/dist/cli/commands/files/list.js +6 -8
- package/dist/cli/commands/files/list.js.map +1 -1
- package/dist/cli/commands/files/update.d.ts +4 -4
- package/dist/cli/commands/files/update.d.ts.map +1 -1
- package/dist/cli/commands/files/update.js +8 -10
- package/dist/cli/commands/files/update.js.map +1 -1
- package/dist/cli/commands/files/upload.d.ts +4 -4
- package/dist/cli/commands/files/upload.d.ts.map +1 -1
- package/dist/cli/commands/files/upload.js +10 -12
- package/dist/cli/commands/files/upload.js.map +1 -1
- package/dist/cli/commands/uploads/abort.d.ts +2 -2
- package/dist/cli/commands/uploads/abort.d.ts.map +1 -1
- package/dist/cli/commands/uploads/abort.js.map +1 -1
- package/dist/cli/commands/uploads/complete.d.ts +2 -2
- package/dist/cli/commands/uploads/complete.d.ts.map +1 -1
- package/dist/cli/commands/uploads/complete.js.map +1 -1
- package/dist/cli/commands/uploads/content.d.ts +2 -2
- package/dist/cli/commands/uploads/content.d.ts.map +1 -1
- package/dist/cli/commands/uploads/content.js +1 -1
- package/dist/cli/commands/uploads/content.js.map +1 -1
- package/dist/cli/commands/uploads/create.d.ts +4 -4
- package/dist/cli/commands/uploads/create.d.ts.map +1 -1
- package/dist/cli/commands/uploads/create.js +8 -11
- package/dist/cli/commands/uploads/create.js.map +1 -1
- package/dist/cli/commands/uploads/get.d.ts +2 -2
- package/dist/cli/commands/uploads/get.d.ts.map +1 -1
- package/dist/cli/commands/uploads/get.js.map +1 -1
- package/dist/cli/commands/uploads/parts-complete.d.ts +2 -2
- package/dist/cli/commands/uploads/parts-complete.d.ts.map +1 -1
- package/dist/cli/commands/uploads/parts-complete.js.map +1 -1
- package/dist/cli/commands/uploads/parts-list.d.ts +2 -2
- package/dist/cli/commands/uploads/parts-list.d.ts.map +1 -1
- package/dist/cli/commands/uploads/parts-list.js.map +1 -1
- package/dist/cli/commands/uploads/parts-urls.d.ts +2 -2
- package/dist/cli/commands/uploads/parts-urls.d.ts.map +1 -1
- package/dist/cli/commands/uploads/parts-urls.js.map +1 -1
- package/dist/cli/commands/uploads/progress.d.ts +2 -2
- package/dist/cli/commands/uploads/progress.d.ts.map +1 -1
- package/dist/cli/commands/uploads/progress.js.map +1 -1
- package/dist/cli/commands/uploads/transfer.d.ts +4 -4
- package/dist/cli/commands/uploads/transfer.d.ts.map +1 -1
- package/dist/cli/commands/uploads/transfer.js +9 -12
- package/dist/cli/commands/uploads/transfer.js.map +1 -1
- package/dist/cli/index.d.ts +13 -13
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/client.js +22 -5
- package/dist/cli/utils/client.js.map +1 -1
- package/dist/cli/utils/options.js +7 -43
- package/dist/cli/utils/options.js.map +1 -1
- package/dist/node/cli/commands/files/delete.d.ts +4 -4
- package/dist/node/cli/commands/files/delete.d.ts.map +1 -1
- package/dist/node/cli/commands/files/delete.js +8 -10
- package/dist/node/cli/commands/files/delete.js.map +1 -1
- package/dist/node/cli/commands/files/download-url.d.ts +4 -4
- package/dist/node/cli/commands/files/download-url.d.ts.map +1 -1
- package/dist/node/cli/commands/files/download-url.js +8 -10
- package/dist/node/cli/commands/files/download-url.js.map +1 -1
- package/dist/node/cli/commands/files/download.d.ts +4 -4
- package/dist/node/cli/commands/files/download.d.ts.map +1 -1
- package/dist/node/cli/commands/files/download.js +9 -11
- package/dist/node/cli/commands/files/download.js.map +1 -1
- package/dist/node/cli/commands/files/get.d.ts +4 -4
- package/dist/node/cli/commands/files/get.d.ts.map +1 -1
- package/dist/node/cli/commands/files/get.js +8 -10
- package/dist/node/cli/commands/files/get.js.map +1 -1
- package/dist/node/cli/commands/files/list.d.ts +4 -4
- package/dist/node/cli/commands/files/list.d.ts.map +1 -1
- package/dist/node/cli/commands/files/list.js +6 -8
- package/dist/node/cli/commands/files/list.js.map +1 -1
- package/dist/node/cli/commands/files/update.d.ts +4 -4
- package/dist/node/cli/commands/files/update.d.ts.map +1 -1
- package/dist/node/cli/commands/files/update.js +8 -10
- package/dist/node/cli/commands/files/update.js.map +1 -1
- package/dist/node/cli/commands/files/upload.d.ts +4 -4
- package/dist/node/cli/commands/files/upload.d.ts.map +1 -1
- package/dist/node/cli/commands/files/upload.js +9 -11
- package/dist/node/cli/commands/files/upload.js.map +1 -1
- package/dist/node/cli/commands/uploads/abort.d.ts +2 -2
- package/dist/node/cli/commands/uploads/abort.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/abort.js.map +1 -1
- package/dist/node/cli/commands/uploads/complete.d.ts +2 -2
- package/dist/node/cli/commands/uploads/complete.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/complete.js.map +1 -1
- package/dist/node/cli/commands/uploads/content.d.ts +2 -2
- package/dist/node/cli/commands/uploads/content.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/content.js.map +1 -1
- package/dist/node/cli/commands/uploads/create.d.ts +4 -4
- package/dist/node/cli/commands/uploads/create.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/create.js +8 -11
- package/dist/node/cli/commands/uploads/create.js.map +1 -1
- package/dist/node/cli/commands/uploads/get.d.ts +2 -2
- package/dist/node/cli/commands/uploads/get.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/get.js.map +1 -1
- package/dist/node/cli/commands/uploads/parts-complete.d.ts +2 -2
- package/dist/node/cli/commands/uploads/parts-complete.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/parts-complete.js.map +1 -1
- package/dist/node/cli/commands/uploads/parts-list.d.ts +2 -2
- package/dist/node/cli/commands/uploads/parts-list.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/parts-list.js.map +1 -1
- package/dist/node/cli/commands/uploads/parts-urls.d.ts +2 -2
- package/dist/node/cli/commands/uploads/parts-urls.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/parts-urls.js.map +1 -1
- package/dist/node/cli/commands/uploads/progress.d.ts +2 -2
- package/dist/node/cli/commands/uploads/progress.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/progress.js.map +1 -1
- package/dist/node/cli/commands/uploads/transfer.d.ts +4 -4
- package/dist/node/cli/commands/uploads/transfer.d.ts.map +1 -1
- package/dist/node/cli/commands/uploads/transfer.js +8 -11
- package/dist/node/cli/commands/uploads/transfer.js.map +1 -1
- package/dist/node/cli/index.d.ts +13 -13
- package/dist/node/cli/index.d.ts.map +1 -1
- package/dist/node/cli/index.js +14 -14
- package/dist/node/cli/index.js.map +1 -1
- package/dist/node/cli/utils/client.js +22 -5
- package/dist/node/cli/utils/client.js.map +1 -1
- package/dist/node/cli/utils/options.js +7 -43
- package/dist/node/cli/utils/options.js.map +1 -1
- package/dist/node/client/clients.d.ts +217 -194
- package/dist/node/client/clients.d.ts.map +1 -1
- package/dist/node/client/clients.js +17 -9
- package/dist/node/client/clients.js.map +1 -1
- package/dist/node/client/helpers.d.ts +15 -6
- package/dist/node/client/helpers.d.ts.map +1 -1
- package/dist/node/client/helpers.js +176 -30
- package/dist/node/client/helpers.js.map +1 -1
- package/dist/node/client/react.d.ts +217 -194
- package/dist/node/client/react.d.ts.map +1 -1
- package/dist/node/client/react.js.map +1 -1
- package/dist/node/client/solid.d.ts +218 -196
- package/dist/node/client/solid.d.ts.map +1 -1
- package/dist/node/client/solid.js.map +1 -1
- package/dist/node/client/svelte.d.ts +216 -193
- package/dist/node/client/svelte.d.ts.map +1 -1
- package/dist/node/client/svelte.js.map +1 -1
- package/dist/node/client/vanilla.d.ts +217 -195
- package/dist/node/client/vanilla.d.ts.map +1 -1
- package/dist/node/client/vanilla.js.map +1 -1
- package/dist/node/client/vue.d.ts +217 -194
- package/dist/node/client/vue.d.ts.map +1 -1
- package/dist/node/client/vue.js.map +1 -1
- package/dist/node/config.d.ts +6 -6
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/config.js.map +1 -1
- package/dist/node/definition.d.ts +588 -219
- package/dist/node/definition.d.ts.map +1 -1
- package/dist/node/definition.js +27 -3
- package/dist/node/definition.js.map +1 -1
- package/dist/node/file-key.d.ts +19 -0
- package/dist/node/file-key.d.ts.map +1 -0
- package/dist/node/file-key.js +47 -0
- package/dist/node/file-key.js.map +1 -0
- package/dist/node/index.d.ts +582 -175
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +3 -2
- package/dist/node/index.js.map +1 -1
- package/dist/node/routes/files.js +99 -64
- package/dist/node/routes/files.js.map +1 -1
- package/dist/node/routes/index.d.ts +1497 -721
- package/dist/node/routes/index.d.ts.map +1 -1
- package/dist/node/routes/shared.js +5 -9
- package/dist/node/routes/shared.js.map +1 -1
- package/dist/node/routes/uploads.js +105 -47
- package/dist/node/routes/uploads.js.map +1 -1
- package/dist/node/schema.d.ts +6 -6
- package/dist/node/schema.d.ts.map +1 -1
- package/dist/node/schema.js +12 -3
- package/dist/node/schema.js.map +1 -1
- package/dist/node/services/files.d.ts +6 -2
- package/dist/node/services/files.d.ts.map +1 -1
- package/dist/node/services/files.js +22 -20
- package/dist/node/services/files.js.map +1 -1
- package/dist/node/services/helpers.js +37 -15
- package/dist/node/services/helpers.js.map +1 -1
- package/dist/node/services/uploads.d.ts +10 -5
- package/dist/node/services/uploads.d.ts.map +1 -1
- package/dist/node/services/uploads.js +340 -63
- package/dist/node/services/uploads.js.map +1 -1
- package/dist/node/storage/fs.d.ts.map +1 -1
- package/dist/node/storage/fs.js +16 -10
- package/dist/node/storage/fs.js.map +1 -1
- package/dist/node/storage/object-key.js +36 -0
- package/dist/node/storage/object-key.js.map +1 -0
- package/dist/node/storage/r2-binding.d.ts +59 -0
- package/dist/node/storage/r2-binding.d.ts.map +1 -0
- package/dist/node/storage/r2-binding.js +245 -0
- package/dist/node/storage/r2-binding.js.map +1 -0
- package/dist/node/storage/r2.d.ts +6 -5
- package/dist/node/storage/r2.d.ts.map +1 -1
- package/dist/node/storage/s3.d.ts.map +1 -1
- package/dist/node/storage/s3.js +16 -10
- package/dist/node/storage/s3.js.map +1 -1
- package/dist/node/storage/types.d.ts +6 -5
- package/dist/node/storage/types.d.ts.map +1 -1
- package/dist/node/types.d.ts +1 -2
- package/dist/node/types.d.ts.map +1 -1
- package/package.json +26 -46
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.1.0/node_modules/nanostores/atom/index.js.map +0 -1
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.1.0/node_modules/nanostores/clean-stores/index.js +0 -6
- package/dist/browser/client/node_modules/.pnpm/nanostores@1.1.0/node_modules/nanostores/computed/index.js.map +0 -1
- package/dist/browser/client/packages/fragment-upload/src/keys.d.ts +0 -7
- package/dist/browser/client/packages/fragment-upload/src/keys.d.ts.map +0 -1
- package/dist/browser/client/packages/fragment-upload/src/keys.js +0 -28
- package/dist/browser/client/packages/fragment-upload/src/keys.js.map +0 -1
- package/dist/browser/index-BdjKPO4J.d.ts +0 -177
- package/dist/browser/index-BdjKPO4J.d.ts.map +0 -1
- package/dist/browser/index.js +0 -3
- package/dist/browser/src-vdNJUbjT.js +0 -1982
- package/dist/browser/src-vdNJUbjT.js.map +0 -1
- package/dist/cli/keys.js +0 -32
- package/dist/cli/keys.js.map +0 -1
- package/dist/node/keys.d.ts +0 -12
- package/dist/node/keys.d.ts.map +0 -1
- package/dist/node/keys.js +0 -63
- package/dist/node/keys.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,148 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Upload Fragment
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Full-stack upload fragment for Fragno. It supports direct-to-storage uploads (signed URLs), proxy
|
|
4
|
+
uploads, and one-shot multipart form uploads. Strategy selection is driven by the storage adapter in
|
|
5
|
+
`storage.initUpload` and the configured thresholds.
|
|
6
|
+
|
|
7
|
+
## Upload Inventory
|
|
8
|
+
|
|
9
|
+
Routes below are relative to the fragment mount (for example, `/api/uploads`). Upload session
|
|
10
|
+
responses include the resolved `provider` plus canonical provider-sticky follow-up URLs:
|
|
11
|
+
`statusEndpoint`, `progressEndpoint`, `completeEndpoint`, `abortEndpoint`, and the strategy-specific
|
|
12
|
+
`partsEndpoint`, `partsCompleteEndpoint`, or `contentEndpoint` when applicable.
|
|
13
|
+
|
|
14
|
+
**Direct single (signed URL to storage)** Steps:
|
|
15
|
+
|
|
16
|
+
1. `POST /uploads` returns `uploadUrl`, `uploadHeaders`, and canonical follow-up URLs. Writes:
|
|
17
|
+
`upload`.
|
|
18
|
+
2. Client `PUT` to the signed URL. Writes: none.
|
|
19
|
+
3. `POST /uploads/:uploadId/complete`. Writes: `upload`, `file`.
|
|
20
|
+
4. Optional `POST /uploads/:uploadId/progress`. Writes: `upload`.
|
|
21
|
+
|
|
22
|
+
Storage ops: `initUpload` (before DB availability checks), optional `finalizeUpload` (before
|
|
23
|
+
completion writes). DB ops: after `initUpload`, read `file` + `upload` (availability), write
|
|
24
|
+
`upload`, then mark complete + insert `file`.
|
|
25
|
+
|
|
26
|
+
**Direct multipart (signed part URLs)** Steps:
|
|
27
|
+
|
|
28
|
+
1. `POST /uploads` creates the upload and returns canonical follow-up URLs. Writes: `upload`.
|
|
29
|
+
2. `POST /uploads/:uploadId/parts` returns signed part URLs. Writes: none.
|
|
30
|
+
3. Client `PUT` to part URLs. Writes: none.
|
|
31
|
+
4. `POST /uploads/:uploadId/parts/complete`. Writes: `upload_part`, `upload`.
|
|
32
|
+
5. `POST /uploads/:uploadId/complete`. Writes: `upload`, `file`.
|
|
33
|
+
|
|
34
|
+
Storage ops: `initUpload` (before DB availability checks), `getPartUploadUrls`,
|
|
35
|
+
`completeMultipartUpload`. DB ops: after `initUpload`, read `file` + `upload` (availability) and
|
|
36
|
+
write `upload`; later read `upload`, write `upload_part` rows + progress, then mark complete +
|
|
37
|
+
insert `file`.
|
|
38
|
+
|
|
39
|
+
**Proxy stream (upload session)** Steps:
|
|
40
|
+
|
|
41
|
+
1. `POST /uploads` creates the upload and returns canonical follow-up URLs. Writes: `upload`.
|
|
42
|
+
2. `PUT /uploads/:uploadId/content` streams content through the fragment. Writes: `upload`, `file`.
|
|
43
|
+
|
|
44
|
+
Storage ops: `initUpload` (before DB availability checks), `writeStream`. DB ops: after
|
|
45
|
+
`initUpload`, read `file` + `upload` (availability) and write `upload`; then mark complete + insert
|
|
46
|
+
`file` (or mark failed on storage error).
|
|
47
|
+
|
|
48
|
+
**One-shot multipart form (server mediated)** Steps:
|
|
49
|
+
|
|
50
|
+
1. `POST /files` with `multipart/form-data`. Writes: `upload`, `file`.
|
|
51
|
+
2. Server chooses proxy stream or server-side `PUT` to `uploadUrl`. Writes: none.
|
|
52
|
+
3. Upload finalizes on success. Writes: none.
|
|
53
|
+
|
|
54
|
+
Storage ops: `initUpload`, `writeStream` OR server `PUT` to `uploadUrl`, optional `finalizeUpload`
|
|
55
|
+
(before DB writes). DB ops: after storage succeeds, read availability, then create completed
|
|
56
|
+
`upload` + `file` (or failed `upload` on error).
|
|
57
|
+
|
|
58
|
+
Note: If `initUpload` returns `direct-multipart`, this route responds with
|
|
59
|
+
`409 UPLOAD_INVALID_STATE` and you should use the multipart session flow instead.
|
|
60
|
+
|
|
61
|
+
**Helpers (status, progress, abort)** Steps:
|
|
62
|
+
|
|
63
|
+
1. `GET /uploads/:uploadId` fetches status. Writes: none.
|
|
64
|
+
2. `POST /uploads/:uploadId/progress` records progress. Writes: `upload`.
|
|
65
|
+
3. `POST /uploads/:uploadId/abort` cancels the upload. Writes: `upload`.
|
|
66
|
+
|
|
67
|
+
Helper behavior note:
|
|
68
|
+
|
|
69
|
+
- Official helpers use the returned canonical follow-up URLs instead of reconstructing
|
|
70
|
+
`/uploads/:uploadId/...` paths locally.
|
|
71
|
+
- For proxy uploads (`PUT /uploads/:uploadId/content`), helpers try streamed upload first and then a
|
|
72
|
+
buffered fallback.
|
|
73
|
+
- If both attempts fail at the transport layer, helpers throw an actionable error suggesting
|
|
74
|
+
`POST /files` (one-shot multipart) or switching to a direct strategy.
|
|
75
|
+
|
|
76
|
+
## Upload Flow Diagram
|
|
77
|
+
|
|
78
|
+
```mermaid
|
|
79
|
+
flowchart TD
|
|
80
|
+
C[Client]
|
|
81
|
+
F[Upload Fragment]
|
|
82
|
+
S[(Storage Adapter)]
|
|
83
|
+
D[(DB)]
|
|
84
|
+
|
|
85
|
+
C -->|"POST /uploads"| F
|
|
86
|
+
F -->|"storage.initUpload"| S
|
|
87
|
+
F -->|"DB read: availability"| D
|
|
88
|
+
F -->|"DB write: create upload"| D
|
|
89
|
+
|
|
90
|
+
C -->|"PUT uploadUrl (direct single)"| S
|
|
91
|
+
C -->|"POST /uploads/:uploadId/complete"| F
|
|
92
|
+
F -->|"DB read: upload"| D
|
|
93
|
+
F -->|"storage.finalizeUpload (optional)"| S
|
|
94
|
+
F -->|"DB write: complete upload + file"| D
|
|
95
|
+
|
|
96
|
+
C -->|"POST /uploads/:uploadId/parts"| F
|
|
97
|
+
F -->|"DB read: upload"| D
|
|
98
|
+
F -->|"storage.getPartUploadUrls"| S
|
|
99
|
+
C -->|"PUT part URLs"| S
|
|
100
|
+
C -->|"POST /uploads/:uploadId/parts/complete"| F
|
|
101
|
+
F -->|"DB write: upload_part + progress"| D
|
|
102
|
+
C -->|"POST /uploads/:uploadId/complete"| F
|
|
103
|
+
F -->|"storage.completeMultipartUpload"| S
|
|
104
|
+
F -->|"DB write: complete upload + file"| D
|
|
105
|
+
|
|
106
|
+
C -->|"PUT /uploads/:uploadId/content (proxy)"| F
|
|
107
|
+
F -->|"storage.writeStream"| S
|
|
108
|
+
F -->|"DB write: complete upload + file"| D
|
|
109
|
+
|
|
110
|
+
C -->|"POST /files (multipart form)"| F
|
|
111
|
+
F -->|"storage.initUpload"| S
|
|
112
|
+
F -->|"storage.writeStream OR server PUT uploadUrl"| S
|
|
113
|
+
F -->|"storage.finalizeUpload (optional)"| S
|
|
114
|
+
F -->|"DB read: availability"| D
|
|
115
|
+
F -->|"DB write: completed upload + file"| D
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Durable Hooks and DB Entities
|
|
119
|
+
|
|
120
|
+
Durable hooks are persisted to the internal `fragno_hooks` table when `uow.triggerHook(...)` is
|
|
121
|
+
called inside a DB transaction. Hook execution is out-of-band and may re-read or update upload
|
|
122
|
+
entities.
|
|
123
|
+
|
|
124
|
+
**onUploadTimeout (scheduled when `POST /uploads` creates a new upload)** Steps:
|
|
125
|
+
|
|
126
|
+
1. Writes: `fragno_hooks` (process time = `upload.expiresAt`, payload includes `uploadId`).
|
|
127
|
+
2. When executed, reads: `upload`. If still active and expired, writes: `upload` (status `expired`,
|
|
128
|
+
error fields, timestamps).
|
|
129
|
+
3. Invokes `config.onUploadFailed` (no DB writes from the hook itself).
|
|
130
|
+
|
|
131
|
+
**onFileReady (after `upload` completes + `file` is created)** Steps:
|
|
132
|
+
|
|
133
|
+
1. Writes: `fragno_hooks`.
|
|
134
|
+
2. Hook execution is notification-only; no DB writes.
|
|
135
|
+
|
|
136
|
+
**onUploadFailed (after upload failure or abort)** Steps:
|
|
137
|
+
|
|
138
|
+
1. Writes: `fragno_hooks`.
|
|
139
|
+
2. Hook execution is notification-only; no DB writes.
|
|
140
|
+
|
|
141
|
+
**onFileDeleted (after `file` is marked deleted)** Steps:
|
|
142
|
+
|
|
143
|
+
1. Writes: `fragno_hooks`.
|
|
144
|
+
2. Hook execution deletes the storage object via `storage.deleteObject`.
|
|
145
|
+
3. Invokes `config.onFileDeleted` (the storage delete is retried durably first).
|
|
4
146
|
|
|
5
147
|
## Build
|
|
6
148
|
|
|
@@ -22,10 +164,10 @@ node packages/fragment-upload/bin/run.js --help
|
|
|
22
164
|
```bash
|
|
23
165
|
# Use the CLI (base URL points to the mounted upload fragment)
|
|
24
166
|
fragno-upload --help
|
|
25
|
-
fragno-upload uploads create -b https://host.example.com/api/uploads --file-key
|
|
26
|
-
fragno-upload uploads transfer -b https://host.example.com/api/uploads -f ./demo.txt --file-key
|
|
27
|
-
fragno-upload files list -b https://host.example.com/api/uploads --prefix
|
|
28
|
-
fragno-upload files download -b https://host.example.com/api/uploads --file-key
|
|
167
|
+
fragno-upload uploads create -b https://host.example.com/api/uploads --provider r2-binding --file-key users/42/avatar --filename demo.txt --size-bytes 10 --content-type text/plain
|
|
168
|
+
fragno-upload uploads transfer -b https://host.example.com/api/uploads -f ./demo.txt --provider r2-binding --file-key users/42/avatar
|
|
169
|
+
fragno-upload files list -b https://host.example.com/api/uploads --provider r2-binding --prefix users/42/
|
|
170
|
+
fragno-upload files download -b https://host.example.com/api/uploads --provider r2-binding --file-key users/42/avatar -o ./download.txt
|
|
29
171
|
```
|
|
30
172
|
|
|
31
173
|
Environment defaults:
|
|
@@ -35,9 +177,3 @@ Environment defaults:
|
|
|
35
177
|
- `FRAGNO_UPLOAD_TIMEOUT_MS`
|
|
36
178
|
- `FRAGNO_UPLOAD_RETRIES`
|
|
37
179
|
- `FRAGNO_UPLOAD_RETRY_DELAY_MS`
|
|
38
|
-
|
|
39
|
-
## Next Steps
|
|
40
|
-
|
|
41
|
-
- Define your routes in `src/index.ts`
|
|
42
|
-
- Add framework-specific clients in `src/client/`
|
|
43
|
-
- See `AGENTS.md` for detailed development patterns
|
|
@@ -14,7 +14,7 @@ function createUploadFragmentClients(config = {}) {
|
|
|
14
14
|
});
|
|
15
15
|
return {
|
|
16
16
|
useFiles: builder.createHook("/files"),
|
|
17
|
-
useFile: builder.createHook("/files
|
|
17
|
+
useFile: builder.createHook("/files/by-key"),
|
|
18
18
|
useCreateUpload: builder.createMutator("POST", "/uploads"),
|
|
19
19
|
useUploadStatus: builder.createHook("/uploads/:uploadId"),
|
|
20
20
|
useCompleteUpload: builder.createMutator("POST", "/uploads/:uploadId/complete", (invalidate, params) => {
|
|
@@ -28,16 +28,24 @@ function createUploadFragmentClients(config = {}) {
|
|
|
28
28
|
if (!uploadId) return;
|
|
29
29
|
invalidate("GET", "/uploads/:uploadId", { pathParams: { uploadId } });
|
|
30
30
|
}),
|
|
31
|
-
useUpdateFile: builder.createMutator("PATCH", "/files
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
useUpdateFile: builder.createMutator("PATCH", "/files/by-key", (invalidate, params) => {
|
|
32
|
+
const provider = params.queryParams?.["provider"];
|
|
33
|
+
const key = params.queryParams?.["key"];
|
|
34
|
+
if (!provider || !key) return;
|
|
35
|
+
invalidate("GET", "/files/by-key", { queryParams: {
|
|
36
|
+
provider,
|
|
37
|
+
key
|
|
38
|
+
} });
|
|
35
39
|
invalidate("GET", "/files", {});
|
|
36
40
|
}),
|
|
37
|
-
useDeleteFile: builder.createMutator("DELETE", "/files
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
useDeleteFile: builder.createMutator("DELETE", "/files/by-key", (invalidate, params) => {
|
|
42
|
+
const provider = params.queryParams?.["provider"];
|
|
43
|
+
const key = params.queryParams?.["key"];
|
|
44
|
+
if (!provider || !key) return;
|
|
45
|
+
invalidate("GET", "/files/by-key", { queryParams: {
|
|
46
|
+
provider,
|
|
47
|
+
key
|
|
48
|
+
} });
|
|
41
49
|
invalidate("GET", "/files", {});
|
|
42
50
|
}),
|
|
43
51
|
useUploadHelpers: builder.createStore(helpers)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clients.js","names":["createClientBuilder","FragnoPublicClientConfig","uploadFragmentDefinition","uploadRoutes","createUploadHelpers","createUploadFragmentClients","config","builder","fetcher","defaultOptions","getFetcher","helpers","buildUrl","path","useFiles","createHook","useFile","useCreateUpload","createMutator","useUploadStatus","useCompleteUpload","invalidate","params","uploadId","pathParams","useAbortUpload","useUpdateFile","
|
|
1
|
+
{"version":3,"file":"clients.js","names":["createClientBuilder","FragnoPublicClientConfig","uploadFragmentDefinition","uploadRoutes","createUploadHelpers","createUploadFragmentClients","config","builder","fetcher","defaultOptions","getFetcher","helpers","buildUrl","path","useFiles","createHook","useFile","useCreateUpload","createMutator","useUploadStatus","useCompleteUpload","invalidate","params","uploadId","pathParams","useAbortUpload","useUpdateFile","provider","queryParams","key","useDeleteFile","useUploadHelpers","createStore"],"sources":["../../../src/client/clients.ts"],"sourcesContent":["import { createClientBuilder } from \"@fragno-dev/core/client\";\nimport type { FragnoPublicClientConfig } from \"@fragno-dev/core/client\";\n\nimport { uploadFragmentDefinition } from \"../definition\";\nimport { uploadRoutes } from \"../routes\";\nimport { createUploadHelpers } from \"./helpers\";\n\nexport function createUploadFragmentClients(config: FragnoPublicClientConfig = {}) {\n const builder = createClientBuilder(uploadFragmentDefinition, config, uploadRoutes);\n const { fetcher, defaultOptions } = builder.getFetcher();\n const helpers = createUploadHelpers({\n buildUrl: (path) => builder.buildUrl(path),\n fetcher,\n defaultOptions,\n });\n\n return {\n useFiles: builder.createHook(\"/files\"),\n useFile: builder.createHook(\"/files/by-key\"),\n useCreateUpload: builder.createMutator(\"POST\", \"/uploads\"),\n useUploadStatus: builder.createHook(\"/uploads/:uploadId\"),\n useCompleteUpload: builder.createMutator(\n \"POST\",\n \"/uploads/:uploadId/complete\",\n (invalidate, params) => {\n const uploadId = params.pathParams.uploadId;\n if (!uploadId) {\n return;\n }\n invalidate(\"GET\", \"/uploads/:uploadId\", { pathParams: { uploadId } });\n invalidate(\"GET\", \"/files\", {});\n },\n ),\n useAbortUpload: builder.createMutator(\n \"POST\",\n \"/uploads/:uploadId/abort\",\n (invalidate, params) => {\n const uploadId = params.pathParams.uploadId;\n if (!uploadId) {\n return;\n }\n invalidate(\"GET\", \"/uploads/:uploadId\", { pathParams: { uploadId } });\n },\n ),\n useUpdateFile: builder.createMutator(\"PATCH\", \"/files/by-key\", (invalidate, params) => {\n const provider = params.queryParams?.[\"provider\"];\n const key = params.queryParams?.[\"key\"];\n if (!provider || !key) {\n return;\n }\n invalidate(\"GET\", \"/files/by-key\", { queryParams: { provider, key } });\n invalidate(\"GET\", \"/files\", {});\n }),\n useDeleteFile: builder.createMutator(\"DELETE\", \"/files/by-key\", (invalidate, params) => {\n const provider = params.queryParams?.[\"provider\"];\n const key = params.queryParams?.[\"key\"];\n if (!provider || !key) {\n return;\n }\n invalidate(\"GET\", \"/files/by-key\", { queryParams: { provider, key } });\n invalidate(\"GET\", \"/files\", {});\n }),\n useUploadHelpers: builder.createStore(helpers),\n };\n}\n"],"mappings":";;;;;;AAOA,SAAgBK,4BAA4BC,SAAmC,EAAE,EAAE;CACjF,MAAMC,UAAUP,oBAAoBE,0BAA0BI,QAAQH,aAAa;CACnF,MAAM,EAAEK,SAASC,mBAAmBF,QAAQG,YAAY;CACxD,MAAMC,UAAUP,oBAAoB;EAClCQ,WAAWC,SAASN,QAAQK,SAASC,KAAK;EAC1CL;EACAC;EACD,CAAC;AAEF,QAAO;EACLK,UAAUP,QAAQQ,WAAW,SAAS;EACtCC,SAAST,QAAQQ,WAAW,gBAAgB;EAC5CE,iBAAiBV,QAAQW,cAAc,QAAQ,WAAW;EAC1DC,iBAAiBZ,QAAQQ,WAAW,qBAAqB;EACzDK,mBAAmBb,QAAQW,cACzB,QACA,gCACCG,YAAYC,WAAW;GACtB,MAAMC,WAAWD,OAAOE,WAAWD;AACnC,OAAI,CAACA,SACH;AAEFF,cAAW,OAAO,sBAAsB,EAAEG,YAAY,EAAED,UAAS,EAAG,CAAC;AACrEF,cAAW,OAAO,UAAU,EAAE,CAAC;IAElC;EACDI,gBAAgBlB,QAAQW,cACtB,QACA,6BACCG,YAAYC,WAAW;GACtB,MAAMC,WAAWD,OAAOE,WAAWD;AACnC,OAAI,CAACA,SACH;AAEFF,cAAW,OAAO,sBAAsB,EAAEG,YAAY,EAAED,UAAS,EAAG,CAAC;IAExE;EACDG,eAAenB,QAAQW,cAAc,SAAS,kBAAkBG,YAAYC,WAAW;GACrF,MAAMK,WAAWL,OAAOM,cAAc;GACtC,MAAMC,MAAMP,OAAOM,cAAc;AACjC,OAAI,CAACD,YAAY,CAACE,IAChB;AAEFR,cAAW,OAAO,iBAAiB,EAAEO,aAAa;IAAED;IAAUE;IAAI,EAAG,CAAC;AACtER,cAAW,OAAO,UAAU,EAAE,CAAC;IAC/B;EACFS,eAAevB,QAAQW,cAAc,UAAU,kBAAkBG,YAAYC,WAAW;GACtF,MAAMK,WAAWL,OAAOM,cAAc;GACtC,MAAMC,MAAMP,OAAOM,cAAc;AACjC,OAAI,CAACD,YAAY,CAACE,IAChB;AAEFR,cAAW,OAAO,iBAAiB,EAAEO,aAAa;IAAED;IAAUE;IAAI,EAAG,CAAC;AACtER,cAAW,OAAO,UAAU,EAAE,CAAC;IAC/B;EACFU,kBAAkBxB,QAAQyB,YAAYrB,QAAO;EAC9C"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FileKeyEncoded, FileKeyParts } from "./packages/fragment-upload/src/keys.js";
|
|
2
1
|
import { UploadChecksum } from "./packages/fragment-upload/src/storage/types.js";
|
|
3
2
|
import { FileMetadata, FileVisibility, UploadStrategy } from "./packages/fragment-upload/src/types.js";
|
|
4
3
|
|
|
@@ -10,8 +9,8 @@ type UploadProgress = {
|
|
|
10
9
|
totalParts?: number;
|
|
11
10
|
};
|
|
12
11
|
type CreateUploadAndTransferOptions = {
|
|
13
|
-
|
|
14
|
-
fileKey
|
|
12
|
+
provider: string;
|
|
13
|
+
fileKey: string;
|
|
15
14
|
filename?: string;
|
|
16
15
|
contentType?: string;
|
|
17
16
|
checksum?: UploadChecksum | null;
|
|
@@ -21,10 +20,16 @@ type CreateUploadAndTransferOptions = {
|
|
|
21
20
|
metadata?: Record<string, unknown>;
|
|
22
21
|
onProgress?: (progress: UploadProgress) => void;
|
|
23
22
|
};
|
|
23
|
+
type DownloadMethod = "signed-url" | "content";
|
|
24
|
+
type DownloadFileOptions = {
|
|
25
|
+
provider: string;
|
|
26
|
+
method: DownloadMethod;
|
|
27
|
+
};
|
|
24
28
|
type UploadCreateResponse = {
|
|
25
29
|
uploadId: string;
|
|
26
30
|
fileKey: string;
|
|
27
|
-
|
|
31
|
+
provider: string;
|
|
32
|
+
status: "created" | "in_progress";
|
|
28
33
|
strategy: UploadStrategy;
|
|
29
34
|
expiresAt: string;
|
|
30
35
|
upload: {
|
|
@@ -34,8 +39,12 @@ type UploadCreateResponse = {
|
|
|
34
39
|
uploadHeaders?: Record<string, string>;
|
|
35
40
|
partSizeBytes?: number;
|
|
36
41
|
maxParts?: number;
|
|
42
|
+
statusEndpoint: string;
|
|
43
|
+
progressEndpoint: string;
|
|
37
44
|
partsEndpoint?: string;
|
|
45
|
+
partsCompleteEndpoint?: string;
|
|
38
46
|
completeEndpoint: string;
|
|
47
|
+
abortEndpoint: string;
|
|
39
48
|
contentEndpoint?: string;
|
|
40
49
|
};
|
|
41
50
|
};
|
|
@@ -44,8 +53,8 @@ type UploadHelpers = {
|
|
|
44
53
|
upload: UploadCreateResponse;
|
|
45
54
|
file: FileMetadata;
|
|
46
55
|
}>;
|
|
47
|
-
downloadFile: (
|
|
56
|
+
downloadFile: (fileKey: string, options: DownloadFileOptions) => Promise<Response>;
|
|
48
57
|
};
|
|
49
58
|
//#endregion
|
|
50
|
-
export { CreateUploadAndTransferOptions, UploadCreateResponse, UploadHelpers };
|
|
59
|
+
export { CreateUploadAndTransferOptions, DownloadFileOptions, UploadCreateResponse, UploadHelpers };
|
|
51
60
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","names":[],"sources":["../../../src/client/helpers.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","names":[],"sources":["../../../src/client/helpers.ts"],"sourcesContent":[],"mappings":";;;;KAGY,cAAA;;EAAA,UAAA,EAAA,MAAc;EAOd,aAAA,EAAA,MAAA;EAA8B,UAAA,CAAA,EAAA,MAAA;;AAO3B,KAPH,8BAAA,GAOG;UAEF,EAAA,MAAA;SACa,EAAA,MAAA;EAAc,QAAA,CAAA,EAAA,MAAA;EAG5B,WAAA,CAAA,EAAA,MAAc;EAEd,QAAA,CAAA,EAVC,cAUkB,GAAA,IAErB;EAGE,IAAA,CAAA,EAAA,MAAA,EAAA;EAAoB,UAAA,CAAA,EAbjB,cAaiB;YAKpB,CAAA,EAAA,MAAA;UAMQ,CAAA,EAtBP,MAsBO,CAAA,MAAA,EAAA,OAAA,CAAA;EAAM,UAAA,CAAA,EAAA,CAAA,QAAA,EArBA,cAqBA,EAAA,GAAA,IAAA;AAa1B,CAAA;AAAyB,KA/Bb,cAAA,GA+Ba,YAAA,GAAA,SAAA;AAEf,KA/BE,mBAAA,GA+BF;UACG,EAAA,MAAA;QACY,EA/Bf,cA+Be;;AAAlB,KA5BK,oBAAA,GA4BL;UACoC,EAAA,MAAA;SAAgC,EAAA,MAAA;UAAR,EAAA,MAAA;EAAO,MAAA,EAAA,SAAA,GAAA,aAAA;YAxB9D;;;;;;oBAMQ;;;;;;;;;;;;KAaR,aAAA;kCAEF,eACG,mCACN;YAAkB;UAA4B;;2CACV,wBAAwB,QAAQ"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { encodeFileKey } from "./packages/fragment-upload/src/keys.js";
|
|
2
|
-
|
|
3
1
|
//#region src/client/helpers.ts
|
|
4
2
|
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
3
|
+
const PROXY_UPLOAD_STRATEGY_HINT = "Server selected proxy upload strategy for this file (no direct upload URL was returned). If you expected direct-to-storage upload, your active provider/config does not support direct upload for this request.";
|
|
4
|
+
const PROXY_UPLOAD_RECOVERY_HINT = "Verify the /uploads/:uploadId/content endpoint is reachable from the client, or switch provider/config so /uploads returns a direct strategy.";
|
|
5
|
+
const DOWNLOAD_METHOD_HINT = "Pick the download method explicitly: use 'signed-url' for GET /files/by-key/download-url, otherwise use 'content' for GET /files/by-key/content.";
|
|
5
6
|
const mergeHeaders = (base, next) => {
|
|
6
7
|
const merged = new Headers(base ?? void 0);
|
|
7
8
|
if (!next) return merged;
|
|
@@ -25,6 +26,74 @@ const readJsonSafely = async (response) => {
|
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
};
|
|
29
|
+
const toErrorMessage = (error) => {
|
|
30
|
+
if (error instanceof Error && error.message) return error.message;
|
|
31
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
32
|
+
return "Unknown network error";
|
|
33
|
+
};
|
|
34
|
+
const readMessageFromPayload = (payload) => {
|
|
35
|
+
if (payload && typeof payload === "object" && "message" in payload && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
const toAbsoluteUrl = (url) => {
|
|
39
|
+
try {
|
|
40
|
+
if (typeof window !== "undefined" && window.location?.origin) return new URL(url, window.location.origin);
|
|
41
|
+
return new URL(url, "http://fragno.local");
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const validateProxyContentUrl = (url) => {
|
|
47
|
+
const parsed = toAbsoluteUrl(url);
|
|
48
|
+
if (!parsed) return `Proxy upload endpoint '${url}' is not a valid URL.`;
|
|
49
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return `Proxy upload endpoint '${url}' must use http:// or https://.`;
|
|
50
|
+
if (typeof window !== "undefined" && window.location?.protocol === "https:" && parsed.protocol === "http:") return `Proxy upload endpoint '${url}' uses http:// on a https:// page.`;
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
const buildProxyUploadErrorMessage = (input) => {
|
|
54
|
+
const detailLines = [];
|
|
55
|
+
if (typeof input.status === "number") detailLines.push(`response status ${input.status}`);
|
|
56
|
+
if (input.streamError !== void 0) detailLines.push(`streamed upload error: ${toErrorMessage(input.streamError)}`);
|
|
57
|
+
if (input.fallbackError !== void 0) detailLines.push(`buffered upload error: ${toErrorMessage(input.fallbackError)}`);
|
|
58
|
+
const details = detailLines.length > 0 ? ` Details: ${detailLines.join(" | ")}.` : "";
|
|
59
|
+
return `${PROXY_UPLOAD_STRATEGY_HINT} Proxy upload to '${input.endpointUrl}' failed.${details} ${PROXY_UPLOAD_RECOVERY_HINT}`;
|
|
60
|
+
};
|
|
61
|
+
const buildDownloadRequestErrorMessage = (input) => {
|
|
62
|
+
const detailLines = [];
|
|
63
|
+
if (typeof input.status === "number") detailLines.push(`response status ${input.status}`);
|
|
64
|
+
const payloadMessage = readMessageFromPayload(input.payload);
|
|
65
|
+
if (payloadMessage) detailLines.push(`message: ${payloadMessage}`);
|
|
66
|
+
if (input.requestError !== void 0) detailLines.push(`request error: ${toErrorMessage(input.requestError)}`);
|
|
67
|
+
const details = detailLines.length > 0 ? ` Details: ${detailLines.join(" | ")}.` : "";
|
|
68
|
+
const hint = input.hint ? ` ${input.hint}` : "";
|
|
69
|
+
return `Download request to '${input.endpointUrl}' failed.${details}${hint}`;
|
|
70
|
+
};
|
|
71
|
+
const sanitizeSignedDownloadUrl = (url) => {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = new URL(url);
|
|
74
|
+
parsed.search = "";
|
|
75
|
+
parsed.hash = "";
|
|
76
|
+
return parsed.toString();
|
|
77
|
+
} catch {
|
|
78
|
+
return "<redacted-signed-url>";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const readErrorCodeFromPayload = (payload) => {
|
|
82
|
+
if (payload && typeof payload === "object" && "code" in payload) {
|
|
83
|
+
const code = payload.code;
|
|
84
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
const hasText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
89
|
+
const requireUploadEndpoint = (endpoint, label) => {
|
|
90
|
+
if (!hasText(endpoint)) throw new Error(`Missing ${label} endpoint for upload`);
|
|
91
|
+
return endpoint;
|
|
92
|
+
};
|
|
93
|
+
const buildByKeyQuery = (provider, fileKey) => new URLSearchParams({
|
|
94
|
+
provider,
|
|
95
|
+
key: fileKey
|
|
96
|
+
}).toString();
|
|
28
97
|
const createUploadHelpers = (input) => {
|
|
29
98
|
const { buildUrl, fetcher, defaultOptions } = input;
|
|
30
99
|
const fetchJson = async (path, init, expectedErrorCode) => {
|
|
@@ -38,9 +107,9 @@ const createUploadHelpers = (input) => {
|
|
|
38
107
|
}
|
|
39
108
|
return await response.json();
|
|
40
109
|
};
|
|
41
|
-
const reportProgress = async (
|
|
110
|
+
const reportProgress = async (progressEndpoint, progress, onProgress) => {
|
|
42
111
|
onProgress?.(progress);
|
|
43
|
-
await fetchJson(
|
|
112
|
+
await fetchJson(progressEndpoint, {
|
|
44
113
|
method: "POST",
|
|
45
114
|
headers: { "Content-Type": "application/json" },
|
|
46
115
|
body: JSON.stringify({
|
|
@@ -53,12 +122,13 @@ const createUploadHelpers = (input) => {
|
|
|
53
122
|
const filename = options.filename ?? (typeof File !== "undefined" && file instanceof File && file.name ? file.name : "upload");
|
|
54
123
|
const contentType = options.contentType ?? (file.type && file.type.length > 0 ? file.type : void 0) ?? DEFAULT_CONTENT_TYPE;
|
|
55
124
|
const sizeBytes = file.size;
|
|
56
|
-
if (!options.
|
|
125
|
+
if (!hasText(options.provider)) throw new Error("Provider is required");
|
|
126
|
+
if (!hasText(options.fileKey)) throw new Error("File key is required");
|
|
57
127
|
const upload = await fetchJson("/uploads", {
|
|
58
128
|
method: "POST",
|
|
59
129
|
headers: { "Content-Type": "application/json" },
|
|
60
130
|
body: JSON.stringify({
|
|
61
|
-
|
|
131
|
+
provider: options.provider,
|
|
62
132
|
fileKey: options.fileKey,
|
|
63
133
|
filename,
|
|
64
134
|
sizeBytes,
|
|
@@ -73,6 +143,7 @@ const createUploadHelpers = (input) => {
|
|
|
73
143
|
const totalBytes = sizeBytes;
|
|
74
144
|
let bytesUploaded = 0;
|
|
75
145
|
let partsUploaded = 0;
|
|
146
|
+
const progressEndpoint = requireUploadEndpoint(upload.upload.progressEndpoint, "progress");
|
|
76
147
|
if (upload.strategy === "direct-single") {
|
|
77
148
|
if (!upload.upload.uploadUrl) throw new Error("Missing upload URL for direct upload");
|
|
78
149
|
const uploadResponse = await fetcher(upload.upload.uploadUrl, {
|
|
@@ -83,7 +154,7 @@ const createUploadHelpers = (input) => {
|
|
|
83
154
|
if (!uploadResponse.ok) throw new Error(`Direct upload failed (${uploadResponse.status})`);
|
|
84
155
|
bytesUploaded = totalBytes;
|
|
85
156
|
partsUploaded = 1;
|
|
86
|
-
await reportProgress(
|
|
157
|
+
await reportProgress(progressEndpoint, {
|
|
87
158
|
bytesUploaded,
|
|
88
159
|
totalBytes,
|
|
89
160
|
partsUploaded,
|
|
@@ -101,6 +172,7 @@ const createUploadHelpers = (input) => {
|
|
|
101
172
|
if (upload.strategy === "direct-multipart") {
|
|
102
173
|
const partSizeBytes = upload.upload.partSizeBytes;
|
|
103
174
|
if (!partSizeBytes || !upload.upload.partsEndpoint) throw new Error("Missing multipart configuration for upload");
|
|
175
|
+
const partsCompleteEndpoint = requireUploadEndpoint(upload.upload.partsCompleteEndpoint, "multipart completion");
|
|
104
176
|
const totalParts = Math.ceil(totalBytes / partSizeBytes);
|
|
105
177
|
if (upload.upload.maxParts && totalParts > upload.upload.maxParts) throw new Error("Multipart upload exceeds maximum parts");
|
|
106
178
|
const partNumbers = Array.from({ length: totalParts }, (_, i) => i + 1);
|
|
@@ -135,14 +207,14 @@ const createUploadHelpers = (input) => {
|
|
|
135
207
|
});
|
|
136
208
|
bytesUploaded += partSize;
|
|
137
209
|
partsUploaded += 1;
|
|
138
|
-
await reportProgress(
|
|
210
|
+
await reportProgress(progressEndpoint, {
|
|
139
211
|
bytesUploaded,
|
|
140
212
|
totalBytes,
|
|
141
213
|
partsUploaded,
|
|
142
214
|
totalParts
|
|
143
215
|
}, options.onProgress);
|
|
144
216
|
}
|
|
145
|
-
await fetchJson(
|
|
217
|
+
await fetchJson(partsCompleteEndpoint, {
|
|
146
218
|
method: "POST",
|
|
147
219
|
headers: { "Content-Type": "application/json" },
|
|
148
220
|
body: JSON.stringify({ parts: completedParts })
|
|
@@ -157,6 +229,9 @@ const createUploadHelpers = (input) => {
|
|
|
157
229
|
};
|
|
158
230
|
}
|
|
159
231
|
if (!upload.upload.contentEndpoint) throw new Error("Missing proxy content endpoint for upload");
|
|
232
|
+
const proxyContentUrl = buildUrl(upload.upload.contentEndpoint);
|
|
233
|
+
const proxyUrlValidationError = validateProxyContentUrl(proxyContentUrl);
|
|
234
|
+
if (proxyUrlValidationError) throw new Error(`${proxyUrlValidationError} ${PROXY_UPLOAD_RECOVERY_HINT}`);
|
|
160
235
|
const source = file.stream();
|
|
161
236
|
const stream = new ReadableStream({ start(controller) {
|
|
162
237
|
const reader = source.getReader();
|
|
@@ -190,14 +265,27 @@ const createUploadHelpers = (input) => {
|
|
|
190
265
|
requestInit.duplex = "half";
|
|
191
266
|
let proxyResponse;
|
|
192
267
|
try {
|
|
193
|
-
proxyResponse = await fetcher(
|
|
194
|
-
} catch (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
268
|
+
proxyResponse = await fetcher(proxyContentUrl, requestInit);
|
|
269
|
+
} catch (streamError) {
|
|
270
|
+
let fallbackResponse;
|
|
271
|
+
try {
|
|
272
|
+
fallbackResponse = await fetcher(proxyContentUrl, buildRequestInit(defaultOptions, {
|
|
273
|
+
method: "PUT",
|
|
274
|
+
headers: { "Content-Type": DEFAULT_CONTENT_TYPE },
|
|
275
|
+
body: file
|
|
276
|
+
}));
|
|
277
|
+
} catch (fallbackError) {
|
|
278
|
+
throw new Error(buildProxyUploadErrorMessage({
|
|
279
|
+
endpointUrl: proxyContentUrl,
|
|
280
|
+
streamError,
|
|
281
|
+
fallbackError
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
if (!fallbackResponse.ok) throw new Error(buildProxyUploadErrorMessage({
|
|
285
|
+
endpointUrl: proxyContentUrl,
|
|
286
|
+
status: fallbackResponse.status,
|
|
287
|
+
streamError
|
|
199
288
|
}));
|
|
200
|
-
if (!fallbackResponse.ok) throw new Error(`Proxy upload failed (${fallbackResponse.status})`);
|
|
201
289
|
options.onProgress?.({
|
|
202
290
|
bytesUploaded: totalBytes,
|
|
203
291
|
totalBytes,
|
|
@@ -208,28 +296,86 @@ const createUploadHelpers = (input) => {
|
|
|
208
296
|
file: await fallbackResponse.json()
|
|
209
297
|
};
|
|
210
298
|
}
|
|
211
|
-
if (!proxyResponse.ok) throw new Error(
|
|
299
|
+
if (!proxyResponse.ok) throw new Error(buildProxyUploadErrorMessage({
|
|
300
|
+
endpointUrl: proxyContentUrl,
|
|
301
|
+
status: proxyResponse.status
|
|
302
|
+
}));
|
|
212
303
|
return {
|
|
213
304
|
upload,
|
|
214
305
|
file: await proxyResponse.json()
|
|
215
306
|
};
|
|
216
307
|
};
|
|
217
|
-
const downloadFile = async (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
308
|
+
const downloadFile = async (fileKey, options) => {
|
|
309
|
+
if (!hasText(fileKey)) throw new Error("File key is required");
|
|
310
|
+
if (!options || !hasText(options.provider)) throw new Error("Download provider is required");
|
|
311
|
+
if (!options || options.method !== "signed-url" && options.method !== "content") throw new Error(`Download method is required. ${DOWNLOAD_METHOD_HINT}`);
|
|
312
|
+
const byKeyQuery = buildByKeyQuery(options.provider, fileKey);
|
|
313
|
+
if (options.method === "signed-url") {
|
|
314
|
+
const downloadUrlEndpoint = buildUrl(`/files/by-key/download-url?${byKeyQuery}`);
|
|
315
|
+
let downloadUrlResponse;
|
|
316
|
+
try {
|
|
317
|
+
downloadUrlResponse = await fetcher(downloadUrlEndpoint, buildRequestInit(defaultOptions, { method: "GET" }));
|
|
318
|
+
} catch (error) {
|
|
319
|
+
throw new Error(buildDownloadRequestErrorMessage({
|
|
320
|
+
endpointUrl: downloadUrlEndpoint,
|
|
321
|
+
requestError: error
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
if (!downloadUrlResponse.ok) {
|
|
325
|
+
const errorPayload = await readJsonSafely(downloadUrlResponse);
|
|
326
|
+
const hint = readErrorCodeFromPayload(errorPayload) === "SIGNED_URL_UNSUPPORTED" ? "Requested method 'signed-url' is unsupported by this storage adapter. This is a programming error. Use method 'content' when streaming downloads are available." : void 0;
|
|
327
|
+
throw new Error(buildDownloadRequestErrorMessage({
|
|
328
|
+
endpointUrl: downloadUrlEndpoint,
|
|
329
|
+
status: downloadUrlResponse.status,
|
|
330
|
+
payload: errorPayload,
|
|
331
|
+
hint
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
221
334
|
const payload = await downloadUrlResponse.json();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
335
|
+
const sanitizedUrl = sanitizeSignedDownloadUrl(payload.url);
|
|
336
|
+
let response;
|
|
337
|
+
try {
|
|
338
|
+
response = await fetcher(payload.url, {
|
|
339
|
+
method: "GET",
|
|
340
|
+
headers: payload.headers
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new Error(buildDownloadRequestErrorMessage({
|
|
344
|
+
endpointUrl: sanitizedUrl,
|
|
345
|
+
requestError: error
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
const payloadError = await readJsonSafely(response);
|
|
350
|
+
throw new Error(buildDownloadRequestErrorMessage({
|
|
351
|
+
endpointUrl: sanitizedUrl,
|
|
352
|
+
status: response.status,
|
|
353
|
+
payload: payloadError
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
return response;
|
|
226
357
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
358
|
+
const contentEndpoint = buildUrl(`/files/by-key/content?${byKeyQuery}`);
|
|
359
|
+
let contentResponse;
|
|
360
|
+
try {
|
|
361
|
+
contentResponse = await fetcher(contentEndpoint, buildRequestInit(defaultOptions, { method: "GET" }));
|
|
362
|
+
} catch (error) {
|
|
363
|
+
throw new Error(buildDownloadRequestErrorMessage({
|
|
364
|
+
endpointUrl: contentEndpoint,
|
|
365
|
+
requestError: error
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
if (!contentResponse.ok) {
|
|
369
|
+
const contentError = await readJsonSafely(contentResponse);
|
|
370
|
+
const hint = readErrorCodeFromPayload(contentError) === "SIGNED_URL_UNSUPPORTED" ? "The 'content' download endpoint is unsupported by this storage adapter. This request used method 'content'. Use method 'signed-url' when signed downloads are available." : void 0;
|
|
371
|
+
throw new Error(buildDownloadRequestErrorMessage({
|
|
372
|
+
endpointUrl: contentEndpoint,
|
|
373
|
+
status: contentResponse.status,
|
|
374
|
+
payload: contentError,
|
|
375
|
+
hint
|
|
376
|
+
}));
|
|
231
377
|
}
|
|
232
|
-
return
|
|
378
|
+
return contentResponse;
|
|
233
379
|
};
|
|
234
380
|
return {
|
|
235
381
|
createUploadAndTransfer,
|