@uploadista/client-core 0.0.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/.turbo/turbo-build.log +5 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/auth/auth-http-client.d.ts +50 -0
- package/dist/auth/auth-http-client.d.ts.map +1 -0
- package/dist/auth/auth-http-client.js +110 -0
- package/dist/auth/direct-auth.d.ts +38 -0
- package/dist/auth/direct-auth.d.ts.map +1 -0
- package/dist/auth/direct-auth.js +95 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/no-auth.d.ts +26 -0
- package/dist/auth/no-auth.d.ts.map +1 -0
- package/dist/auth/no-auth.js +33 -0
- package/dist/auth/saas-auth.d.ts +80 -0
- package/dist/auth/saas-auth.d.ts.map +1 -0
- package/dist/auth/saas-auth.js +167 -0
- package/dist/auth/types.d.ts +101 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +8 -0
- package/dist/chunk-buffer.d.ts +209 -0
- package/dist/chunk-buffer.d.ts.map +1 -0
- package/dist/chunk-buffer.js +236 -0
- package/dist/client/create-uploadista-client.d.ts +369 -0
- package/dist/client/create-uploadista-client.d.ts.map +1 -0
- package/dist/client/create-uploadista-client.js +518 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/uploadista-api.d.ts +284 -0
- package/dist/client/uploadista-api.d.ts.map +1 -0
- package/dist/client/uploadista-api.js +444 -0
- package/dist/client/uploadista-websocket-manager.d.ts +110 -0
- package/dist/client/uploadista-websocket-manager.d.ts.map +1 -0
- package/dist/client/uploadista-websocket-manager.js +207 -0
- package/dist/error.d.ts +106 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +69 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +59 -0
- package/dist/mock-data-store.d.ts +30 -0
- package/dist/mock-data-store.d.ts.map +1 -0
- package/dist/mock-data-store.js +88 -0
- package/dist/network-monitor.d.ts +262 -0
- package/dist/network-monitor.d.ts.map +1 -0
- package/dist/network-monitor.js +291 -0
- package/dist/services/abort-controller-service.d.ts +19 -0
- package/dist/services/abort-controller-service.d.ts.map +1 -0
- package/dist/services/abort-controller-service.js +4 -0
- package/dist/services/checksum-service.d.ts +4 -0
- package/dist/services/checksum-service.d.ts.map +1 -0
- package/dist/services/checksum-service.js +1 -0
- package/dist/services/file-reader-service.d.ts +38 -0
- package/dist/services/file-reader-service.d.ts.map +1 -0
- package/dist/services/file-reader-service.js +4 -0
- package/dist/services/fingerprint-service.d.ts +4 -0
- package/dist/services/fingerprint-service.d.ts.map +1 -0
- package/dist/services/fingerprint-service.js +1 -0
- package/dist/services/http-client.d.ts +182 -0
- package/dist/services/http-client.d.ts.map +1 -0
- package/dist/services/http-client.js +1 -0
- package/dist/services/id-generation-service.d.ts +10 -0
- package/dist/services/id-generation-service.d.ts.map +1 -0
- package/dist/services/id-generation-service.js +1 -0
- package/dist/services/index.d.ts +11 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/platform-service.d.ts +48 -0
- package/dist/services/platform-service.d.ts.map +1 -0
- package/dist/services/platform-service.js +10 -0
- package/dist/services/service-container.d.ts +25 -0
- package/dist/services/service-container.d.ts.map +1 -0
- package/dist/services/service-container.js +1 -0
- package/dist/services/storage-service.d.ts +26 -0
- package/dist/services/storage-service.d.ts.map +1 -0
- package/dist/services/storage-service.js +1 -0
- package/dist/services/websocket-service.d.ts +36 -0
- package/dist/services/websocket-service.d.ts.map +1 -0
- package/dist/services/websocket-service.js +4 -0
- package/dist/smart-chunker.d.ts +72 -0
- package/dist/smart-chunker.d.ts.map +1 -0
- package/dist/smart-chunker.js +317 -0
- package/dist/storage/client-storage.d.ts +148 -0
- package/dist/storage/client-storage.d.ts.map +1 -0
- package/dist/storage/client-storage.js +62 -0
- package/dist/storage/in-memory-storage-service.d.ts +7 -0
- package/dist/storage/in-memory-storage-service.d.ts.map +1 -0
- package/dist/storage/in-memory-storage-service.js +24 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/types/buffered-chunk.d.ts +6 -0
- package/dist/types/buffered-chunk.d.ts.map +1 -0
- package/dist/types/buffered-chunk.js +1 -0
- package/dist/types/chunk-metrics.d.ts +12 -0
- package/dist/types/chunk-metrics.d.ts.map +1 -0
- package/dist/types/chunk-metrics.js +1 -0
- package/dist/types/flow-result.d.ts +11 -0
- package/dist/types/flow-result.d.ts.map +1 -0
- package/dist/types/flow-result.js +1 -0
- package/dist/types/flow-upload-config.d.ts +54 -0
- package/dist/types/flow-upload-config.d.ts.map +1 -0
- package/dist/types/flow-upload-config.js +1 -0
- package/dist/types/flow-upload-item.d.ts +16 -0
- package/dist/types/flow-upload-item.d.ts.map +1 -0
- package/dist/types/flow-upload-item.js +1 -0
- package/dist/types/flow-upload-options.d.ts +41 -0
- package/dist/types/flow-upload-options.d.ts.map +1 -0
- package/dist/types/flow-upload-options.js +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/multi-flow-upload-options.d.ts +33 -0
- package/dist/types/multi-flow-upload-options.d.ts.map +1 -0
- package/dist/types/multi-flow-upload-options.js +1 -0
- package/dist/types/multi-flow-upload-state.d.ts +9 -0
- package/dist/types/multi-flow-upload-state.d.ts.map +1 -0
- package/dist/types/multi-flow-upload-state.js +1 -0
- package/dist/types/performance-insights.d.ts +11 -0
- package/dist/types/performance-insights.d.ts.map +1 -0
- package/dist/types/performance-insights.js +1 -0
- package/dist/types/previous-upload.d.ts +20 -0
- package/dist/types/previous-upload.d.ts.map +1 -0
- package/dist/types/previous-upload.js +9 -0
- package/dist/types/upload-options.d.ts +40 -0
- package/dist/types/upload-options.d.ts.map +1 -0
- package/dist/types/upload-options.js +1 -0
- package/dist/types/upload-response.d.ts +6 -0
- package/dist/types/upload-response.d.ts.map +1 -0
- package/dist/types/upload-response.js +1 -0
- package/dist/types/upload-result.d.ts +57 -0
- package/dist/types/upload-result.d.ts.map +1 -0
- package/dist/types/upload-result.js +1 -0
- package/dist/types/upload-session-metrics.d.ts +16 -0
- package/dist/types/upload-session-metrics.d.ts.map +1 -0
- package/dist/types/upload-session-metrics.js +1 -0
- package/dist/upload/chunk-upload.d.ts +40 -0
- package/dist/upload/chunk-upload.d.ts.map +1 -0
- package/dist/upload/chunk-upload.js +82 -0
- package/dist/upload/flow-upload.d.ts +48 -0
- package/dist/upload/flow-upload.d.ts.map +1 -0
- package/dist/upload/flow-upload.js +240 -0
- package/dist/upload/index.d.ts +3 -0
- package/dist/upload/index.d.ts.map +1 -0
- package/dist/upload/index.js +2 -0
- package/dist/upload/parallel-upload.d.ts +65 -0
- package/dist/upload/parallel-upload.d.ts.map +1 -0
- package/dist/upload/parallel-upload.js +231 -0
- package/dist/upload/single-upload.d.ts +118 -0
- package/dist/upload/single-upload.d.ts.map +1 -0
- package/dist/upload/single-upload.js +332 -0
- package/dist/upload/upload-manager.d.ts +30 -0
- package/dist/upload/upload-manager.d.ts.map +1 -0
- package/dist/upload/upload-manager.js +57 -0
- package/dist/upload/upload-metrics.d.ts +37 -0
- package/dist/upload/upload-metrics.d.ts.map +1 -0
- package/dist/upload/upload-metrics.js +236 -0
- package/dist/upload/upload-storage.d.ts +32 -0
- package/dist/upload/upload-storage.d.ts.map +1 -0
- package/dist/upload/upload-storage.js +46 -0
- package/dist/upload/upload-strategy.d.ts +66 -0
- package/dist/upload/upload-strategy.d.ts.map +1 -0
- package/dist/upload/upload-strategy.js +171 -0
- package/dist/upload/upload-utils.d.ts +26 -0
- package/dist/upload/upload-utils.d.ts.map +1 -0
- package/dist/upload/upload-utils.js +80 -0
- package/package.json +29 -0
- package/src/__tests__/smart-chunking.test.ts +399 -0
- package/src/auth/__tests__/auth-http-client.test.ts +327 -0
- package/src/auth/__tests__/direct-auth.test.ts +135 -0
- package/src/auth/__tests__/no-auth.test.ts +40 -0
- package/src/auth/__tests__/saas-auth.test.ts +337 -0
- package/src/auth/auth-http-client.ts +150 -0
- package/src/auth/direct-auth.ts +121 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/no-auth.ts +39 -0
- package/src/auth/saas-auth.ts +218 -0
- package/src/auth/types.ts +105 -0
- package/src/chunk-buffer.ts +287 -0
- package/src/client/create-uploadista-client.ts +901 -0
- package/src/client/index.ts +3 -0
- package/src/client/uploadista-api.ts +857 -0
- package/src/client/uploadista-websocket-manager.ts +275 -0
- package/src/error.ts +149 -0
- package/src/index.ts +13 -0
- package/src/logger.ts +104 -0
- package/src/mock-data-store.ts +97 -0
- package/src/network-monitor.ts +445 -0
- package/src/services/abort-controller-service.ts +21 -0
- package/src/services/checksum-service.ts +3 -0
- package/src/services/file-reader-service.ts +44 -0
- package/src/services/fingerprint-service.ts +6 -0
- package/src/services/http-client.ts +229 -0
- package/src/services/id-generation-service.ts +9 -0
- package/src/services/index.ts +10 -0
- package/src/services/platform-service.ts +65 -0
- package/src/services/service-container.ts +24 -0
- package/src/services/storage-service.ts +29 -0
- package/src/services/websocket-service.ts +33 -0
- package/src/smart-chunker.ts +451 -0
- package/src/storage/client-storage.ts +186 -0
- package/src/storage/in-memory-storage-service.ts +33 -0
- package/src/storage/index.ts +2 -0
- package/src/types/buffered-chunk.ts +5 -0
- package/src/types/chunk-metrics.ts +11 -0
- package/src/types/flow-result.ts +14 -0
- package/src/types/flow-upload-config.ts +56 -0
- package/src/types/flow-upload-item.ts +16 -0
- package/src/types/flow-upload-options.ts +56 -0
- package/src/types/index.ts +13 -0
- package/src/types/multi-flow-upload-options.ts +39 -0
- package/src/types/multi-flow-upload-state.ts +9 -0
- package/src/types/performance-insights.ts +7 -0
- package/src/types/previous-upload.ts +22 -0
- package/src/types/upload-options.ts +56 -0
- package/src/types/upload-response.ts +6 -0
- package/src/types/upload-result.ts +60 -0
- package/src/types/upload-session-metrics.ts +15 -0
- package/src/upload/chunk-upload.ts +151 -0
- package/src/upload/flow-upload.ts +367 -0
- package/src/upload/index.ts +2 -0
- package/src/upload/parallel-upload.ts +387 -0
- package/src/upload/single-upload.ts +554 -0
- package/src/upload/upload-manager.ts +106 -0
- package/src/upload/upload-metrics.ts +340 -0
- package/src/upload/upload-storage.ts +87 -0
- package/src/upload/upload-strategy.ts +296 -0
- package/src/upload/upload-utils.ts +114 -0
- package/tsconfig.json +23 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import type { InputFile, UploadFile } from "@uploadista/core/types";
|
|
2
|
+
import type { UploadistaApi } from "../client/uploadista-api";
|
|
3
|
+
import { UploadistaError } from "../error";
|
|
4
|
+
import type { Logger } from "../logger";
|
|
5
|
+
import type { AbortControllerLike } from "../services/abort-controller-service";
|
|
6
|
+
import type { ChecksumService } from "../services/checksum-service";
|
|
7
|
+
import type { FileSource } from "../services/file-reader-service";
|
|
8
|
+
import type { IdGenerationService } from "../services/id-generation-service";
|
|
9
|
+
import type { PlatformService, Timeout } from "../services/platform-service";
|
|
10
|
+
import type { WebSocketLike } from "../services/websocket-service";
|
|
11
|
+
import type { SmartChunker, SmartChunkerConfig } from "../smart-chunker";
|
|
12
|
+
import type { ClientStorage } from "../storage/client-storage";
|
|
13
|
+
import {
|
|
14
|
+
type OnProgress,
|
|
15
|
+
type OnShouldRetry,
|
|
16
|
+
shouldRetry,
|
|
17
|
+
uploadChunk,
|
|
18
|
+
} from "./chunk-upload";
|
|
19
|
+
import type { UploadMetrics } from "./upload-metrics";
|
|
20
|
+
import {
|
|
21
|
+
removeFromClientStorage,
|
|
22
|
+
saveUploadInClientStorage,
|
|
23
|
+
} from "./upload-storage";
|
|
24
|
+
import { encodeMetadata, inStatusCategory } from "./upload-utils";
|
|
25
|
+
|
|
26
|
+
export type Callbacks = {
|
|
27
|
+
onProgress?: OnProgress;
|
|
28
|
+
onChunkComplete?: (
|
|
29
|
+
chunkSize: number,
|
|
30
|
+
bytesAccepted: number,
|
|
31
|
+
bytesTotal: number | null,
|
|
32
|
+
) => void;
|
|
33
|
+
onSuccess?: (payload: UploadFile) => void;
|
|
34
|
+
onError?: (error: Error | UploadistaError) => void;
|
|
35
|
+
onStart?: (file: { uploadId: string; size: number | null }) => void;
|
|
36
|
+
onJobStart?: (jobId: string) => void;
|
|
37
|
+
onShouldRetry?: OnShouldRetry;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type SingleUploadResult = {
|
|
41
|
+
uploadIdStorageKey: string | undefined;
|
|
42
|
+
uploadId: string;
|
|
43
|
+
offset: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Start uploading the file using PATCH requests. The file will be divided
|
|
48
|
+
* into chunks as specified in the chunkSize option. During the upload
|
|
49
|
+
* the onProgress event handler may be invoked multiple times.
|
|
50
|
+
*/
|
|
51
|
+
export async function performUpload({
|
|
52
|
+
uploadId,
|
|
53
|
+
offset,
|
|
54
|
+
source,
|
|
55
|
+
uploadLengthDeferred,
|
|
56
|
+
retryAttempt = 0,
|
|
57
|
+
abortController,
|
|
58
|
+
retryDelays,
|
|
59
|
+
smartChunker,
|
|
60
|
+
uploadistaApi,
|
|
61
|
+
logger,
|
|
62
|
+
smartChunking,
|
|
63
|
+
metrics,
|
|
64
|
+
platformService,
|
|
65
|
+
onRetry,
|
|
66
|
+
...callbacks
|
|
67
|
+
}: {
|
|
68
|
+
uploadId: string;
|
|
69
|
+
offset: number;
|
|
70
|
+
retryAttempt?: number;
|
|
71
|
+
source: FileSource;
|
|
72
|
+
abortController: AbortControllerLike;
|
|
73
|
+
uploadLengthDeferred: boolean | undefined;
|
|
74
|
+
retryDelays: number[] | undefined;
|
|
75
|
+
smartChunker: SmartChunker;
|
|
76
|
+
uploadistaApi: UploadistaApi;
|
|
77
|
+
logger: Logger;
|
|
78
|
+
smartChunking?: SmartChunkerConfig;
|
|
79
|
+
metrics: UploadMetrics;
|
|
80
|
+
platformService: PlatformService;
|
|
81
|
+
onRetry?: (timeout: Timeout) => void;
|
|
82
|
+
} & Callbacks): Promise<void> {
|
|
83
|
+
let offsetBeforeRetry = offset;
|
|
84
|
+
let currentOffset = offset;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const res = await uploadChunk({
|
|
88
|
+
uploadId,
|
|
89
|
+
source,
|
|
90
|
+
offset,
|
|
91
|
+
uploadLengthDeferred,
|
|
92
|
+
onProgress: callbacks.onProgress,
|
|
93
|
+
abortController,
|
|
94
|
+
smartChunker,
|
|
95
|
+
uploadistaApi,
|
|
96
|
+
logger,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!inStatusCategory(res.status, 200) || res.upload == null) {
|
|
100
|
+
throw new UploadistaError({
|
|
101
|
+
name: "NETWORK_UNEXPECTED_RESPONSE",
|
|
102
|
+
message: "Unexpected response while uploading chunk",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
currentOffset = res.upload.offset;
|
|
107
|
+
|
|
108
|
+
callbacks.onProgress?.(uploadId, currentOffset, res.upload.size ?? 0);
|
|
109
|
+
callbacks.onChunkComplete?.(
|
|
110
|
+
currentOffset - offset,
|
|
111
|
+
offset,
|
|
112
|
+
res.upload?.size ?? 0,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Record detailed chunk metrics
|
|
116
|
+
if (smartChunking?.enabled !== false) {
|
|
117
|
+
const chunkIndex = Math.floor(offset / (currentOffset - offset || 1));
|
|
118
|
+
const chunkSize = currentOffset - offset;
|
|
119
|
+
const chunkDuration = Date.now() - (Date.now() - 100); // Approximate, real timing is in uploadChunk
|
|
120
|
+
const lastDecision = smartChunker.getLastDecision();
|
|
121
|
+
|
|
122
|
+
metrics.recordChunk({
|
|
123
|
+
chunkIndex,
|
|
124
|
+
size: chunkSize,
|
|
125
|
+
duration: chunkDuration,
|
|
126
|
+
speed: chunkSize / (chunkDuration / 1000),
|
|
127
|
+
success: true,
|
|
128
|
+
retryCount: retryAttempt,
|
|
129
|
+
networkCondition: lastDecision?.networkCondition?.type,
|
|
130
|
+
chunkingStrategy: lastDecision?.strategy,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Update smart chunker with connection metrics for pooling optimization
|
|
134
|
+
const connectionMetrics = uploadistaApi.getConnectionMetrics();
|
|
135
|
+
smartChunker.updateConnectionMetrics(connectionMetrics);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (currentOffset >= (source.size ?? 0)) {
|
|
139
|
+
if (source) source.close();
|
|
140
|
+
|
|
141
|
+
// Complete metrics session
|
|
142
|
+
if (smartChunking?.enabled !== false) {
|
|
143
|
+
const sessionMetrics = metrics.endSession();
|
|
144
|
+
if (sessionMetrics) {
|
|
145
|
+
logger.log(
|
|
146
|
+
`Upload completed: ${sessionMetrics.totalSize} bytes in ${sessionMetrics.totalDuration}ms, avg speed: ${Math.round(sessionMetrics.averageSpeed / 1024)}KB/s`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
callbacks.onSuccess?.(res.upload);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await performUpload({
|
|
156
|
+
uploadId,
|
|
157
|
+
offset: currentOffset,
|
|
158
|
+
source,
|
|
159
|
+
uploadLengthDeferred,
|
|
160
|
+
retryDelays,
|
|
161
|
+
smartChunker,
|
|
162
|
+
platformService,
|
|
163
|
+
uploadistaApi,
|
|
164
|
+
logger,
|
|
165
|
+
smartChunking,
|
|
166
|
+
metrics,
|
|
167
|
+
onRetry,
|
|
168
|
+
abortController,
|
|
169
|
+
...callbacks,
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
// Check if we should retry, when enabled, before sending the error to the user.
|
|
173
|
+
if (retryDelays != null) {
|
|
174
|
+
// We will reset the attempt counter if
|
|
175
|
+
// - we were already able to connect to the server (offset != null) and
|
|
176
|
+
// - we were able to upload a small chunk of data to the server
|
|
177
|
+
const shouldResetDelays =
|
|
178
|
+
offset != null && currentOffset > offsetBeforeRetry;
|
|
179
|
+
if (shouldResetDelays) {
|
|
180
|
+
retryAttempt = 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const castedErr = !(err instanceof UploadistaError)
|
|
184
|
+
? new UploadistaError({
|
|
185
|
+
name: "NETWORK_ERROR",
|
|
186
|
+
message: "Network error",
|
|
187
|
+
cause: err as Error,
|
|
188
|
+
})
|
|
189
|
+
: err;
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
shouldRetry(
|
|
193
|
+
platformService,
|
|
194
|
+
castedErr,
|
|
195
|
+
retryAttempt,
|
|
196
|
+
retryDelays,
|
|
197
|
+
callbacks.onShouldRetry,
|
|
198
|
+
)
|
|
199
|
+
) {
|
|
200
|
+
const delay = retryDelays[retryAttempt];
|
|
201
|
+
|
|
202
|
+
offsetBeforeRetry = offset;
|
|
203
|
+
|
|
204
|
+
const timeout = platformService.setTimeout(async () => {
|
|
205
|
+
await performUpload({
|
|
206
|
+
uploadId,
|
|
207
|
+
offset,
|
|
208
|
+
source,
|
|
209
|
+
retryAttempt: retryAttempt + 1,
|
|
210
|
+
uploadLengthDeferred,
|
|
211
|
+
retryDelays,
|
|
212
|
+
smartChunker,
|
|
213
|
+
platformService,
|
|
214
|
+
uploadistaApi,
|
|
215
|
+
logger,
|
|
216
|
+
smartChunking,
|
|
217
|
+
metrics,
|
|
218
|
+
onRetry,
|
|
219
|
+
abortController,
|
|
220
|
+
...callbacks,
|
|
221
|
+
});
|
|
222
|
+
}, delay);
|
|
223
|
+
onRetry?.(timeout);
|
|
224
|
+
} else {
|
|
225
|
+
throw new UploadistaError({
|
|
226
|
+
name: "UPLOAD_CHUNK_FAILED",
|
|
227
|
+
message: `failed to upload chunk for ${uploadId} at offset ${offset}`,
|
|
228
|
+
cause: err as Error,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a new upload using the creation extension by sending a POST
|
|
237
|
+
* request to the endpoint. After successful creation the file will be
|
|
238
|
+
* uploaded
|
|
239
|
+
*/
|
|
240
|
+
export async function createUpload({
|
|
241
|
+
fingerprint,
|
|
242
|
+
storageId,
|
|
243
|
+
source,
|
|
244
|
+
uploadLengthDeferred,
|
|
245
|
+
metadata,
|
|
246
|
+
uploadistaApi,
|
|
247
|
+
logger,
|
|
248
|
+
checksumService,
|
|
249
|
+
clientStorage,
|
|
250
|
+
generateId,
|
|
251
|
+
storeFingerprintForResuming,
|
|
252
|
+
openWebSocket,
|
|
253
|
+
closeWebSocket,
|
|
254
|
+
computeChecksum = true,
|
|
255
|
+
checksumAlgorithm = "sha256",
|
|
256
|
+
platformService,
|
|
257
|
+
...callbacks
|
|
258
|
+
}: {
|
|
259
|
+
fingerprint: string;
|
|
260
|
+
storageId: string;
|
|
261
|
+
source: FileSource;
|
|
262
|
+
uploadLengthDeferred: boolean | undefined;
|
|
263
|
+
metadata: Record<string, string>;
|
|
264
|
+
uploadistaApi: UploadistaApi;
|
|
265
|
+
logger: Logger;
|
|
266
|
+
clientStorage: ClientStorage;
|
|
267
|
+
generateId: IdGenerationService;
|
|
268
|
+
storeFingerprintForResuming: boolean;
|
|
269
|
+
openWebSocket: (uploadId: string) => WebSocketLike;
|
|
270
|
+
closeWebSocket: (uploadId: string) => void;
|
|
271
|
+
checksumService: ChecksumService;
|
|
272
|
+
computeChecksum?: boolean;
|
|
273
|
+
checksumAlgorithm?: string;
|
|
274
|
+
platformService: PlatformService;
|
|
275
|
+
} & Callbacks): Promise<SingleUploadResult | undefined> {
|
|
276
|
+
if (!uploadLengthDeferred && source.size == null) {
|
|
277
|
+
const error = new UploadistaError({
|
|
278
|
+
name: "UPLOAD_SIZE_NOT_SPECIFIED",
|
|
279
|
+
message: "expected size to be set",
|
|
280
|
+
});
|
|
281
|
+
callbacks.onError?.(error);
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Compute checksum if enabled and file is a File object
|
|
286
|
+
let checksum: string | undefined;
|
|
287
|
+
if (computeChecksum && platformService.isFileLike(source.input)) {
|
|
288
|
+
try {
|
|
289
|
+
logger.log("Computing file checksum...");
|
|
290
|
+
checksum = await checksumService.computeChecksum(
|
|
291
|
+
new Uint8Array(source.input as any),
|
|
292
|
+
);
|
|
293
|
+
logger.log(`Checksum computed: ${checksum}`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
logger.log(
|
|
296
|
+
`Warning: Failed to compute checksum: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
297
|
+
);
|
|
298
|
+
// Continue without checksum if computation fails
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const createUploadData: InputFile = {
|
|
303
|
+
uploadLengthDeferred,
|
|
304
|
+
storageId,
|
|
305
|
+
size: source.size ?? 0,
|
|
306
|
+
metadata: metadata ? encodeMetadata(metadata) : undefined,
|
|
307
|
+
fileName: source.name ?? undefined,
|
|
308
|
+
type: source.type ?? "",
|
|
309
|
+
lastModified: source.lastModified ?? undefined,
|
|
310
|
+
checksum,
|
|
311
|
+
checksumAlgorithm: checksum ? checksumAlgorithm : undefined,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const { upload, status } = await uploadistaApi.createUpload(createUploadData);
|
|
315
|
+
|
|
316
|
+
if (!inStatusCategory(status, 200) || upload == null) {
|
|
317
|
+
const error = new UploadistaError({
|
|
318
|
+
name: "NETWORK_UNEXPECTED_RESPONSE",
|
|
319
|
+
message: "Unexpected response while creating upload",
|
|
320
|
+
});
|
|
321
|
+
callbacks.onError?.(error);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
logger.log(`Created upload ${upload.id}`);
|
|
326
|
+
|
|
327
|
+
openWebSocket(upload.id);
|
|
328
|
+
|
|
329
|
+
if (upload.size === 0) {
|
|
330
|
+
// Nothing to upload and file was successfully created
|
|
331
|
+
callbacks.onSuccess?.(upload);
|
|
332
|
+
if (source) source.close();
|
|
333
|
+
closeWebSocket(upload.id);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const uploadIdStorageKey = await saveUploadInClientStorage({
|
|
338
|
+
clientStorage,
|
|
339
|
+
fingerprint,
|
|
340
|
+
size: upload.size ?? 0,
|
|
341
|
+
metadata: upload.metadata ?? {},
|
|
342
|
+
clientStorageKey: null,
|
|
343
|
+
storeFingerprintForResuming,
|
|
344
|
+
generateId,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
callbacks.onStart?.({
|
|
348
|
+
uploadId: upload.id,
|
|
349
|
+
size: upload.size ?? null,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
uploadIdStorageKey,
|
|
354
|
+
uploadId: upload.id,
|
|
355
|
+
offset: upload.offset,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Try to resume an existing upload. First a HEAD request will be sent
|
|
361
|
+
* to retrieve the offset. If the request fails a new upload will be
|
|
362
|
+
* created. In the case of a successful response the file will be uploaded.
|
|
363
|
+
*/
|
|
364
|
+
export async function resumeUpload({
|
|
365
|
+
uploadId,
|
|
366
|
+
storageId,
|
|
367
|
+
uploadIdStorageKey,
|
|
368
|
+
fingerprint,
|
|
369
|
+
source,
|
|
370
|
+
uploadLengthDeferred,
|
|
371
|
+
uploadistaApi,
|
|
372
|
+
logger,
|
|
373
|
+
platformService,
|
|
374
|
+
checksumService,
|
|
375
|
+
clientStorage,
|
|
376
|
+
generateId,
|
|
377
|
+
storeFingerprintForResuming,
|
|
378
|
+
openWebSocket,
|
|
379
|
+
...callbacks
|
|
380
|
+
}: {
|
|
381
|
+
uploadId: string;
|
|
382
|
+
storageId: string;
|
|
383
|
+
uploadIdStorageKey: string;
|
|
384
|
+
fingerprint: string;
|
|
385
|
+
platformService: PlatformService;
|
|
386
|
+
source: FileSource;
|
|
387
|
+
uploadLengthDeferred: boolean | undefined;
|
|
388
|
+
uploadistaApi: UploadistaApi;
|
|
389
|
+
checksumService: ChecksumService;
|
|
390
|
+
logger: Logger;
|
|
391
|
+
clientStorage: ClientStorage;
|
|
392
|
+
generateId: IdGenerationService;
|
|
393
|
+
storeFingerprintForResuming: boolean;
|
|
394
|
+
openWebSocket: (uploadId: string) => WebSocketLike;
|
|
395
|
+
} & Callbacks): Promise<SingleUploadResult | undefined> {
|
|
396
|
+
const res = await uploadistaApi.getUpload(uploadId);
|
|
397
|
+
const status = res.status;
|
|
398
|
+
|
|
399
|
+
if (!inStatusCategory(status, 200)) {
|
|
400
|
+
// If the upload is locked (indicated by the 423 Locked status code), we
|
|
401
|
+
// emit an error instead of directly starting a new upload. This way the
|
|
402
|
+
// retry logic can catch the error and will retry the upload. An upload
|
|
403
|
+
// is usually locked for a short period of time and will be available
|
|
404
|
+
// afterwards.
|
|
405
|
+
if (status === 423) {
|
|
406
|
+
const error = new UploadistaError({
|
|
407
|
+
name: "UPLOAD_LOCKED",
|
|
408
|
+
message: "upload is currently locked; retry later",
|
|
409
|
+
});
|
|
410
|
+
callbacks.onError?.(error);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (inStatusCategory(status, 400)) {
|
|
415
|
+
// Remove stored fingerprint and corresponding endpoint,
|
|
416
|
+
// on client errors since the file can not be found
|
|
417
|
+
await removeFromClientStorage(clientStorage, uploadIdStorageKey);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Try to create a new upload
|
|
421
|
+
return await createUpload({
|
|
422
|
+
platformService,
|
|
423
|
+
fingerprint,
|
|
424
|
+
storageId,
|
|
425
|
+
source,
|
|
426
|
+
uploadLengthDeferred,
|
|
427
|
+
metadata: {},
|
|
428
|
+
uploadistaApi,
|
|
429
|
+
logger,
|
|
430
|
+
checksumService,
|
|
431
|
+
clientStorage,
|
|
432
|
+
generateId,
|
|
433
|
+
storeFingerprintForResuming,
|
|
434
|
+
openWebSocket,
|
|
435
|
+
closeWebSocket: () => {}, // Placeholder, will be provided by caller
|
|
436
|
+
...callbacks,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const upload = res.upload;
|
|
441
|
+
if (upload == null) {
|
|
442
|
+
const error = new UploadistaError({
|
|
443
|
+
name: "NETWORK_UNEXPECTED_RESPONSE",
|
|
444
|
+
message: "Unexpected response while resuming upload",
|
|
445
|
+
});
|
|
446
|
+
callbacks.onError?.(error);
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await saveUploadInClientStorage({
|
|
451
|
+
clientStorage,
|
|
452
|
+
fingerprint,
|
|
453
|
+
size: upload.size ?? 0,
|
|
454
|
+
metadata: upload.metadata ?? {},
|
|
455
|
+
clientStorageKey: uploadIdStorageKey,
|
|
456
|
+
storeFingerprintForResuming,
|
|
457
|
+
generateId,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Upload has already been completed and we do not need to send additional
|
|
461
|
+
// data to the server
|
|
462
|
+
if (upload.offset === upload.size) {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
openWebSocket(upload.id);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
uploadId,
|
|
470
|
+
uploadIdStorageKey,
|
|
471
|
+
offset: upload.offset,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Initiate the uploading procedure for a non-parallel upload. Here the entire file is
|
|
477
|
+
* uploaded in a sequential matter.
|
|
478
|
+
*/
|
|
479
|
+
export async function startSingleUpload({
|
|
480
|
+
source,
|
|
481
|
+
uploadId,
|
|
482
|
+
uploadIdStorageKey,
|
|
483
|
+
storageId,
|
|
484
|
+
fingerprint,
|
|
485
|
+
platformService,
|
|
486
|
+
uploadLengthDeferred,
|
|
487
|
+
uploadistaApi,
|
|
488
|
+
checksumService,
|
|
489
|
+
logger,
|
|
490
|
+
clientStorage,
|
|
491
|
+
generateId,
|
|
492
|
+
storeFingerprintForResuming,
|
|
493
|
+
openWebSocket,
|
|
494
|
+
closeWebSocket,
|
|
495
|
+
...callbacks
|
|
496
|
+
}: {
|
|
497
|
+
source: FileSource;
|
|
498
|
+
uploadId: string | null;
|
|
499
|
+
uploadIdStorageKey: string | null;
|
|
500
|
+
storageId: string;
|
|
501
|
+
fingerprint: string;
|
|
502
|
+
platformService: PlatformService;
|
|
503
|
+
uploadLengthDeferred: boolean | undefined;
|
|
504
|
+
uploadistaApi: UploadistaApi;
|
|
505
|
+
checksumService: ChecksumService;
|
|
506
|
+
logger: Logger;
|
|
507
|
+
clientStorage: ClientStorage;
|
|
508
|
+
generateId: IdGenerationService;
|
|
509
|
+
storeFingerprintForResuming: boolean;
|
|
510
|
+
openWebSocket: (uploadId: string) => WebSocketLike;
|
|
511
|
+
closeWebSocket: (uploadId: string) => void;
|
|
512
|
+
} & Callbacks): Promise<SingleUploadResult | undefined> {
|
|
513
|
+
// The upload had been started previously and we should reuse this URL.
|
|
514
|
+
if (uploadId != null && uploadIdStorageKey != null) {
|
|
515
|
+
logger.log(`Resuming upload from previous id: ${uploadId}`);
|
|
516
|
+
return await resumeUpload({
|
|
517
|
+
uploadId,
|
|
518
|
+
uploadIdStorageKey,
|
|
519
|
+
storageId,
|
|
520
|
+
fingerprint,
|
|
521
|
+
source,
|
|
522
|
+
checksumService,
|
|
523
|
+
uploadLengthDeferred,
|
|
524
|
+
uploadistaApi,
|
|
525
|
+
logger,
|
|
526
|
+
platformService,
|
|
527
|
+
clientStorage,
|
|
528
|
+
generateId,
|
|
529
|
+
storeFingerprintForResuming,
|
|
530
|
+
openWebSocket,
|
|
531
|
+
...callbacks,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// An upload has not started for the file yet, so we start a new one
|
|
536
|
+
logger.log("Creating a new upload");
|
|
537
|
+
return await createUpload({
|
|
538
|
+
fingerprint,
|
|
539
|
+
storageId,
|
|
540
|
+
source,
|
|
541
|
+
uploadLengthDeferred,
|
|
542
|
+
metadata: {},
|
|
543
|
+
uploadistaApi,
|
|
544
|
+
logger,
|
|
545
|
+
checksumService,
|
|
546
|
+
platformService,
|
|
547
|
+
clientStorage,
|
|
548
|
+
generateId,
|
|
549
|
+
storeFingerprintForResuming,
|
|
550
|
+
openWebSocket,
|
|
551
|
+
closeWebSocket,
|
|
552
|
+
...callbacks,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { UploadistaApi } from "../client/uploadista-api";
|
|
2
|
+
import { UploadistaError } from "../error";
|
|
3
|
+
import type { AbortControllerLike } from "../services/abort-controller-service";
|
|
4
|
+
import {
|
|
5
|
+
type PlatformService,
|
|
6
|
+
type Timeout,
|
|
7
|
+
wait,
|
|
8
|
+
} from "../services/platform-service";
|
|
9
|
+
import type { ClientStorage } from "../storage/client-storage";
|
|
10
|
+
import { shouldRetry } from "./chunk-upload";
|
|
11
|
+
import { removeFromClientStorage } from "./upload-storage";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Use the Termination extension to delete an upload from the server by sending a DELETE
|
|
15
|
+
* request to the specified upload URL. This is only possible if the server supports the
|
|
16
|
+
* Termination extension. If the `retryDelays` property is set, the method will
|
|
17
|
+
* also retry if an error occurs.
|
|
18
|
+
*/
|
|
19
|
+
export async function terminate(
|
|
20
|
+
uploadId: string,
|
|
21
|
+
uploadistaApi: UploadistaApi,
|
|
22
|
+
platformService: PlatformService,
|
|
23
|
+
retryDelays: number[] | undefined,
|
|
24
|
+
retryAttempt = 0,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
const res = await uploadistaApi.deleteUpload(uploadId);
|
|
28
|
+
// A 204 response indicates a successful request
|
|
29
|
+
if (res.status === 204) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new UploadistaError({
|
|
34
|
+
name: "NETWORK_UNEXPECTED_RESPONSE",
|
|
35
|
+
message: "Unexpected response while terminating upload",
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const error = err as UploadistaError;
|
|
39
|
+
|
|
40
|
+
if (!shouldRetry(platformService, error, retryAttempt, retryDelays)) {
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Instead of keeping track of the retry attempts, we remove the first element from the delays
|
|
45
|
+
// array. If the array is empty, all retry attempts are used up and we will bubble up the error.
|
|
46
|
+
// We recursively call the terminate function will removing elements from the retryDelays array.
|
|
47
|
+
const delay = retryDelays?.[retryAttempt] ?? 0;
|
|
48
|
+
|
|
49
|
+
await wait(platformService, delay);
|
|
50
|
+
|
|
51
|
+
return await terminate(
|
|
52
|
+
uploadId,
|
|
53
|
+
uploadistaApi,
|
|
54
|
+
platformService,
|
|
55
|
+
retryDelays,
|
|
56
|
+
retryAttempt + 1,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Abort any running request and stop the current upload. After abort is called, no event
|
|
63
|
+
* handler will be invoked anymore. You can use the `start` method to resume the upload
|
|
64
|
+
* again.
|
|
65
|
+
* If `shouldTerminate` is true, the `terminate` function will be called to remove the
|
|
66
|
+
* current upload from the server.
|
|
67
|
+
*/
|
|
68
|
+
export async function abort({
|
|
69
|
+
uploadId,
|
|
70
|
+
uploadIdStorageKey,
|
|
71
|
+
retryTimeout,
|
|
72
|
+
shouldTerminate,
|
|
73
|
+
abortController,
|
|
74
|
+
uploadistaApi,
|
|
75
|
+
platformService,
|
|
76
|
+
retryDelays,
|
|
77
|
+
clientStorage,
|
|
78
|
+
}: {
|
|
79
|
+
uploadId: string;
|
|
80
|
+
uploadIdStorageKey: string | undefined;
|
|
81
|
+
retryTimeout: Timeout | null;
|
|
82
|
+
shouldTerminate: boolean;
|
|
83
|
+
abortController: AbortControllerLike;
|
|
84
|
+
uploadistaApi: UploadistaApi;
|
|
85
|
+
platformService: PlatformService;
|
|
86
|
+
retryDelays?: number[];
|
|
87
|
+
clientStorage: ClientStorage;
|
|
88
|
+
}): Promise<void> {
|
|
89
|
+
// Stop any current running request.
|
|
90
|
+
abortController.abort();
|
|
91
|
+
|
|
92
|
+
// Stop any timeout used for initiating a retry.
|
|
93
|
+
if (retryTimeout != null) {
|
|
94
|
+
platformService.clearTimeout(retryTimeout);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!shouldTerminate || uploadId == null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await terminate(uploadId, uploadistaApi, platformService, retryDelays);
|
|
102
|
+
|
|
103
|
+
if (uploadIdStorageKey != null) {
|
|
104
|
+
return removeFromClientStorage(clientStorage, uploadIdStorageKey);
|
|
105
|
+
}
|
|
106
|
+
}
|