@visulima/storage 1.0.0-alpha.1 → 1.0.0-alpha.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/CHANGELOG.md +30 -0
- package/README.md +33 -14
- package/dist/adapter/nuxt/module.d.ts +39 -0
- package/dist/adapter/nuxt/module.js +93 -0
- package/dist/adapter/nuxt/package.json +20 -0
- package/dist/handler/base/base-handler-core.d.ts +91 -0
- package/dist/handler/base/base-handler-fetch.d.ts +76 -0
- package/dist/handler/base/base-handler-node.d.ts +137 -0
- package/dist/handler/http/fetch/index.d.ts +4 -0
- package/dist/handler/http/fetch/index.js +4 -0
- package/dist/handler/http/hono/index.d.ts +67 -0
- package/dist/handler/http/hono/index.js +65 -0
- package/dist/handler/http/nextjs/index.d.ts +61 -0
- package/dist/handler/http/nextjs/index.js +37 -0
- package/dist/handler/http/node/index.d.ts +4 -0
- package/dist/handler/http/node/index.js +4 -0
- package/dist/handler/http/solid-start/index.d.ts +60 -0
- package/dist/handler/http/solid-start/index.js +44 -0
- package/dist/handler/multipart/multipart-base.d.ts +84 -0
- package/dist/handler/multipart/multipart-fetch.d.ts +64 -0
- package/dist/handler/multipart/multipart.d.ts +49 -0
- package/dist/handler/rest/rest-base.d.ts +106 -0
- package/dist/handler/rest/rest-fetch.d.ts +88 -0
- package/dist/handler/rest/rest.d.ts +93 -0
- package/dist/handler/tus/tus-base.d.ts +152 -0
- package/dist/handler/tus/tus-fetch.d.ts +72 -0
- package/dist/handler/tus/tus.d.ts +78 -0
- package/dist/handler/types.d.ts +53 -0
- package/dist/handler/utils/request-parser.d.ts +72 -0
- package/dist/handler/utils/response-builder.d.ts +83 -0
- package/dist/handler/utils/storage-utils.d.ts +10 -0
- package/dist/handler/utils/stream-utils.d.ts +29 -0
- package/dist/handler/utils/upload-handlers.d.ts +76 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +13 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/no-op-metrics.d.ts +15 -0
- package/dist/metrics/opentelemetry-metrics.d.ts +55 -0
- package/dist/openapi/index.d.ts +5 -0
- package/dist/openapi/index.js +5 -0
- package/dist/openapi/rest.d.ts +7 -0
- package/dist/openapi/shared.d.ts +13 -0
- package/dist/openapi/transform.d.ts +3 -0
- package/dist/openapi/tus.d.ts +7 -0
- package/dist/openapi/xhr.d.ts +7 -0
- package/dist/packem_shared/AwsLightFile-tTneXZgG.js +11 -0
- package/dist/packem_shared/AwsLightMetaStorage-BWSOtVaN.js +4 -0
- package/dist/packem_shared/AwsLightStorage-Blv-GP4v.js +133 -0
- package/dist/packem_shared/AzureFile-CesgFzps.js +8 -0
- package/dist/packem_shared/AzureSMetaStorage-CFs-OYJT.js +88 -0
- package/dist/packem_shared/AzureStorage-B0v048hG.js +357 -0
- package/dist/packem_shared/BaseTransformer-C2gLib6v.js +82 -0
- package/dist/packem_shared/DiskStorage-BlltBst6.js +12 -0
- package/dist/packem_shared/DiskStorageWithChecksum-BJTmMklc.js +238 -0
- package/dist/packem_shared/ERRORS-D0apMqnc.js +97 -0
- package/dist/packem_shared/File-Bb3P23dr.js +69 -0
- package/dist/packem_shared/GCSConfig-vPP22kN6.js +11 -0
- package/dist/packem_shared/GCSFile-BIEunhAN.js +8 -0
- package/dist/packem_shared/GCSMetaStorage-BMFEdzJ7.js +7 -0
- package/dist/packem_shared/GCStorage-DnhiwaS9.js +406 -0
- package/dist/packem_shared/LocalMetaStorage-CZHhKkMd.js +7 -0
- package/dist/packem_shared/MediaTransformer-D6658DL1.js +1197 -0
- package/dist/packem_shared/MetaStorage-pECeFOad.js +46 -0
- package/dist/packem_shared/Metadata-DRLXeJ0F.js +89 -0
- package/dist/packem_shared/Multipart-CNFK_Okz.js +163 -0
- package/dist/packem_shared/Multipart-CtUL7BQw.js +128 -0
- package/dist/packem_shared/NetlifyBlobFile-CXzyjqrD.js +14 -0
- package/dist/packem_shared/NetlifyBlobMetaStorage-DxZ5aDvD.js +9 -0
- package/dist/packem_shared/NetlifyBlobStorage-CLPtjHqd.js +376 -0
- package/dist/packem_shared/NoOpMetrics-DhAk5rXc.js +10 -0
- package/dist/packem_shared/OpenTelemetryMetrics-BnxhqIaH.js +67 -0
- package/dist/packem_shared/Rest-CAOAEkOj.js +267 -0
- package/dist/packem_shared/Rest-DNuLwBrK.js +228 -0
- package/dist/packem_shared/S3File-DZiyk9Qt.js +11 -0
- package/dist/packem_shared/S3MetaStorage-Dz9aDabA.js +76 -0
- package/dist/packem_shared/S3Storage-DHmTk-hU.js +316 -0
- package/dist/packem_shared/TUS_RESUMABLE-GJzZ9R-f.js +434 -0
- package/dist/packem_shared/Tus-B8PmlMgR.js +200 -0
- package/dist/packem_shared/Tus-C4F1aYOl.js +195 -0
- package/dist/packem_shared/ValidationError-BfF1aE4h.js +26 -0
- package/dist/packem_shared/VercelBlobFile-BCg4aTEq.js +18 -0
- package/dist/packem_shared/VercelBlobMetaStorage-Bd9F-VFm.js +9 -0
- package/dist/packem_shared/VercelBlobStorage-DD9LMOS6.js +261 -0
- package/dist/packem_shared/_commonjsHelpers-B85MJLTf.js +5 -0
- package/dist/packem_shared/aws-light-meta-storage-DKJBgbR6.js +446 -0
- package/dist/packem_shared/backblaze-BlMnIcBC.js +20 -0
- package/dist/packem_shared/base-handler-core-BKuf4YLT.js +303 -0
- package/dist/packem_shared/base-handler-fetch-Cr0hLiqg.js +291 -0
- package/dist/packem_shared/base-handler-node-gk5aN9Cx.js +730 -0
- package/dist/packem_shared/cache-B88MXQ_2.js +18 -0
- package/dist/packem_shared/cloudflare-Bi1q8wXE.js +21 -0
- package/dist/packem_shared/defaultCloudStorageFileNameValidation-DUuacjWv.js +11 -0
- package/dist/packem_shared/digitalOcean-CWQRJM3L.js +21 -0
- package/dist/packem_shared/disk-storage-Bh2wy3YW.js +810 -0
- package/dist/packem_shared/gcs-meta-storage-0tgMNnPO.js +158 -0
- package/dist/packem_shared/has-content-CY66ehMK.js +3 -0
- package/dist/packem_shared/headers-DoS5nwM-.js +3220 -0
- package/dist/packem_shared/is-expired-CTThU1q5.js +8 -0
- package/dist/packem_shared/isRetryableError-Dycp7127.js +82 -0
- package/dist/packem_shared/isValidMediaType-BeDgiObq.js +35 -0
- package/dist/packem_shared/local-meta-storage-By-3SNBI.js +649 -0
- package/dist/packem_shared/minio-C2YBZQOw.js +22 -0
- package/dist/packem_shared/multipart-base-B3i67SSX.js +102 -0
- package/dist/packem_shared/part-match-BMNqHDYD.js +96 -0
- package/dist/packem_shared/path-CR6YkPXX-7R1-9CMk.js +161 -0
- package/dist/packem_shared/response-builder-BtnRiBUI.js +45 -0
- package/dist/packem_shared/rest-base-GRCcnan7.js +457 -0
- package/dist/packem_shared/restOpenApiSpec-CwcHHY2C.js +800 -0
- package/dist/packem_shared/s3-base-storage-C1DIHTYY.js +510 -0
- package/dist/packem_shared/sharedGet-Cpo7QUyu.js +1019 -0
- package/dist/packem_shared/storage-C6NSXb8h.js +1175 -0
- package/dist/packem_shared/tigris-Hac8TKnX.js +21 -0
- package/dist/packem_shared/transformOpenApiSpec-DWM5WtnN.js +975 -0
- package/dist/packem_shared/tusOpenApiSpec-6ohiToBy.js +740 -0
- package/dist/packem_shared/update-size-CCGm6i1J.js +8 -0
- package/dist/packem_shared/validator-BeX_lJet.js +78 -0
- package/dist/packem_shared/waitForStorage-Cscw85sx.js +18 -0
- package/dist/packem_shared/wasabi-DIyflHSd.js +21 -0
- package/dist/packem_shared/xhrOpenApiSpec-DS17brnx.js +264 -0
- package/dist/storage/aws/clients/backblaze.d.ts +12 -0
- package/dist/storage/aws/clients/cloudflare.d.ts +13 -0
- package/dist/storage/aws/clients/digital-ocean.d.ts +12 -0
- package/dist/storage/aws/clients/index.d.ts +6 -0
- package/dist/storage/aws/clients/index.js +6 -0
- package/dist/storage/aws/clients/minio.d.ts +13 -0
- package/dist/storage/aws/clients/tigris.d.ts +12 -0
- package/dist/storage/aws/clients/types.d.ts +95 -0
- package/dist/storage/aws/clients/wasabi.d.ts +12 -0
- package/dist/storage/aws/index.d.ts +4 -0
- package/dist/storage/aws/index.js +3 -0
- package/dist/storage/aws/s3-base-storage.d.ts +248 -0
- package/dist/storage/aws/s3-client-adapter.d.ts +110 -0
- package/dist/storage/aws/s3-file.d.ts +10 -0
- package/dist/storage/aws/s3-meta-storage.d.ts +15 -0
- package/dist/storage/aws/s3-storage.d.ts +70 -0
- package/dist/storage/aws/types.d.ts +119 -0
- package/dist/storage/aws-light/aws-light-api-adapter.d.ts +130 -0
- package/dist/storage/aws-light/aws-light-file.d.ts +10 -0
- package/dist/storage/aws-light/aws-light-meta-storage.d.ts +19 -0
- package/dist/storage/aws-light/aws-light-storage.d.ts +65 -0
- package/dist/storage/aws-light/index.d.ts +4 -0
- package/dist/storage/aws-light/index.js +3 -0
- package/dist/storage/aws-light/types.d.ts +37 -0
- package/dist/storage/azure/azure-file.d.ts +6 -0
- package/dist/storage/azure/azure-meta-storage.d.ts +15 -0
- package/dist/storage/azure/azure-storage.d.ts +69 -0
- package/dist/storage/azure/index.d.ts +4 -0
- package/dist/storage/azure/index.js +3 -0
- package/dist/storage/azure/types.d.ts +62 -0
- package/dist/storage/gcs/fetch-error.d.ts +11 -0
- package/dist/storage/gcs/gcs-config.d.ts +2 -0
- package/dist/storage/gcs/gcs-file.d.ts +6 -0
- package/dist/storage/gcs/gcs-meta-storage.d.ts +27 -0
- package/dist/storage/gcs/gcs-storage.d.ts +91 -0
- package/dist/storage/gcs/index.d.ts +5 -0
- package/dist/storage/gcs/index.js +4 -0
- package/dist/storage/gcs/types.d.ts +55 -0
- package/dist/storage/gcs/utils.d.ts +7 -0
- package/dist/storage/local/disk-storage-with-checksum.d.ts +15 -0
- package/dist/storage/local/disk-storage.d.ts +135 -0
- package/dist/storage/local/index.d.ts +4 -0
- package/dist/storage/local/index.js +3 -0
- package/dist/storage/local/local-meta-storage.d.ts +32 -0
- package/dist/storage/meta-storage.d.ts +30 -0
- package/dist/storage/netlify-blob/index.d.ts +4 -0
- package/dist/storage/netlify-blob/index.js +3 -0
- package/dist/storage/netlify-blob/netlify-blob-file.d.ts +12 -0
- package/dist/storage/netlify-blob/netlify-blob-meta-storage.d.ts +7 -0
- package/dist/storage/netlify-blob/netlify-blob-storage.d.ts +93 -0
- package/dist/storage/netlify-blob/types.d.ts +30 -0
- package/dist/storage/storage.d.ts +353 -0
- package/dist/storage/types.d.ts +221 -0
- package/dist/storage/utils/file/file.d.ts +25 -0
- package/dist/storage/utils/file/get-file-status.d.ts +9 -0
- package/dist/storage/utils/file/has-content.d.ts +9 -0
- package/dist/storage/utils/file/index.d.ts +10 -0
- package/dist/storage/utils/file/is-expired.d.ts +8 -0
- package/dist/storage/utils/file/metadata.d.ts +18 -0
- package/dist/storage/utils/file/part-match.d.ts +11 -0
- package/dist/storage/utils/file/types.d.ts +37 -0
- package/dist/storage/utils/file/update-metadata.d.ts +10 -0
- package/dist/storage/utils/file/update-size.d.ts +10 -0
- package/dist/storage/vercel-blob/index.d.ts +4 -0
- package/dist/storage/vercel-blob/index.js +3 -0
- package/dist/storage/vercel-blob/types.d.ts +32 -0
- package/dist/storage/vercel-blob/vercel-blob-file.d.ts +16 -0
- package/dist/storage/vercel-blob/vercel-blob-meta-storage.d.ts +7 -0
- package/dist/storage/vercel-blob/vercel-blob-storage.d.ts +76 -0
- package/dist/transformer/audio-transformer.d.ts +128 -0
- package/dist/transformer/audio-transformer.js +278 -0
- package/dist/transformer/base-transformer.d.ts +63 -0
- package/dist/transformer/image-transformer.d.ts +571 -0
- package/dist/transformer/image-transformer.js +1100 -0
- package/dist/transformer/index.d.ts +5 -0
- package/dist/transformer/index.js +4 -0
- package/dist/transformer/media-transformer.d.ts +332 -0
- package/dist/transformer/types.d.ts +652 -0
- package/dist/transformer/utils.d.ts +33 -0
- package/dist/transformer/validation-error.d.ts +23 -0
- package/dist/transformer/video-transformer.d.ts +145 -0
- package/dist/transformer/video-transformer.js +310 -0
- package/dist/utils/cache.d.ts +32 -0
- package/dist/utils/chunked-upload.d.ts +65 -0
- package/dist/utils/detect-file-type.d.ts +28 -0
- package/dist/utils/errors.d.ts +74 -0
- package/dist/utils/file-path-url-matcher.d.ts +18 -0
- package/dist/utils/headers.d.ts +111 -0
- package/dist/utils/http.d.ts +92 -0
- package/dist/utils/locker.d.ts +26 -0
- package/dist/utils/pipes/stream-checksum.d.ts +47 -0
- package/dist/utils/pipes/stream-length.d.ts +22 -0
- package/dist/utils/primitives/get-last-one.d.ts +3 -0
- package/dist/utils/primitives/is-record.d.ts +2 -0
- package/dist/utils/primitives/map-values.d.ts +9 -0
- package/dist/utils/primitives/pick.d.ts +2 -0
- package/dist/utils/primitives/to-milliseconds.d.ts +12 -0
- package/dist/utils/primitives/to-seconds.d.ts +12 -0
- package/dist/utils/range-checksum.d.ts +33 -0
- package/dist/utils/range-hasher.d.ts +46 -0
- package/dist/utils/retry.d.ts +64 -0
- package/dist/utils/types.d.ts +108 -0
- package/dist/utils/validation-error.d.ts +22 -0
- package/dist/utils/validator.d.ts +36 -0
- package/package.json +5 -5
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { c as createHttpError } from './headers-DoS5nwM-.js';
|
|
2
|
+
import { hasBody } from 'type-is';
|
|
3
|
+
import { a as getHeader } from './base-handler-core-BKuf4YLT.js';
|
|
4
|
+
import { ERRORS } from './ERRORS-D0apMqnc.js';
|
|
5
|
+
import { b as buildResponseFile, a as buildFileHeaders, c as buildChunkedUploadHeaders, d as buildFileMetadataHeaders } from './response-builder-BtnRiBUI.js';
|
|
6
|
+
|
|
7
|
+
const parseMetadata = (request, existingMetadata = {}) => {
|
|
8
|
+
const metadataHeader = getHeader(request, "x-file-metadata", true);
|
|
9
|
+
if (!metadataHeader) {
|
|
10
|
+
return existingMetadata;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return { ...existingMetadata, ...JSON.parse(metadataHeader) };
|
|
14
|
+
} catch {
|
|
15
|
+
return existingMetadata;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const parseContentDispositionValue = (contentDisposition) => {
|
|
19
|
+
if (!contentDisposition) {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
const maxSearchLength = 2e3;
|
|
23
|
+
const searchString = contentDisposition.length > maxSearchLength ? contentDisposition.slice(0, Math.max(0, maxSearchLength)) : contentDisposition;
|
|
24
|
+
const filenameIndex = searchString.toLowerCase().indexOf("filename");
|
|
25
|
+
if (filenameIndex === -1) {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
let equalsIndex = filenameIndex + 8;
|
|
29
|
+
while (equalsIndex < searchString.length && (searchString[equalsIndex] === "*" || searchString[equalsIndex] === " " || searchString[equalsIndex] === " ")) {
|
|
30
|
+
equalsIndex++;
|
|
31
|
+
}
|
|
32
|
+
if (equalsIndex >= searchString.length || searchString[equalsIndex] !== "=") {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
equalsIndex++;
|
|
36
|
+
while (equalsIndex < searchString.length && (searchString[equalsIndex] === " " || searchString[equalsIndex] === " ")) {
|
|
37
|
+
equalsIndex++;
|
|
38
|
+
}
|
|
39
|
+
if (equalsIndex >= searchString.length) {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
let valueStart = equalsIndex;
|
|
43
|
+
let valueEnd;
|
|
44
|
+
const firstChar = searchString[equalsIndex];
|
|
45
|
+
if (firstChar === '"' || firstChar === "'") {
|
|
46
|
+
valueStart = equalsIndex + 1;
|
|
47
|
+
valueEnd = searchString.indexOf(firstChar, valueStart);
|
|
48
|
+
if (valueEnd === -1) {
|
|
49
|
+
valueEnd = searchString.indexOf(";", valueStart);
|
|
50
|
+
if (valueEnd === -1) {
|
|
51
|
+
valueEnd = searchString.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
valueEnd = searchString.indexOf(";", valueStart);
|
|
56
|
+
if (valueEnd === -1) {
|
|
57
|
+
valueEnd = searchString.length;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (valueStart >= valueEnd) {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
const filename = searchString.substring(valueStart, valueEnd).trim();
|
|
64
|
+
return filename || void 0;
|
|
65
|
+
};
|
|
66
|
+
const parseContentDisposition = (request) => {
|
|
67
|
+
const contentDisposition = getHeader(request, "content-disposition", true);
|
|
68
|
+
return parseContentDispositionValue(contentDisposition);
|
|
69
|
+
};
|
|
70
|
+
const parseChunkHeaders = (request) => {
|
|
71
|
+
const isChunkedUpload = getHeader(request, "x-chunked-upload", true) === "true";
|
|
72
|
+
const chunkOffsetHeader = getHeader(request, "x-chunk-offset", true);
|
|
73
|
+
const totalSizeHeader = getHeader(request, "x-total-size", true);
|
|
74
|
+
const result = {
|
|
75
|
+
isChunkedUpload
|
|
76
|
+
};
|
|
77
|
+
if (chunkOffsetHeader) {
|
|
78
|
+
const offset = Number.parseInt(chunkOffsetHeader, 10);
|
|
79
|
+
if (!Number.isNaN(offset) && offset >= 0) {
|
|
80
|
+
result.chunkOffset = offset;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (totalSizeHeader) {
|
|
84
|
+
const size = Number.parseInt(totalSizeHeader, 10);
|
|
85
|
+
if (!Number.isNaN(size) && size > 0) {
|
|
86
|
+
result.totalSize = size;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
const validateRequestBody = (request, allowEmptyForChunked = false) => {
|
|
92
|
+
const isChunkedUpload = getHeader(request, "x-chunked-upload", true) === "true";
|
|
93
|
+
if (allowEmptyForChunked && isChunkedUpload) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!hasBody(request)) {
|
|
97
|
+
throw createHttpError(400, "Request body is required");
|
|
98
|
+
}
|
|
99
|
+
const contentLength = Number.parseInt(getHeader(request, "content-length") || "0", 10);
|
|
100
|
+
if (contentLength === 0 && !isChunkedUpload) {
|
|
101
|
+
throw createHttpError(400, "Content-Length is required and must be greater than 0");
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const validateContentLength = (request, allowZeroForChunked = false, maxSize) => {
|
|
105
|
+
const isChunkedUpload = getHeader(request, "x-chunked-upload", true) === "true";
|
|
106
|
+
const contentLength = Number.parseInt(getHeader(request, "content-length") || "0", 10);
|
|
107
|
+
if (!allowZeroForChunked && !isChunkedUpload && contentLength === 0) {
|
|
108
|
+
throw createHttpError(400, "Content-Length is required and must be greater than 0");
|
|
109
|
+
}
|
|
110
|
+
if (maxSize !== void 0 && contentLength > maxSize) {
|
|
111
|
+
throw createHttpError(413, `File size exceeds maximum allowed size of ${maxSize} bytes`);
|
|
112
|
+
}
|
|
113
|
+
return contentLength;
|
|
114
|
+
};
|
|
115
|
+
const extractFileInit = (request, contentLength, contentType = "application/octet-stream") => {
|
|
116
|
+
const originalName = parseContentDisposition(request);
|
|
117
|
+
const metadata = parseMetadata(request);
|
|
118
|
+
const { isChunkedUpload, totalSize } = parseChunkHeaders(request);
|
|
119
|
+
const fileSize = isChunkedUpload && totalSize ? totalSize : contentLength;
|
|
120
|
+
if (isChunkedUpload && totalSize) {
|
|
121
|
+
metadata._chunkedUpload = true;
|
|
122
|
+
metadata._chunks = [];
|
|
123
|
+
metadata._totalSize = totalSize;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
contentType,
|
|
127
|
+
metadata,
|
|
128
|
+
originalName,
|
|
129
|
+
size: fileSize
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const isUploadComplete = (chunks, totalSize) => {
|
|
134
|
+
if (chunks.length === 0) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const sorted = [...chunks].toSorted((a, b) => a.offset - b.offset);
|
|
138
|
+
const firstChunk = sorted[0];
|
|
139
|
+
if (!firstChunk || firstChunk.offset !== 0) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
let currentEnd = firstChunk.length;
|
|
143
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
144
|
+
const chunk = sorted[i];
|
|
145
|
+
if (!chunk) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (chunk.offset > currentEnd) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
currentEnd = Math.max(currentEnd, chunk.offset + chunk.length);
|
|
152
|
+
}
|
|
153
|
+
return currentEnd >= totalSize;
|
|
154
|
+
};
|
|
155
|
+
const validateChunk = (chunkOffset, chunkLength, totalSize, maxChunkSize) => {
|
|
156
|
+
if (Number.isNaN(chunkOffset) || chunkOffset < 0) {
|
|
157
|
+
throw new Error("Chunk offset must be a valid non-negative number");
|
|
158
|
+
}
|
|
159
|
+
if (chunkLength <= 0) {
|
|
160
|
+
throw new Error("Chunk length must be greater than 0");
|
|
161
|
+
}
|
|
162
|
+
if (chunkLength > maxChunkSize) {
|
|
163
|
+
throw new Error(`Chunk size exceeds maximum allowed size of ${maxChunkSize} bytes`);
|
|
164
|
+
}
|
|
165
|
+
if (chunkOffset + chunkLength > totalSize) {
|
|
166
|
+
throw new Error(`Chunk exceeds file size. Offset: ${chunkOffset}, Size: ${chunkLength}, Total: ${totalSize}`);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const trackChunk = (chunks, chunkInfo) => {
|
|
170
|
+
const existingChunk = chunks.find((chunk) => chunk.offset === chunkInfo.offset && chunk.length === chunkInfo.length);
|
|
171
|
+
if (!existingChunk) {
|
|
172
|
+
return [...chunks, chunkInfo];
|
|
173
|
+
}
|
|
174
|
+
if (chunkInfo.checksum && existingChunk.checksum !== chunkInfo.checksum) {
|
|
175
|
+
return chunks.map((chunk) => chunk.offset === chunkInfo.offset ? { ...chunk, checksum: chunkInfo.checksum } : chunk);
|
|
176
|
+
}
|
|
177
|
+
return chunks;
|
|
178
|
+
};
|
|
179
|
+
const isChunkedUpload = (file) => {
|
|
180
|
+
const metadata = file.metadata || {};
|
|
181
|
+
return metadata._chunkedUpload === true;
|
|
182
|
+
};
|
|
183
|
+
const getTotalSize = (file) => {
|
|
184
|
+
const metadata = file.metadata || {};
|
|
185
|
+
const isChunkedUploadFile = metadata._chunkedUpload === true;
|
|
186
|
+
if (isChunkedUploadFile && typeof metadata._totalSize === "number") {
|
|
187
|
+
return metadata._totalSize;
|
|
188
|
+
}
|
|
189
|
+
return void 0;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
class RestBase {
|
|
193
|
+
/**
|
|
194
|
+
* Handle single file deletion.
|
|
195
|
+
* @param id File ID to delete
|
|
196
|
+
* @returns Promise resolving to ResponseFile with deletion result
|
|
197
|
+
*/
|
|
198
|
+
async deleteSingle(id) {
|
|
199
|
+
const file = await this.storage.delete({ id });
|
|
200
|
+
if (file.status === void 0) {
|
|
201
|
+
throw createHttpError(404, "File not found");
|
|
202
|
+
}
|
|
203
|
+
return { ...file, headers: {}, statusCode: 204 };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Handle file creation (POST).
|
|
207
|
+
* @param config File initialization config
|
|
208
|
+
* @param isChunkedUpload Whether this is a chunked upload initialization
|
|
209
|
+
* @param requestUrl Request URL for Location header
|
|
210
|
+
* @param bodyStream Request body stream (for non-chunked uploads)
|
|
211
|
+
* @param contentLength Content length (for non-chunked uploads)
|
|
212
|
+
* @returns Promise resolving to ResponseFile with upload result
|
|
213
|
+
*/
|
|
214
|
+
async handlePost(config, isChunkedUpload2, requestUrl, bodyStream, contentLength) {
|
|
215
|
+
if (isChunkedUpload2 && config.size !== void 0) {
|
|
216
|
+
const size = typeof config.size === "number" ? config.size : Number.parseInt(String(config.size), 10);
|
|
217
|
+
if (size > 0 && size > this.storage.maxUploadSize) {
|
|
218
|
+
throw createHttpError(413, `File size exceeds maximum allowed size of ${this.storage.maxUploadSize} bytes`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const file = await this.storage.create(config);
|
|
222
|
+
if (isChunkedUpload2) {
|
|
223
|
+
const locationUrl2 = this.buildFileUrl(requestUrl, file);
|
|
224
|
+
return buildResponseFile(
|
|
225
|
+
file,
|
|
226
|
+
{
|
|
227
|
+
...buildFileHeaders(file, locationUrl2),
|
|
228
|
+
"X-Chunked-Upload": "true",
|
|
229
|
+
"X-Upload-ID": file.id
|
|
230
|
+
},
|
|
231
|
+
201
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const completedFile = await this.storage.write({
|
|
235
|
+
body: bodyStream,
|
|
236
|
+
contentLength,
|
|
237
|
+
id: file.id,
|
|
238
|
+
start: 0
|
|
239
|
+
});
|
|
240
|
+
const locationUrl = this.buildFileUrl(requestUrl, completedFile);
|
|
241
|
+
return buildResponseFile(completedFile, buildFileHeaders(completedFile, locationUrl), 201);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Handle file update or creation (PUT).
|
|
245
|
+
* @param id File ID from URL
|
|
246
|
+
* @param config File initialization config (for new files)
|
|
247
|
+
* @param requestUrl Request URL for Location header
|
|
248
|
+
* @param bodyStream Request body stream
|
|
249
|
+
* @param contentLength Content length
|
|
250
|
+
* @param metadata Optional metadata to merge (for updates)
|
|
251
|
+
* @returns Promise resolving to ResponseFile with upload result
|
|
252
|
+
*/
|
|
253
|
+
async handlePut(id, config, requestUrl, bodyStream, contentLength, metadata) {
|
|
254
|
+
let file;
|
|
255
|
+
let isUpdate = false;
|
|
256
|
+
try {
|
|
257
|
+
await this.storage.getMeta(id);
|
|
258
|
+
isUpdate = true;
|
|
259
|
+
if (metadata) {
|
|
260
|
+
await this.storage.update({ id }, { metadata });
|
|
261
|
+
}
|
|
262
|
+
file = await this.storage.write({
|
|
263
|
+
body: bodyStream,
|
|
264
|
+
contentLength,
|
|
265
|
+
id,
|
|
266
|
+
start: 0
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const errorWithCode = error;
|
|
270
|
+
if (errorWithCode.UploadErrorCode === ERRORS.FILE_NOT_FOUND || errorWithCode.code === "ENOENT") {
|
|
271
|
+
try {
|
|
272
|
+
const newFile = await this.storage.create(config);
|
|
273
|
+
file = await this.storage.write({
|
|
274
|
+
body: bodyStream,
|
|
275
|
+
contentLength,
|
|
276
|
+
id: newFile.id,
|
|
277
|
+
start: 0
|
|
278
|
+
});
|
|
279
|
+
} catch (createError) {
|
|
280
|
+
throw createError;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const locationUrl = this.buildFileUrl(requestUrl, file);
|
|
287
|
+
return buildResponseFile(file, buildFileHeaders(file, locationUrl), isUpdate ? 200 : 201);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Handle chunked upload chunk (PATCH).
|
|
291
|
+
* @param id File ID from URL
|
|
292
|
+
* @param chunkOffset Chunk offset in bytes
|
|
293
|
+
* @param contentLength Chunk content length
|
|
294
|
+
* @param chunkChecksum Optional chunk checksum
|
|
295
|
+
* @param requestUrl Request URL for Location header
|
|
296
|
+
* @param bodyStream Request body stream
|
|
297
|
+
* @returns Promise resolving to ResponseFile with upload progress
|
|
298
|
+
*/
|
|
299
|
+
async handlePatch(id, chunkOffset, contentLength, chunkChecksum, requestUrl, bodyStream) {
|
|
300
|
+
let file = await this.storage.getMeta(id);
|
|
301
|
+
const metadata = file.metadata || {};
|
|
302
|
+
const isChunkedUploadFile = isChunkedUpload(file);
|
|
303
|
+
if (isChunkedUploadFile) {
|
|
304
|
+
const totalSize2 = getTotalSize(file);
|
|
305
|
+
if (totalSize2 && file.size !== totalSize2) {
|
|
306
|
+
file = { ...file, size: totalSize2 };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const totalSize = typeof metadata._totalSize === "number" ? metadata._totalSize : file.size || 0;
|
|
310
|
+
if (!isChunkedUploadFile) {
|
|
311
|
+
throw createHttpError(400, "File is not a chunked upload. Use POST or PUT for full file uploads.");
|
|
312
|
+
}
|
|
313
|
+
const MAX_CHUNK_SIZE = 100 * 1024 * 1024;
|
|
314
|
+
try {
|
|
315
|
+
validateChunk(chunkOffset, contentLength, totalSize, MAX_CHUNK_SIZE);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof Error && error.message.includes("exceeds maximum")) {
|
|
318
|
+
throw createHttpError(413, error.message);
|
|
319
|
+
}
|
|
320
|
+
throw createHttpError(400, error instanceof Error ? error.message : String(error));
|
|
321
|
+
}
|
|
322
|
+
if (file.status === "completed") {
|
|
323
|
+
const locationUrl2 = this.buildFileUrl(requestUrl, file);
|
|
324
|
+
return buildResponseFile(
|
|
325
|
+
file,
|
|
326
|
+
{
|
|
327
|
+
...buildFileHeaders(file, locationUrl2),
|
|
328
|
+
"X-Upload-Complete": "true"
|
|
329
|
+
},
|
|
330
|
+
200
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const existingChunks = Array.isArray(metadata._chunks) ? metadata._chunks : [];
|
|
334
|
+
const chunkInfo = {
|
|
335
|
+
checksum: chunkChecksum,
|
|
336
|
+
length: contentLength,
|
|
337
|
+
offset: chunkOffset
|
|
338
|
+
};
|
|
339
|
+
const chunks = trackChunk(existingChunks, chunkInfo);
|
|
340
|
+
const updatedMetadata = {
|
|
341
|
+
...metadata,
|
|
342
|
+
_chunks: chunks
|
|
343
|
+
};
|
|
344
|
+
await this.storage.update({ id }, { metadata: updatedMetadata });
|
|
345
|
+
let updatedFile = await this.storage.write({
|
|
346
|
+
body: bodyStream,
|
|
347
|
+
contentLength,
|
|
348
|
+
id,
|
|
349
|
+
start: chunkOffset
|
|
350
|
+
});
|
|
351
|
+
let isComplete = updatedFile.bytesWritten >= totalSize;
|
|
352
|
+
if (isChunkedUploadFile) {
|
|
353
|
+
const isChunksComplete = isUploadComplete(chunks, totalSize);
|
|
354
|
+
isComplete = isChunksComplete;
|
|
355
|
+
if (updatedFile.status === "completed" && !isComplete) {
|
|
356
|
+
await this.storage.update({ id }, { status: "part" });
|
|
357
|
+
updatedFile = { ...updatedFile, status: "part" };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const finalFile = isComplete && updatedFile.bytesWritten !== totalSize ? { ...updatedFile, bytesWritten: totalSize } : updatedFile;
|
|
361
|
+
const locationUrl = this.buildFileUrl(requestUrl, finalFile);
|
|
362
|
+
const headers = {
|
|
363
|
+
...buildFileHeaders(finalFile, locationUrl),
|
|
364
|
+
...buildChunkedUploadHeaders(finalFile, isComplete),
|
|
365
|
+
"x-upload-offset": String(finalFile.bytesWritten || 0)
|
|
366
|
+
};
|
|
367
|
+
return buildResponseFile(finalFile, headers, isComplete ? 200 : 202);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Handle file metadata retrieval (HEAD).
|
|
371
|
+
* @param id File ID from URL
|
|
372
|
+
* @returns Promise resolving to ResponseFile with metadata headers
|
|
373
|
+
*/
|
|
374
|
+
async handleHead(id) {
|
|
375
|
+
let file = await this.storage.getMeta(id);
|
|
376
|
+
const isChunkedUploadFile = isChunkedUpload(file);
|
|
377
|
+
if (isChunkedUploadFile) {
|
|
378
|
+
const totalSize = getTotalSize(file);
|
|
379
|
+
if (totalSize && file.size !== totalSize) {
|
|
380
|
+
file = { ...file, size: totalSize };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const headers = {
|
|
384
|
+
...buildFileMetadataHeaders(file)
|
|
385
|
+
};
|
|
386
|
+
if (isChunkedUploadFile) {
|
|
387
|
+
const metadata = file.metadata || {};
|
|
388
|
+
const totalSize = getTotalSize(file) || file.size || 0;
|
|
389
|
+
const chunks = Array.isArray(metadata._chunks) ? metadata._chunks : [];
|
|
390
|
+
const isComplete = isUploadComplete(chunks, totalSize);
|
|
391
|
+
Object.assign(headers, buildChunkedUploadHeaders(file, isComplete));
|
|
392
|
+
}
|
|
393
|
+
return buildResponseFile(file, headers, 200);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Handle OPTIONS request with REST API capabilities.
|
|
397
|
+
* @param methods Array of supported HTTP methods
|
|
398
|
+
* @param maxUploadSize Maximum upload size
|
|
399
|
+
* @returns ResponseFile with CORS headers
|
|
400
|
+
*/
|
|
401
|
+
// eslint-disable-next-line class-methods-use-this
|
|
402
|
+
handleOptions(methods, maxUploadSize) {
|
|
403
|
+
const headers = {
|
|
404
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type, Content-Length, Content-Disposition, X-File-Metadata, X-Chunked-Upload, X-Total-Size, X-Chunk-Offset, X-Chunk-Checksum",
|
|
405
|
+
"Access-Control-Allow-Methods": methods.map((method) => method.toUpperCase()).join(", "),
|
|
406
|
+
"Access-Control-Max-Age": 86400,
|
|
407
|
+
"X-Max-Upload-Size": String(maxUploadSize)
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
headers,
|
|
411
|
+
statusCode: 204
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Handle batch file deletion.
|
|
416
|
+
* @param ids Array of file IDs to delete
|
|
417
|
+
* @returns Promise resolving to ResponseList with deletion results
|
|
418
|
+
*/
|
|
419
|
+
async deleteBatch(ids) {
|
|
420
|
+
const result = await this.storage.deleteBatch(ids);
|
|
421
|
+
if (result.successfulCount === 0 && result.failedCount > 0) {
|
|
422
|
+
const failedIds = result.failed.map((errorItem) => errorItem.id).join(", ");
|
|
423
|
+
throw createHttpError(404, `Failed to delete files: ${failedIds}`);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
data: result.successful,
|
|
427
|
+
headers: result.failedCount > 0 ? {
|
|
428
|
+
"X-Delete-Errors": JSON.stringify(result.failed),
|
|
429
|
+
"X-Delete-Failed": String(result.failedCount),
|
|
430
|
+
"X-Delete-Successful": String(result.successfulCount)
|
|
431
|
+
} : {
|
|
432
|
+
"X-Delete-Successful": String(result.successfulCount)
|
|
433
|
+
},
|
|
434
|
+
statusCode: result.successfulCount === ids.length ? 204 : 207
|
|
435
|
+
// 207 Multi-Status for partial success
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Storage instance for file operations.
|
|
440
|
+
*/
|
|
441
|
+
// eslint-disable-next-line class-methods-use-this
|
|
442
|
+
get storage() {
|
|
443
|
+
throw new Error("storage must be implemented");
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Build file URL from request URL and file data.
|
|
447
|
+
* @param _requestUrl Request URL string
|
|
448
|
+
* @param _file File object containing ID and content type
|
|
449
|
+
* @returns Constructed file URL with extension based on content type
|
|
450
|
+
*/
|
|
451
|
+
// eslint-disable-next-line class-methods-use-this
|
|
452
|
+
buildFileUrl(_requestUrl, _file) {
|
|
453
|
+
throw new Error("buildFileUrl must be implemented");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export { RestBase as R, validateContentLength as a, parseChunkHeaders as b, parseContentDispositionValue as c, extractFileInit as e, parseContentDisposition as p, validateRequestBody as v };
|