@devrev/airsync-sdk 2.0.0 → 2.1.0-beta.0
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 +1 -1
- package/dist/common/constants.d.ts +1 -0
- package/dist/common/constants.d.ts.map +1 -1
- package/dist/common/constants.js +4 -1
- package/dist/multithreading/adapters/extraction-adapter.d.ts.map +1 -1
- package/dist/multithreading/adapters/extraction-adapter.js +60 -34
- package/dist/uploader/uploader.d.ts +21 -1
- package/dist/uploader/uploader.d.ts.map +1 -1
- package/dist/uploader/uploader.interfaces.d.ts +6 -0
- package/dist/uploader/uploader.interfaces.d.ts.map +1 -1
- package/dist/uploader/uploader.js +197 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AirSync SDK
|
|
2
2
|
|
|
3
|
-
[](https://coveralls.io/github/devrev/airsync-sdk?branch=main)
|
|
4
4
|
|
|
5
5
|
TypeScript SDK for building AirSync snap-ins (connectors) that sync data between external systems and DevRev. It handles the AirSync control protocol for you: worker threads, timeouts, state, artifact uploads, attachment streaming and loading.
|
|
6
6
|
|
|
@@ -4,6 +4,7 @@ export declare const STATELESS_LOADING_EVENT_TYPES: EventType[];
|
|
|
4
4
|
export declare const STATELESS_EVENT_TYPES: EventType[];
|
|
5
5
|
export declare const ARTIFACT_BATCH_SIZE = 2000;
|
|
6
6
|
export declare const MAX_DEVREV_ARTIFACT_SIZE: number;
|
|
7
|
+
export declare const S3_MULTIPART_CHUNK_SIZE: number;
|
|
7
8
|
export declare const MAX_DEVREV_FILENAME_LENGTH = 256;
|
|
8
9
|
export declare const MAX_DEVREV_FILENAME_EXTENSION_LENGTH = 20;
|
|
9
10
|
export declare const MAX_EVENT_SIZE_BYTES = 200000;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/common/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIhD,eAAO,MAAM,gCAAgC,aAI5C,CAAC;AAEF,eAAO,MAAM,6BAA6B,aAGzC,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAGjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,wBAAwB,QAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/common/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIhD,eAAO,MAAM,gCAAgC,aAI5C,CAAC;AAEF,eAAO,MAAM,6BAA6B,aAGzC,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAGjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,wBAAwB,QAAyB,CAAC;AAG/D,eAAO,MAAM,uBAAuB,QAAmB,CAAC;AACxD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAC9C,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAGvD,eAAO,MAAM,oBAAoB,SAAU,CAAC;AAE5C,eAAO,MAAM,0BAA0B,QAEtC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,MAA0B,CAAC;AACzD,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,oBAAY,uBAAuB;IACjC,wBAAwB,6BAA6B;IACrD,WAAW,gBAAgB;IAC3B,mBAAmB,wBAAwB;CAC5C;AAED,eAAO,MAAM,eAAe,KAAsB,CAAC;AAEnD,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AACrD,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAY,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,6BAA6B,CAAC"}
|
package/dist/common/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UNBOUNDED_DATE_TIME_VALUE = exports.MEMORY_LOG_INTERVAL = exports.HARD_TIMEOUT_MULTIPLIER = exports.DEFAULT_LAMBDA_TIMEOUT = exports.LIBRARY_VERSION = exports.AirSyncDefaultItemTypes = exports.UNKNOWN_EVENT_TYPE = exports.SSOR_ATTACHMENT = exports.EVENT_SIZE_THRESHOLD_BYTES = exports.MAX_EVENT_SIZE_BYTES = exports.MAX_DEVREV_FILENAME_EXTENSION_LENGTH = exports.MAX_DEVREV_FILENAME_LENGTH = exports.MAX_DEVREV_ARTIFACT_SIZE = exports.ARTIFACT_BATCH_SIZE = exports.STATELESS_EVENT_TYPES = exports.STATELESS_LOADING_EVENT_TYPES = exports.STATELESS_EXTRACTION_EVENT_TYPES = void 0;
|
|
3
|
+
exports.UNBOUNDED_DATE_TIME_VALUE = exports.MEMORY_LOG_INTERVAL = exports.HARD_TIMEOUT_MULTIPLIER = exports.DEFAULT_LAMBDA_TIMEOUT = exports.LIBRARY_VERSION = exports.AirSyncDefaultItemTypes = exports.UNKNOWN_EVENT_TYPE = exports.SSOR_ATTACHMENT = exports.EVENT_SIZE_THRESHOLD_BYTES = exports.MAX_EVENT_SIZE_BYTES = exports.MAX_DEVREV_FILENAME_EXTENSION_LENGTH = exports.MAX_DEVREV_FILENAME_LENGTH = exports.S3_MULTIPART_CHUNK_SIZE = exports.MAX_DEVREV_ARTIFACT_SIZE = exports.ARTIFACT_BATCH_SIZE = exports.STATELESS_EVENT_TYPES = exports.STATELESS_LOADING_EVENT_TYPES = exports.STATELESS_EXTRACTION_EVENT_TYPES = void 0;
|
|
4
4
|
const extraction_1 = require("../types/extraction");
|
|
5
5
|
const helpers_1 = require("./helpers");
|
|
6
6
|
exports.STATELESS_EXTRACTION_EVENT_TYPES = [
|
|
@@ -18,6 +18,9 @@ exports.STATELESS_EVENT_TYPES = [
|
|
|
18
18
|
];
|
|
19
19
|
exports.ARTIFACT_BATCH_SIZE = 2000;
|
|
20
20
|
exports.MAX_DEVREV_ARTIFACT_SIZE = 2 * 1024 * 1024 * 1024; // 2GB
|
|
21
|
+
// S3 multipart uploads require every non-final part to be at least 5 MiB. A
|
|
22
|
+
// 10 MiB part gives us a useful buffer while keeping the memory overhead low.
|
|
23
|
+
exports.S3_MULTIPART_CHUNK_SIZE = 10 * 1024 * 1024;
|
|
21
24
|
exports.MAX_DEVREV_FILENAME_LENGTH = 256;
|
|
22
25
|
exports.MAX_DEVREV_FILENAME_EXTENSION_LENGTH = 20;
|
|
23
26
|
// Max SQS message size is 250KB, we want to leave some room for the other data in the message
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extraction-adapter.d.ts","sourceRoot":"","sources":["../../../src/multithreading/adapters/extraction-adapter.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EACL,oBAAoB,EACpB,aAAa,EACd,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAIL,kCAAkC,EAClC,yCAAyC,EAEzC,aAAa,EACb,2BAA2B,EAE5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAkB,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAkB,MAAM,oCAAoC,CAAC;AAE9E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,2FAA2F;AAC3F,qBAAa,iBAAiB,CAC5B,cAAc,CACd,SAAQ,WAAW,CAAC,cAAc,CAAC;IACnC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,wBAAwB,CAAC,CAAkB;IACnD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,qBAAqB,CAAC,CAAS;IACvC,OAAO,CAAC,sBAAsB,CAAa;IAY3C,6EAA6E;IAC7E,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAOxC,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE;IAatC,OAAO,CAAC,UAAU;IAoDlB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAW3C,gEAAgE;IAChE,IAAI,SAAS,IAAI,SAAS,QAAQ,EAAE,CAEnC;IAED,OAAO,CAAC,YAAY;IAMpB;;;OAGG;IACH,IAAI,uBAAuB,IAAI,aAAa,EAAE,GAAG,SAAS,CAEzD;IAED,IAAI,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,SAAS,EAE9D;IA6HD,OAAO,CAAC,kBAAkB;YAaZ,cAAc;IAmB5B;;;;OAIG;IACG,iBAAiB,CACrB,UAAU,EAAE,oBAAoB,EAChC,MAAM,EAAE,yCAAyC,GAChD,OAAO,CAAC,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"extraction-adapter.d.ts","sourceRoot":"","sources":["../../../src/multithreading/adapters/extraction-adapter.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EACL,oBAAoB,EACpB,aAAa,EACd,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAIL,kCAAkC,EAClC,yCAAyC,EAEzC,aAAa,EACb,2BAA2B,EAE5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAkB,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAkB,MAAM,oCAAoC,CAAC;AAE9E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,2FAA2F;AAC3F,qBAAa,iBAAiB,CAC5B,cAAc,CACd,SAAQ,WAAW,CAAC,cAAc,CAAC;IACnC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,wBAAwB,CAAC,CAAkB;IACnD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,qBAAqB,CAAC,CAAS;IACvC,OAAO,CAAC,sBAAsB,CAAa;IAY3C,6EAA6E;IAC7E,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAOxC,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE;IAatC,OAAO,CAAC,UAAU;IAoDlB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAW3C,gEAAgE;IAChE,IAAI,SAAS,IAAI,SAAS,QAAQ,EAAE,CAEnC;IAED,OAAO,CAAC,YAAY;IAMpB;;;OAGG;IACH,IAAI,uBAAuB,IAAI,aAAa,EAAE,GAAG,SAAS,CAEzD;IAED,IAAI,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,SAAS,EAE9D;IA6HD,OAAO,CAAC,kBAAkB;YAaZ,cAAc;IAmB5B;;;;OAIG;IACG,iBAAiB,CACrB,UAAU,EAAE,oBAAoB,EAChC,MAAM,EAAE,yCAAyC,GAChD,OAAO,CAAC,2BAA2B,CAAC;IA4IjC,iBAAiB,CAAC,QAAQ,EAAE,EAChC,MAAM,EACN,UAAU,EACV,SAAa,GACd,EAAE;QACD,MAAM,EAAE,yCAAyC,CAAC;QAClD,UAAU,CAAC,EAAE,kCAAkC,CAC7C,cAAc,EACd,oBAAoB,EAAE,EACtB,QAAQ,CACT,CAAC;QACF,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,UAAU,CAAC;CAsHxB"}
|
|
@@ -230,44 +230,70 @@ class ExtractionAdapter extends base_adapter_1.BaseAdapter {
|
|
|
230
230
|
((_a = httpStream.headers['content-type']) === null || _a === void 0 ? void 0 : _a.toString()) ||
|
|
231
231
|
'application/octet-stream';
|
|
232
232
|
const contentLength = (_b = httpStream.headers['content-length']) === null || _b === void 0 ? void 0 : _b.toString();
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
};
|
|
233
|
+
const parsedFileSize = contentLength
|
|
234
|
+
? Number.parseInt(contentLength, 10)
|
|
235
|
+
: undefined;
|
|
236
|
+
const fileSize = parsedFileSize !== undefined && Number.isFinite(parsedFileSize)
|
|
237
|
+
? parsedFileSize
|
|
238
|
+
: undefined;
|
|
239
|
+
let artifactId;
|
|
240
|
+
if (fileSize === undefined) {
|
|
241
|
+
// Unknown-length streams cannot be uploaded using a presigned POST with
|
|
242
|
+
// a guessed Content-Length. Stream them through S3 multipart upload.
|
|
243
|
+
const { response: multipartUpload, error: multipartUploadError } = await this.uploader.streamArtifactMultipart(attachment.file_name, fileType, httpStream);
|
|
244
|
+
if (multipartUploadError || !multipartUpload) {
|
|
245
|
+
return {
|
|
246
|
+
status: 'error',
|
|
247
|
+
error: {
|
|
248
|
+
message: `Error while uploading attachment for attachment ID ${attachment.id}. Skipping attachment. ` +
|
|
249
|
+
(0, logger_1.serializeError)(multipartUploadError !== null && multipartUploadError !== void 0 ? multipartUploadError : new Error('Multipart upload returned no response.')),
|
|
250
|
+
fileSize,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
artifactId = multipartUpload.artifact_id;
|
|
256
255
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
256
|
+
else {
|
|
257
|
+
// Known-size attachments retain the existing single-request upload flow.
|
|
258
|
+
const { error: artifactUrlError, response: artifactUrlResponse } = await this.uploader.getArtifactUploadUrl(attachment.file_name, fileType, fileSize);
|
|
259
|
+
if (artifactUrlError || !artifactUrlResponse) {
|
|
260
|
+
(0, uploader_helpers_1.destroyHttpStream)(httpStream);
|
|
261
|
+
return {
|
|
262
|
+
status: 'error',
|
|
263
|
+
error: {
|
|
264
|
+
message: `Error while preparing artifact for attachment ID ${attachment.id}. Skipping attachment. ${(0, logger_1.serializeError)(artifactUrlError !== null && artifactUrlError !== void 0 ? artifactUrlError : new Error('No artifact upload response.'))}`,
|
|
265
|
+
fileSize,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const { error: uploadedArtifactError } = await this.uploader.streamArtifact(artifactUrlResponse, httpStream);
|
|
270
|
+
if (uploadedArtifactError) {
|
|
271
|
+
(0, uploader_helpers_1.destroyHttpStream)(httpStream);
|
|
272
|
+
return {
|
|
273
|
+
status: 'error',
|
|
274
|
+
error: {
|
|
275
|
+
message: `Error while streaming to artifact for attachment ID ${attachment.id}. Skipping attachment. ` +
|
|
276
|
+
(0, logger_1.serializeError)(uploadedArtifactError),
|
|
277
|
+
fileSize,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const { error: confirmArtifactUploadError } = await this.uploader.confirmArtifactUpload(artifactUrlResponse.artifact_id);
|
|
282
|
+
if (confirmArtifactUploadError) {
|
|
283
|
+
return {
|
|
284
|
+
status: 'error',
|
|
285
|
+
error: {
|
|
286
|
+
message: `Error while confirming upload for attachment ID ${attachment.id}. ` +
|
|
287
|
+
(0, logger_1.serializeError)(confirmArtifactUploadError),
|
|
288
|
+
fileSize,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
artifactId = artifactUrlResponse.artifact_id;
|
|
267
293
|
}
|
|
268
294
|
const ssorAttachment = {
|
|
269
295
|
id: {
|
|
270
|
-
devrev:
|
|
296
|
+
devrev: artifactId,
|
|
271
297
|
external: attachment.id,
|
|
272
298
|
},
|
|
273
299
|
parent_id: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AxiosResponse } from 'axios';
|
|
2
2
|
import { NormalizedAttachment } from '../repo/repo.interfaces';
|
|
3
3
|
import { HttpStreamResponse } from '../types/extraction';
|
|
4
|
-
import { ArtifactToUpload, UploaderFactoryInterface, UploaderResult, UploadResponse } from './uploader.interfaces';
|
|
4
|
+
import { ArtifactToUpload, MultipartUpload, UploaderFactoryInterface, UploaderResult, UploadResponse } from './uploader.interfaces';
|
|
5
5
|
export declare class Uploader {
|
|
6
6
|
private isLocalDevelopment?;
|
|
7
7
|
private devrevApiEndpoint;
|
|
@@ -17,6 +17,26 @@ export declare class Uploader {
|
|
|
17
17
|
uploadArtifact(artifact: ArtifactToUpload, file: Buffer): Promise<UploaderResult<AxiosResponse>>;
|
|
18
18
|
/** Streams a file from an HTTP response directly to the artifact's upload URL. */
|
|
19
19
|
streamArtifact(artifact: ArtifactToUpload, fileStream: HttpStreamResponse): Promise<UploaderResult<AxiosResponse>>;
|
|
20
|
+
/**
|
|
21
|
+
* Creates an S3 multipart upload for an artifact whose size is not known.
|
|
22
|
+
* The returned URL is for part 1; subsequent URLs are obtained with
|
|
23
|
+
* getNextMultipartUploadPart.
|
|
24
|
+
*/
|
|
25
|
+
createMultipartUpload(filename: string, fileType: string, partSize?: number): Promise<UploaderResult<MultipartUpload>>;
|
|
26
|
+
/** Gets a presigned URL for the next multipart upload part. */
|
|
27
|
+
getNextMultipartUploadPart(artifactId: string, cursor: string, partSize?: number): Promise<UploaderResult<MultipartUpload>>;
|
|
28
|
+
/** Finalizes and publishes an S3 multipart artifact upload. */
|
|
29
|
+
completeMultipartUpload(artifactId: string, cursor: string): Promise<UploaderResult<AxiosResponse>>;
|
|
30
|
+
/**
|
|
31
|
+
* Streams an attachment of unknown size through an S3 multipart upload.
|
|
32
|
+
* At most one 10 MiB part is buffered at a time. The final part may be
|
|
33
|
+
* smaller than S3's 5 MiB minimum.
|
|
34
|
+
*/
|
|
35
|
+
streamArtifactMultipart(filename: string, fileType: string, fileStream: HttpStreamResponse): Promise<UploaderResult<{
|
|
36
|
+
artifact_id: string;
|
|
37
|
+
}>>;
|
|
38
|
+
/** Uploads one raw part to its presigned S3 URL. */
|
|
39
|
+
private uploadMultipartPart;
|
|
20
40
|
confirmArtifactUpload(artifactId: string): Promise<{
|
|
21
41
|
response?: AxiosResponse;
|
|
22
42
|
error?: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAUtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAWzD,OAAO,EAEL,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,cAAc,EACd,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B,qBAAa,QAAQ;IACnB,OAAO,CAAC,kBAAkB,CAAC,CAAU;IACrC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,gBAAgB,CAAU;gBAEtB,EAAE,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,wBAAwB;IAW1E,mFAAmF;IAC7E,MAAM,CACV,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,GAChC,OAAO,CAAC,cAAc,CAAC;IAwEpB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IA2B5C,mFAAmF;IAC7E,cAAc,CAClB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAmBzC,kFAAkF;IAC5E,cAAc,CAClB,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAqCzC;;;;OAIG;IACG,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAA0B,GACjC,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IAkB3C,+DAA+D;IACzD,0BAA0B,CAC9B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,SAA0B,GACjC,OAAO,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IAkB3C,+DAA+D;IACzD,uBAAuB,CAC3B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;IAoBzC;;;;OAIG;IACG,uBAAuB,CAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC,cAAc,CAAC;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IA6HnD,oDAAoD;YACtC,mBAAmB;IAsD3B,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QACvD,QAAQ,CAAC,EAAE,aAAa,CAAC;QACzB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;IAgCF,oEAAoE;IAC9D,4BAA4B,CAAC,EACjC,QAAQ,GACT,EAAE;QACD,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QACV,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;QACrC,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7B,CAAC;YAuDY,sBAAsB;YAsBtB,gBAAgB;IAc9B,yEAAyE;IACnE,yBAAyB,CAAC,EAC9B,UAAU,EACV,SAAiB,GAClB,EAAE;QACD,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC;CA6B/C"}
|
|
@@ -40,6 +40,12 @@ export interface ArtifactToUpload {
|
|
|
40
40
|
value: string;
|
|
41
41
|
}[];
|
|
42
42
|
}
|
|
43
|
+
/** Multipart upload state returned by the artifacts upload endpoints. */
|
|
44
|
+
export interface MultipartUpload {
|
|
45
|
+
id: string;
|
|
46
|
+
url: string;
|
|
47
|
+
cursor: string;
|
|
48
|
+
}
|
|
43
49
|
export interface UploadResponse {
|
|
44
50
|
artifact?: Artifact;
|
|
45
51
|
error?: ErrorRecord;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploader.interfaces.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IACxB;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzC,oBAAY,iBAAiB;IAC3B,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,kBAAkB,yBAAyB;IAC3C,kBAAkB,yBAAyB;CAC5C;AAED,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;IAChD,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;CACjD;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,QAAQ,EACN,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,kBAAkB,GACpC,iBAAiB,CAAC,kBAAkB,CACvC,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;KACf,EAAE,CAAC;CACL;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE;QACF,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
|
|
1
|
+
{"version":3,"file":"uploader.interfaces.d.ts","sourceRoot":"","sources":["../../src/uploader/uploader.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IACxB;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzC,oBAAY,iBAAiB;IAC3B,iBAAiB,wBAAwB;IACzC,iBAAiB,wBAAwB;IACzC,kBAAkB,yBAAyB;IAC3C,kBAAkB,yBAAyB;CAC5C;AAED,iEAAiE;AACjE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,EAAE,MAAM,CAAC;IAC/C,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;IAChD,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM,CAAC;CACjD;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,QAAQ,EACN,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,iBAAiB,GACnC,iBAAiB,CAAC,kBAAkB,GACpC,iBAAiB,CAAC,kBAAkB,CACvC,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;KACf,EAAE,CAAC;CACL;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE;QACF,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Uploader = void 0;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const https_1 = __importDefault(require("https"));
|
|
7
9
|
const form_data_1 = __importDefault(require("form-data"));
|
|
8
10
|
const js_jsonl_1 = require("js-jsonl");
|
|
9
11
|
const constants_1 = require("../common/constants");
|
|
@@ -136,6 +138,201 @@ class Uploader {
|
|
|
136
138
|
return { error };
|
|
137
139
|
}
|
|
138
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Creates an S3 multipart upload for an artifact whose size is not known.
|
|
143
|
+
* The returned URL is for part 1; subsequent URLs are obtained with
|
|
144
|
+
* getNextMultipartUploadPart.
|
|
145
|
+
*/
|
|
146
|
+
async createMultipartUpload(filename, fileType, partSize = constants_1.S3_MULTIPART_CHUNK_SIZE) {
|
|
147
|
+
const url = `${this.devrevApiEndpoint}/internal/artifacts.upload.create`;
|
|
148
|
+
try {
|
|
149
|
+
const response = await client_1.axiosClient.post(url, {
|
|
150
|
+
file_name: (0, uploader_helpers_1.truncateFilename)(filename),
|
|
151
|
+
file_type: fileType,
|
|
152
|
+
part_size: partSize,
|
|
153
|
+
}, { headers: Object.assign({}, this.defaultHeaders) });
|
|
154
|
+
return { response: response.data };
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return { error };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Gets a presigned URL for the next multipart upload part. */
|
|
161
|
+
async getNextMultipartUploadPart(artifactId, cursor, partSize = constants_1.S3_MULTIPART_CHUNK_SIZE) {
|
|
162
|
+
const url = `${this.devrevApiEndpoint}/internal/artifacts.upload.next`;
|
|
163
|
+
try {
|
|
164
|
+
const response = await client_1.axiosClient.post(url, {
|
|
165
|
+
id: artifactId,
|
|
166
|
+
cursor,
|
|
167
|
+
part_size: partSize,
|
|
168
|
+
}, { headers: Object.assign({}, this.defaultHeaders) });
|
|
169
|
+
return { response: response.data };
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return { error };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Finalizes and publishes an S3 multipart artifact upload. */
|
|
176
|
+
async completeMultipartUpload(artifactId, cursor) {
|
|
177
|
+
const url = `${this.devrevApiEndpoint}/internal/artifacts.upload.complete`;
|
|
178
|
+
try {
|
|
179
|
+
const response = await client_1.axiosClient.post(url, {
|
|
180
|
+
id: artifactId,
|
|
181
|
+
cursor,
|
|
182
|
+
}, { headers: Object.assign({}, this.defaultHeaders) });
|
|
183
|
+
if (response.status >= 200 && response.status < 300) {
|
|
184
|
+
return { response };
|
|
185
|
+
}
|
|
186
|
+
return { error: response };
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return { error };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Streams an attachment of unknown size through an S3 multipart upload.
|
|
194
|
+
* At most one 10 MiB part is buffered at a time. The final part may be
|
|
195
|
+
* smaller than S3's 5 MiB minimum.
|
|
196
|
+
*/
|
|
197
|
+
async streamArtifactMultipart(filename, fileType, fileStream) {
|
|
198
|
+
try {
|
|
199
|
+
const iterator = fileStream.data[Symbol.asyncIterator]();
|
|
200
|
+
let pendingChunk;
|
|
201
|
+
// Read a complete part before requesting its presigned URL. The gateway
|
|
202
|
+
// signs Content-Length using part_size, so the requested part size must
|
|
203
|
+
// be the exact byte length that will be sent (including the final part).
|
|
204
|
+
const readPart = async () => {
|
|
205
|
+
const chunks = [];
|
|
206
|
+
let length = 0;
|
|
207
|
+
while (length < constants_1.S3_MULTIPART_CHUNK_SIZE) {
|
|
208
|
+
let chunk = pendingChunk;
|
|
209
|
+
pendingChunk = undefined;
|
|
210
|
+
if (!chunk) {
|
|
211
|
+
const next = await iterator.next();
|
|
212
|
+
if (next.done) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
chunk = Buffer.isBuffer(next.value)
|
|
216
|
+
? next.value
|
|
217
|
+
: Buffer.from(next.value);
|
|
218
|
+
}
|
|
219
|
+
if (chunk.length === 0) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const capacity = constants_1.S3_MULTIPART_CHUNK_SIZE - length;
|
|
223
|
+
if (chunk.length <= capacity) {
|
|
224
|
+
chunks.push(chunk);
|
|
225
|
+
length += chunk.length;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
chunks.push(chunk.subarray(0, capacity));
|
|
229
|
+
pendingChunk = chunk.subarray(capacity);
|
|
230
|
+
length += capacity;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return length > 0 ? Buffer.concat(chunks, length) : undefined;
|
|
234
|
+
};
|
|
235
|
+
let part = await readPart();
|
|
236
|
+
if (!part) {
|
|
237
|
+
throw new Error('Cannot complete a multipart upload with an empty file.');
|
|
238
|
+
}
|
|
239
|
+
const { response: createdUpload, error: createError } = await this.createMultipartUpload(filename, fileType, part.length);
|
|
240
|
+
if (createError || !createdUpload) {
|
|
241
|
+
return {
|
|
242
|
+
error: createError !== null && createError !== void 0 ? createError : new Error('Multipart upload creation returned no response.'),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const artifactId = createdUpload.id;
|
|
246
|
+
let cursor = createdUpload.cursor;
|
|
247
|
+
let uploadUrl = createdUpload.url;
|
|
248
|
+
if (!artifactId || !cursor || !uploadUrl) {
|
|
249
|
+
return {
|
|
250
|
+
error: new Error('Multipart upload creation returned incomplete upload information.'),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
while (part) {
|
|
254
|
+
const { response, error } = await this.uploadMultipartPart(uploadUrl, part, part.length);
|
|
255
|
+
if (error || !response) {
|
|
256
|
+
return {
|
|
257
|
+
error: error !== null && error !== void 0 ? error : new Error('Multipart part upload returned no response.'),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const nextPart = part.length === constants_1.S3_MULTIPART_CHUNK_SIZE
|
|
261
|
+
? await readPart()
|
|
262
|
+
: undefined;
|
|
263
|
+
if (!nextPart) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const { response: nextUpload, error: nextError } = await this.getNextMultipartUploadPart(artifactId, cursor, nextPart.length);
|
|
267
|
+
if (nextError || !(nextUpload === null || nextUpload === void 0 ? void 0 : nextUpload.url) || !nextUpload.cursor) {
|
|
268
|
+
return {
|
|
269
|
+
error: nextError !== null && nextError !== void 0 ? nextError : new Error('Could not obtain the next multipart upload URL.'),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
part = nextPart;
|
|
273
|
+
uploadUrl = nextUpload.url;
|
|
274
|
+
cursor = nextUpload.cursor;
|
|
275
|
+
}
|
|
276
|
+
const { error: completeError } = await this.completeMultipartUpload(artifactId, cursor);
|
|
277
|
+
if (completeError) {
|
|
278
|
+
return { error: completeError };
|
|
279
|
+
}
|
|
280
|
+
return { response: { artifact_id: artifactId } };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
return { error };
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
(0, uploader_helpers_1.destroyHttpStream)(fileStream);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Uploads one raw part to its presigned S3 URL. */
|
|
290
|
+
async uploadMultipartPart(uploadUrl, part, contentLength) {
|
|
291
|
+
try {
|
|
292
|
+
// Use Node's native request instead of Axios for the presigned request.
|
|
293
|
+
// Axios may normalize, remove, or recalculate Content-Length; that is
|
|
294
|
+
// not allowed when the presigned URL includes this header in its
|
|
295
|
+
// signature.
|
|
296
|
+
const parsedUrl = new URL(uploadUrl);
|
|
297
|
+
const transport = parsedUrl.protocol === 'https:' ? https_1.default : http_1.default;
|
|
298
|
+
const response = await new Promise((resolve, reject) => {
|
|
299
|
+
const request = transport.request(parsedUrl, {
|
|
300
|
+
method: 'PUT',
|
|
301
|
+
headers: {
|
|
302
|
+
'Content-Length': String(contentLength),
|
|
303
|
+
},
|
|
304
|
+
}, (incomingResponse) => {
|
|
305
|
+
const chunks = [];
|
|
306
|
+
incomingResponse.on('data', (chunk) => chunks.push(chunk));
|
|
307
|
+
incomingResponse.on('end', () => {
|
|
308
|
+
var _a, _b;
|
|
309
|
+
const data = Buffer.concat(chunks);
|
|
310
|
+
const status = (_a = incomingResponse.statusCode) !== null && _a !== void 0 ? _a : 0;
|
|
311
|
+
const axiosResponse = {
|
|
312
|
+
data,
|
|
313
|
+
status,
|
|
314
|
+
statusText: (_b = incomingResponse.statusMessage) !== null && _b !== void 0 ? _b : '',
|
|
315
|
+
headers: incomingResponse.headers,
|
|
316
|
+
config: {},
|
|
317
|
+
request,
|
|
318
|
+
};
|
|
319
|
+
if (status >= 200 && status < 400) {
|
|
320
|
+
resolve(axiosResponse);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
reject(new Error(`Multipart part upload returned HTTP ${status}.`));
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
request.once('error', reject);
|
|
328
|
+
request.end(part);
|
|
329
|
+
});
|
|
330
|
+
return { response };
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
return { error };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
139
336
|
async confirmArtifactUpload(artifactId) {
|
|
140
337
|
const url = `${this.devrevApiEndpoint}/internal/airdrop.artifacts.confirm-upload`;
|
|
141
338
|
try {
|