@protontech/drive-sdk 0.12.1 → 0.13.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/dist/crypto/driveCrypto.d.ts +1 -0
- package/dist/crypto/driveCrypto.js +1 -0
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/interface/featureFlags.d.ts +2 -1
- package/dist/interface/featureFlags.js +1 -0
- package/dist/interface/featureFlags.js.map +1 -1
- package/dist/interface/httpClient.d.ts +1 -0
- package/dist/interface/nodes.d.ts +7 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/internal/apiService/apiService.d.ts +1 -0
- package/dist/internal/apiService/apiService.js +16 -7
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +24 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +110 -101
- package/dist/internal/nodes/apiService.d.ts +4 -0
- package/dist/internal/nodes/apiService.js +4 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +8 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +3 -0
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
- package/dist/internal/nodes/extendedAttributes.js +5 -14
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +16 -22
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +5 -0
- package/dist/internal/nodes/nodesManagement.d.ts +3 -3
- package/dist/internal/nodes/nodesManagement.js +7 -5
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/albumsManager.js +1 -0
- package/dist/internal/photos/albumsManager.js.map +1 -1
- package/dist/internal/photos/nodes.d.ts +1 -1
- package/dist/internal/photos/nodes.js +2 -2
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +5 -5
- package/dist/internal/photos/upload.js +8 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +1 -0
- package/dist/internal/upload/apiService.d.ts +45 -1
- package/dist/internal/upload/apiService.js +69 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/blockVerifier.d.ts +4 -1
- package/dist/internal/upload/blockVerifier.js +5 -0
- package/dist/internal/upload/blockVerifier.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +2 -2
- package/dist/internal/upload/cryptoService.js +1 -3
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +4 -3
- package/dist/internal/upload/fileUploader.js +17 -7
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/index.d.ts +3 -3
- package/dist/internal/upload/index.js +17 -1
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/index.test.d.ts +1 -0
- package/dist/internal/upload/index.test.js +71 -0
- package/dist/internal/upload/index.test.js.map +1 -0
- package/dist/internal/upload/interface.d.ts +2 -0
- package/dist/internal/upload/manager.d.ts +41 -2
- package/dist/internal/upload/manager.js +123 -42
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +267 -0
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/smallFileUploader.d.ts +83 -0
- package/dist/internal/upload/smallFileUploader.js +197 -0
- package/dist/internal/upload/smallFileUploader.js.map +1 -0
- package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
- package/dist/internal/upload/smallFileUploader.test.js +358 -0
- package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
- package/dist/internal/upload/streamReader.d.ts +4 -0
- package/dist/internal/upload/streamReader.js +37 -0
- package/dist/internal/upload/streamReader.js.map +1 -0
- package/dist/internal/upload/streamReader.test.d.ts +1 -0
- package/dist/internal/upload/streamReader.test.js +90 -0
- package/dist/internal/upload/streamReader.test.js.map +1 -0
- package/dist/internal/upload/streamUploader.d.ts +6 -0
- package/dist/internal/upload/streamUploader.js +3 -3
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/telemetry.d.ts +3 -2
- package/dist/internal/upload/telemetry.js +3 -0
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +1 -1
- package/dist/protonDrivePublicLinkClient.js +3 -1
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +1 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +2 -0
- package/src/interface/featureFlags.ts +1 -0
- package/src/interface/httpClient.ts +1 -0
- package/src/interface/nodes.ts +7 -0
- package/src/internal/apiService/apiService.test.ts +30 -0
- package/src/internal/apiService/apiService.ts +23 -7
- package/src/internal/apiService/driveTypes.ts +110 -101
- package/src/internal/nodes/apiService.test.ts +9 -0
- package/src/internal/nodes/apiService.ts +4 -0
- package/src/internal/nodes/cryptoService.ts +11 -1
- package/src/internal/nodes/extendedAttributes.test.ts +25 -25
- package/src/internal/nodes/extendedAttributes.ts +10 -19
- package/src/internal/nodes/interface.ts +5 -0
- package/src/internal/nodes/nodesManagement.ts +8 -6
- package/src/internal/photos/albumsManager.ts +1 -0
- package/src/internal/photos/nodes.ts +2 -2
- package/src/internal/photos/upload.ts +23 -10
- package/src/internal/upload/apiService.ts +167 -2
- package/src/internal/upload/blockVerifier.ts +12 -0
- package/src/internal/upload/cryptoService.ts +10 -10
- package/src/internal/upload/fileUploader.ts +20 -7
- package/src/internal/upload/index.test.ts +99 -0
- package/src/internal/upload/index.ts +45 -4
- package/src/internal/upload/interface.ts +2 -0
- package/src/internal/upload/manager.test.ts +367 -1
- package/src/internal/upload/manager.ts +226 -76
- package/src/internal/upload/smallFileUploader.test.ts +491 -0
- package/src/internal/upload/smallFileUploader.ts +353 -0
- package/src/internal/upload/streamReader.test.ts +109 -0
- package/src/internal/upload/streamReader.ts +38 -0
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/internal/upload/telemetry.ts +5 -1
- package/src/protonDrivePhotosClient.ts +1 -1
- package/src/protonDrivePublicLinkClient.ts +2 -0
- package/src/transformers.ts +2 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { PrivateKey, SessionKey } from '../../crypto';
|
|
2
|
+
import { Logger, Thumbnail, ThumbnailType, UploadMetadata } from '../../interface';
|
|
3
|
+
import { UploadAPIService } from './apiService';
|
|
4
|
+
import { BlockVerifier } from './blockVerifier';
|
|
5
|
+
import { UploadCryptoService } from './cryptoService';
|
|
6
|
+
import { Uploader } from './fileUploader';
|
|
7
|
+
import { NodeRevisionDraft, NodeCrypto } from './interface';
|
|
8
|
+
import { UploadManager } from './manager';
|
|
9
|
+
import { UploadTelemetry } from './telemetry';
|
|
10
|
+
export type NodeKeys = {
|
|
11
|
+
key: PrivateKey;
|
|
12
|
+
contentKeyPacket: Uint8Array<ArrayBuffer>;
|
|
13
|
+
contentKeyPacketSessionKey: SessionKey;
|
|
14
|
+
signingKeys: NodeCrypto['signingKeys'];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Base uploader for small file and small revision uploads.
|
|
18
|
+
* Shares the single-request flow: read content, get node crypto, encrypt, then call API.
|
|
19
|
+
*/
|
|
20
|
+
declare abstract class SmallUploader extends Uploader {
|
|
21
|
+
protected logger: Logger;
|
|
22
|
+
constructor(telemetry: UploadTelemetry, apiService: UploadAPIService, cryptoService: UploadCryptoService, manager: UploadManager, metadata: UploadMetadata, onFinish: () => void, signal: AbortSignal | undefined);
|
|
23
|
+
protected createRevisionDraft(): Promise<{
|
|
24
|
+
revisionDraft: NodeRevisionDraft;
|
|
25
|
+
blockVerifier: BlockVerifier;
|
|
26
|
+
}>;
|
|
27
|
+
protected deleteRevisionDraft(revisionDraft: NodeRevisionDraft): Promise<void>;
|
|
28
|
+
protected startUpload(stream: ReadableStream, thumbnails: Thumbnail[], onProgress?: (uploadedBytes: number) => void): Promise<{
|
|
29
|
+
nodeRevisionUid: string;
|
|
30
|
+
nodeUid: string;
|
|
31
|
+
}>;
|
|
32
|
+
protected abstract getTelemetryContextUid(): string;
|
|
33
|
+
protected abstract handleUpload(stream: ReadableStream, thumbnails: Thumbnail[]): Promise<{
|
|
34
|
+
nodeUid: string;
|
|
35
|
+
nodeRevisionUid: string;
|
|
36
|
+
}>;
|
|
37
|
+
protected buildPayloads(nodeKeys: NodeKeys, stream: ReadableStream, thumbnails: Thumbnail[]): Promise<{
|
|
38
|
+
commitPayload: {
|
|
39
|
+
armoredManifestSignature: string;
|
|
40
|
+
armoredExtendedAttributes: string;
|
|
41
|
+
};
|
|
42
|
+
encryptedBlock: {
|
|
43
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
44
|
+
armoredSignature: string;
|
|
45
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
46
|
+
} | undefined;
|
|
47
|
+
encryptedThumbnails: {
|
|
48
|
+
type: ThumbnailType;
|
|
49
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
50
|
+
}[];
|
|
51
|
+
}>;
|
|
52
|
+
private readStreamContent;
|
|
53
|
+
private encryptThumbnails;
|
|
54
|
+
private encryptContentBlock;
|
|
55
|
+
private encryptCommitPayload;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Uploader for small new files using the single-request small file endpoint.
|
|
59
|
+
*/
|
|
60
|
+
export declare class SmallFileUploader extends SmallUploader {
|
|
61
|
+
private parentFolderUid;
|
|
62
|
+
private name;
|
|
63
|
+
constructor(telemetry: UploadTelemetry, apiService: UploadAPIService, cryptoService: UploadCryptoService, manager: UploadManager, metadata: UploadMetadata, onFinish: () => void, signal: AbortSignal | undefined, parentFolderUid: string, name: string);
|
|
64
|
+
protected getTelemetryContextUid(): string;
|
|
65
|
+
protected handleUpload(stream: ReadableStream, thumbnails: Thumbnail[]): Promise<{
|
|
66
|
+
nodeUid: string;
|
|
67
|
+
nodeRevisionUid: string;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Uploader for small new revisions using the single-request small revision endpoint.
|
|
72
|
+
* Reuses the existing file's keys.
|
|
73
|
+
*/
|
|
74
|
+
export declare class SmallFileRevisionUploader extends SmallUploader {
|
|
75
|
+
private nodeUid;
|
|
76
|
+
constructor(telemetry: UploadTelemetry, apiService: UploadAPIService, cryptoService: UploadCryptoService, manager: UploadManager, metadata: UploadMetadata, onFinish: () => void, signal: AbortSignal | undefined, nodeUid: string);
|
|
77
|
+
protected getTelemetryContextUid(): string;
|
|
78
|
+
protected handleUpload(stream: ReadableStream, thumbnails: Thumbnail[]): Promise<{
|
|
79
|
+
nodeUid: string;
|
|
80
|
+
nodeRevisionUid: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SmallFileRevisionUploader = exports.SmallFileUploader = void 0;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
const errors_2 = require("../errors");
|
|
6
|
+
const nodes_1 = require("../nodes");
|
|
7
|
+
const blockVerifier_1 = require("./blockVerifier");
|
|
8
|
+
const digests_1 = require("./digests");
|
|
9
|
+
const fileUploader_1 = require("./fileUploader");
|
|
10
|
+
const streamReader_1 = require("./streamReader");
|
|
11
|
+
const streamUploader_1 = require("./streamUploader");
|
|
12
|
+
/**
|
|
13
|
+
* Base uploader for small file and small revision uploads.
|
|
14
|
+
* Shares the single-request flow: read content, get node crypto, encrypt, then call API.
|
|
15
|
+
*/
|
|
16
|
+
class SmallUploader extends fileUploader_1.Uploader {
|
|
17
|
+
logger;
|
|
18
|
+
constructor(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal) {
|
|
19
|
+
super(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal);
|
|
20
|
+
this.logger = telemetry.getLoggerForSmallUpload();
|
|
21
|
+
}
|
|
22
|
+
async createRevisionDraft() {
|
|
23
|
+
throw new Error('Small upload does not use revision draft');
|
|
24
|
+
}
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
26
|
+
async deleteRevisionDraft(revisionDraft) {
|
|
27
|
+
throw new Error('Small upload does not use revision draft');
|
|
28
|
+
}
|
|
29
|
+
async startUpload(stream, thumbnails, onProgress) {
|
|
30
|
+
try {
|
|
31
|
+
const result = await this.handleUpload(stream, thumbnails);
|
|
32
|
+
onProgress?.(this.metadata.expectedSize);
|
|
33
|
+
void this.telemetry.uploadFinished(result.nodeRevisionUid, this.metadata.expectedSize);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
void this.telemetry.uploadInitFailed(this.getTelemetryContextUid(), error, this.metadata.expectedSize);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
this.onFinish();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async buildPayloads(nodeKeys, stream, thumbnails) {
|
|
45
|
+
const content = await this.readStreamContent(stream);
|
|
46
|
+
const [encryptedThumbnails, encryptedBlock] = await Promise.all([
|
|
47
|
+
this.encryptThumbnails(nodeKeys, thumbnails),
|
|
48
|
+
this.encryptContentBlock(nodeKeys, content.data),
|
|
49
|
+
]);
|
|
50
|
+
const commitPayload = await this.encryptCommitPayload(nodeKeys, content.sha1, encryptedBlock);
|
|
51
|
+
return {
|
|
52
|
+
commitPayload,
|
|
53
|
+
encryptedBlock,
|
|
54
|
+
encryptedThumbnails,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async readStreamContent(stream) {
|
|
58
|
+
const content = await (0, streamReader_1.readStreamToUint8Array)(stream, this.abortController.signal);
|
|
59
|
+
if (content.length !== this.metadata.expectedSize) {
|
|
60
|
+
throw new errors_1.IntegrityError(new Error('Stream size does not match expected size').message, {
|
|
61
|
+
actual: content.length,
|
|
62
|
+
expected: this.metadata.expectedSize,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const digests = new digests_1.UploadDigests();
|
|
66
|
+
digests.update(content);
|
|
67
|
+
const contentSha1 = digests.digests().sha1;
|
|
68
|
+
if (this.metadata.expectedSha1 && contentSha1 !== this.metadata.expectedSha1) {
|
|
69
|
+
throw new errors_1.IntegrityError(new Error('File hash does not match expected hash').message, {
|
|
70
|
+
uploadedSha1: contentSha1,
|
|
71
|
+
expectedSha1: this.metadata.expectedSha1,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
data: content,
|
|
76
|
+
sha1: contentSha1,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async encryptThumbnails(nodeKeys, thumbnails) {
|
|
80
|
+
const result = [];
|
|
81
|
+
for (const thumbnail of thumbnails) {
|
|
82
|
+
this.logger.debug(`Encrypting thumbnail ${thumbnail.type}`);
|
|
83
|
+
const enc = await this.cryptoService.encryptThumbnail(nodeKeys, thumbnail);
|
|
84
|
+
result.push({ type: thumbnail.type, encryptedData: enc.encryptedData });
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
async encryptContentBlock(nodeKeys, content) {
|
|
89
|
+
this.logger.debug(`Encrypting block`);
|
|
90
|
+
if (content.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let attempt = 0;
|
|
94
|
+
let integrityError = false;
|
|
95
|
+
let encrypted;
|
|
96
|
+
while (!encrypted) {
|
|
97
|
+
attempt++;
|
|
98
|
+
try {
|
|
99
|
+
encrypted = await this.cryptoService.encryptBlock((encryptedBlock) => (0, blockVerifier_1.verifyBlockWithContentKey)(this.cryptoService, nodeKeys.contentKeyPacket, nodeKeys.contentKeyPacketSessionKey, encryptedBlock), nodeKeys, content, 0);
|
|
100
|
+
if (integrityError) {
|
|
101
|
+
void this.telemetry.logBlockVerificationError(true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Do not retry or report anything if the upload was aborted.
|
|
106
|
+
if (error instanceof errors_1.AbortError) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
if (error instanceof errors_1.IntegrityError) {
|
|
110
|
+
integrityError = true;
|
|
111
|
+
}
|
|
112
|
+
if (attempt <= streamUploader_1.MAX_BLOCK_ENCRYPTION_RETRIES) {
|
|
113
|
+
this.logger.warn(`Block encryption failed #${attempt}, retrying: ${(0, errors_2.getErrorMessage)(error)}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
this.logger.error(`Failed to encrypt block`, error);
|
|
117
|
+
if (integrityError) {
|
|
118
|
+
void this.telemetry.logBlockVerificationError(false);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const blockHash = await encrypted.hashPromise;
|
|
124
|
+
return {
|
|
125
|
+
encryptedData: encrypted.encryptedData,
|
|
126
|
+
armoredSignature: encrypted.armoredSignature,
|
|
127
|
+
verificationToken: encrypted.verificationToken,
|
|
128
|
+
blockHash,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async encryptCommitPayload(nodeKeys, contentSha1, encryptedBlock) {
|
|
132
|
+
this.logger.debug(`Preparing commit payload`);
|
|
133
|
+
const manifest = encryptedBlock ? encryptedBlock.blockHash : new Uint8Array(0);
|
|
134
|
+
const extendedAttributes = (0, nodes_1.generateFileExtendedAttributes)({
|
|
135
|
+
modificationTime: this.metadata.modificationTime,
|
|
136
|
+
size: this.metadata.expectedSize,
|
|
137
|
+
blockSizes: this.metadata.expectedSize > 0 ? [this.metadata.expectedSize] : [],
|
|
138
|
+
digests: { sha1: contentSha1 },
|
|
139
|
+
}, this.metadata.additionalMetadata);
|
|
140
|
+
const commitCrypto = await this.cryptoService.commitFile(nodeKeys, manifest, extendedAttributes);
|
|
141
|
+
return {
|
|
142
|
+
armoredManifestSignature: commitCrypto.armoredManifestSignature,
|
|
143
|
+
armoredExtendedAttributes: commitCrypto.armoredExtendedAttributes,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Uploader for small new files using the single-request small file endpoint.
|
|
149
|
+
*/
|
|
150
|
+
class SmallFileUploader extends SmallUploader {
|
|
151
|
+
parentFolderUid;
|
|
152
|
+
name;
|
|
153
|
+
constructor(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal, parentFolderUid, name) {
|
|
154
|
+
super(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal);
|
|
155
|
+
this.parentFolderUid = parentFolderUid;
|
|
156
|
+
this.name = name;
|
|
157
|
+
this.parentFolderUid = parentFolderUid;
|
|
158
|
+
this.name = name;
|
|
159
|
+
}
|
|
160
|
+
getTelemetryContextUid() {
|
|
161
|
+
return this.parentFolderUid;
|
|
162
|
+
}
|
|
163
|
+
async handleUpload(stream, thumbnails) {
|
|
164
|
+
const nodeCrypto = await this.manager.generateNewFileCrypto(this.parentFolderUid, this.name);
|
|
165
|
+
const nodeKeys = {
|
|
166
|
+
key: nodeCrypto.nodeKeys.decrypted.key,
|
|
167
|
+
contentKeyPacket: nodeCrypto.contentKey.encrypted.contentKeyPacket,
|
|
168
|
+
contentKeyPacketSessionKey: nodeCrypto.contentKey.decrypted.contentKeyPacketSessionKey,
|
|
169
|
+
signingKeys: nodeCrypto.signingKeys,
|
|
170
|
+
};
|
|
171
|
+
const payloads = await this.buildPayloads(nodeKeys, stream, thumbnails);
|
|
172
|
+
return this.manager.uploadFile(this.parentFolderUid, nodeCrypto, this.metadata, payloads.commitPayload, payloads.encryptedBlock, payloads.encryptedThumbnails);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.SmallFileUploader = SmallFileUploader;
|
|
176
|
+
/**
|
|
177
|
+
* Uploader for small new revisions using the single-request small revision endpoint.
|
|
178
|
+
* Reuses the existing file's keys.
|
|
179
|
+
*/
|
|
180
|
+
class SmallFileRevisionUploader extends SmallUploader {
|
|
181
|
+
nodeUid;
|
|
182
|
+
constructor(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal, nodeUid) {
|
|
183
|
+
super(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal);
|
|
184
|
+
this.nodeUid = nodeUid;
|
|
185
|
+
this.nodeUid = nodeUid;
|
|
186
|
+
}
|
|
187
|
+
getTelemetryContextUid() {
|
|
188
|
+
return this.nodeUid;
|
|
189
|
+
}
|
|
190
|
+
async handleUpload(stream, thumbnails) {
|
|
191
|
+
const nodeKeys = await this.manager.getExistingFileNodeCrypto(this.nodeUid);
|
|
192
|
+
const payloads = await this.buildPayloads(nodeKeys, stream, thumbnails);
|
|
193
|
+
return this.manager.uploadSmallRevision(this.nodeUid, nodeKeys, payloads.commitPayload, payloads.encryptedBlock, payloads.encryptedThumbnails);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.SmallFileRevisionUploader = SmallFileRevisionUploader;
|
|
197
|
+
//# sourceMappingURL=smallFileUploader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smallFileUploader.js","sourceRoot":"","sources":["../../../src/internal/upload/smallFileUploader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const errors_1 = require("../../errors");
|
|
4
|
+
const interface_1 = require("../../interface");
|
|
5
|
+
const smallFileUploader_1 = require("./smallFileUploader");
|
|
6
|
+
const MOCK_BLOCK_HASH = new Uint8Array(32).fill(1);
|
|
7
|
+
const MOCK_VERIFICATION_TOKEN = new Uint8Array(16).fill(2);
|
|
8
|
+
function createStream(bytes) {
|
|
9
|
+
return new ReadableStream({
|
|
10
|
+
start(controller) {
|
|
11
|
+
controller.enqueue(new Uint8Array(bytes));
|
|
12
|
+
controller.close();
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function mockEncryptBlock(verifyBlock, _nodeKeys, block, _index) {
|
|
17
|
+
const encryptedData = new Uint8Array(block);
|
|
18
|
+
return (async () => {
|
|
19
|
+
await verifyBlock(encryptedData);
|
|
20
|
+
return {
|
|
21
|
+
index: 0,
|
|
22
|
+
encryptedData,
|
|
23
|
+
armoredSignature: 'mockBlockSignature',
|
|
24
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
25
|
+
originalSize: block.length,
|
|
26
|
+
encryptedSize: block.length + 100,
|
|
27
|
+
hash: 'blockHash',
|
|
28
|
+
hashPromise: Promise.resolve(MOCK_BLOCK_HASH),
|
|
29
|
+
};
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
describe('SmallFileUploader', () => {
|
|
33
|
+
let telemetry;
|
|
34
|
+
let apiService;
|
|
35
|
+
let cryptoService;
|
|
36
|
+
let uploadManager;
|
|
37
|
+
let metadata;
|
|
38
|
+
let onFinish;
|
|
39
|
+
let abortController;
|
|
40
|
+
const parentFolderUid = 'parentFolderUid';
|
|
41
|
+
const name = 'test-file.txt';
|
|
42
|
+
const mockNodeCrypto = {
|
|
43
|
+
nodeKeys: {
|
|
44
|
+
decrypted: { key: {} },
|
|
45
|
+
encrypted: {
|
|
46
|
+
armoredKey: 'armoredKey',
|
|
47
|
+
armoredPassphrase: 'armoredPassphrase',
|
|
48
|
+
armoredPassphraseSignature: 'armoredPassphraseSignature',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
contentKey: {
|
|
52
|
+
encrypted: {
|
|
53
|
+
contentKeyPacket: new Uint8Array(10),
|
|
54
|
+
base64ContentKeyPacket: 'base64ContentKeyPacket',
|
|
55
|
+
armoredContentKeyPacketSignature: 'armoredContentKeyPacketSignature',
|
|
56
|
+
},
|
|
57
|
+
decrypted: { contentKeyPacketSessionKey: {} },
|
|
58
|
+
},
|
|
59
|
+
encryptedNode: {
|
|
60
|
+
encryptedName: 'encryptedName',
|
|
61
|
+
hash: 'hash',
|
|
62
|
+
},
|
|
63
|
+
signingKeys: { email: 'test@test.com', addressId: 'addr', nameAndPassphraseSigningKey: {}, contentSigningKey: {} },
|
|
64
|
+
};
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
67
|
+
telemetry = {
|
|
68
|
+
getLoggerForRevision: jest.fn().mockReturnValue({
|
|
69
|
+
debug: jest.fn(),
|
|
70
|
+
info: jest.fn(),
|
|
71
|
+
warn: jest.fn(),
|
|
72
|
+
error: jest.fn(),
|
|
73
|
+
}),
|
|
74
|
+
getLoggerForSmallUpload: jest.fn().mockReturnValue({
|
|
75
|
+
debug: jest.fn(),
|
|
76
|
+
info: jest.fn(),
|
|
77
|
+
warn: jest.fn(),
|
|
78
|
+
error: jest.fn(),
|
|
79
|
+
}),
|
|
80
|
+
logBlockVerificationError: jest.fn(),
|
|
81
|
+
uploadFailed: jest.fn(),
|
|
82
|
+
uploadFinished: jest.fn(),
|
|
83
|
+
uploadInitFailed: jest.fn(),
|
|
84
|
+
};
|
|
85
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
86
|
+
apiService = {};
|
|
87
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
88
|
+
cryptoService = {
|
|
89
|
+
encryptThumbnail: jest.fn().mockImplementation(async (_nodeKeys, thumbnail) => ({
|
|
90
|
+
type: thumbnail.type,
|
|
91
|
+
encryptedData: new Uint8Array(thumbnail.thumbnail),
|
|
92
|
+
originalSize: thumbnail.thumbnail.length,
|
|
93
|
+
encryptedSize: thumbnail.thumbnail.length + 100,
|
|
94
|
+
hash: 'thumbnailHash',
|
|
95
|
+
})),
|
|
96
|
+
encryptBlock: jest.fn().mockImplementation(mockEncryptBlock),
|
|
97
|
+
verifyBlock: jest.fn().mockResolvedValue({ verificationToken: MOCK_VERIFICATION_TOKEN }),
|
|
98
|
+
commitFile: jest.fn().mockResolvedValue({
|
|
99
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
100
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
uploadManager = {
|
|
104
|
+
generateNewFileCrypto: jest.fn().mockResolvedValue(mockNodeCrypto),
|
|
105
|
+
uploadFile: jest.fn().mockResolvedValue({
|
|
106
|
+
nodeUid: 'nodeUid',
|
|
107
|
+
nodeRevisionUid: 'nodeRevisionUid',
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
metadata = {
|
|
111
|
+
expectedSize: 3,
|
|
112
|
+
mediaType: 'application/octet-stream',
|
|
113
|
+
};
|
|
114
|
+
onFinish = jest.fn();
|
|
115
|
+
abortController = new AbortController();
|
|
116
|
+
});
|
|
117
|
+
function createUploader() {
|
|
118
|
+
return new smallFileUploader_1.SmallFileUploader(telemetry, apiService, cryptoService, uploadManager, metadata, onFinish, abortController.signal, parentFolderUid, name);
|
|
119
|
+
}
|
|
120
|
+
describe('uploadFromStream', () => {
|
|
121
|
+
const thumbnails = [];
|
|
122
|
+
const onProgress = jest.fn();
|
|
123
|
+
it('should start upload and call manager.generateNewFileCrypto and manager.uploadFile', async () => {
|
|
124
|
+
const uploader = createUploader();
|
|
125
|
+
const stream = createStream([1, 2, 3]);
|
|
126
|
+
const controller = await uploader.uploadFromStream(stream, thumbnails, onProgress);
|
|
127
|
+
const result = await controller.completion();
|
|
128
|
+
expect(uploadManager.generateNewFileCrypto).toHaveBeenCalledWith(parentFolderUid, name);
|
|
129
|
+
expect(uploadManager.uploadFile).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
131
|
+
expect(onProgress).toHaveBeenCalledWith(metadata.expectedSize);
|
|
132
|
+
});
|
|
133
|
+
it('should throw if upload already started', async () => {
|
|
134
|
+
const uploader = createUploader();
|
|
135
|
+
const stream = createStream([1, 2, 3]);
|
|
136
|
+
await uploader.uploadFromStream(stream, thumbnails, onProgress);
|
|
137
|
+
await expect(uploader.uploadFromStream(stream, thumbnails, onProgress)).rejects.toThrow('Upload already started');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('buildPayloads (via upload flow)', () => {
|
|
141
|
+
it('should build commitPayload, encryptedBlock, and encryptedThumbnails from stream and pass to manager.uploadFile', async () => {
|
|
142
|
+
const uploader = createUploader();
|
|
143
|
+
const stream = createStream([1, 2, 3]);
|
|
144
|
+
const thumbnails = [
|
|
145
|
+
{ type: interface_1.ThumbnailType.Type1, thumbnail: new Uint8Array([10, 20]) },
|
|
146
|
+
{ type: interface_1.ThumbnailType.Type2, thumbnail: new Uint8Array([30, 40, 50]) },
|
|
147
|
+
];
|
|
148
|
+
await uploader.uploadFromStream(stream, thumbnails, undefined);
|
|
149
|
+
await uploader.controller.completion();
|
|
150
|
+
expect(uploadManager.uploadFile).toHaveBeenCalledWith(parentFolderUid, mockNodeCrypto, metadata, expect.objectContaining({
|
|
151
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
152
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
153
|
+
}), expect.objectContaining({
|
|
154
|
+
encryptedData: expect.any(Uint8Array),
|
|
155
|
+
armoredSignature: 'mockBlockSignature',
|
|
156
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
157
|
+
}), [
|
|
158
|
+
{ type: interface_1.ThumbnailType.Type1, encryptedData: expect.any(Uint8Array) },
|
|
159
|
+
{ type: interface_1.ThumbnailType.Type2, encryptedData: expect.any(Uint8Array) },
|
|
160
|
+
]);
|
|
161
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledTimes(1);
|
|
162
|
+
expect(cryptoService.encryptThumbnail).toHaveBeenCalledTimes(2);
|
|
163
|
+
expect(cryptoService.commitFile).toHaveBeenCalledWith(expect.anything(), MOCK_BLOCK_HASH, expect.any(String));
|
|
164
|
+
});
|
|
165
|
+
it('should pass encrypted block data matching stream content to crypto.encryptBlock', async () => {
|
|
166
|
+
const uploader = createUploader();
|
|
167
|
+
const content = [5, 6, 7, 8, 9];
|
|
168
|
+
metadata.expectedSize = content.length;
|
|
169
|
+
const stream = createStream(content);
|
|
170
|
+
await uploader.uploadFromStream(stream, [], undefined);
|
|
171
|
+
await uploader.controller.completion();
|
|
172
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledWith(expect.any(Function), expect.anything(), new Uint8Array(content), 0);
|
|
173
|
+
});
|
|
174
|
+
it('should pass each thumbnail to crypto.encryptThumbnail with nodeKeys', async () => {
|
|
175
|
+
const uploader = createUploader();
|
|
176
|
+
const thumbnails = [
|
|
177
|
+
{ type: interface_1.ThumbnailType.Type1, thumbnail: new Uint8Array([1]) },
|
|
178
|
+
];
|
|
179
|
+
const stream = createStream([1, 2, 3]);
|
|
180
|
+
await uploader.uploadFromStream(stream, thumbnails, undefined);
|
|
181
|
+
await uploader.controller.completion();
|
|
182
|
+
expect(cryptoService.encryptThumbnail).toHaveBeenCalledWith(expect.objectContaining({
|
|
183
|
+
key: mockNodeCrypto.nodeKeys.decrypted.key,
|
|
184
|
+
contentKeyPacket: mockNodeCrypto.contentKey.encrypted.contentKeyPacket,
|
|
185
|
+
contentKeyPacketSessionKey: mockNodeCrypto.contentKey.decrypted.contentKeyPacketSessionKey,
|
|
186
|
+
signingKeys: mockNodeCrypto.signingKeys,
|
|
187
|
+
}), { type: interface_1.ThumbnailType.Type1, thumbnail: new Uint8Array([1]) });
|
|
188
|
+
});
|
|
189
|
+
it('should call commitFile with manifest and extended attributes', async () => {
|
|
190
|
+
const uploader = createUploader();
|
|
191
|
+
const stream = createStream([1, 2, 3]);
|
|
192
|
+
await uploader.uploadFromStream(stream, [], undefined);
|
|
193
|
+
await uploader.controller.completion();
|
|
194
|
+
const [nodeKeys, manifest, extendedAttributes] = cryptoService.commitFile.mock.calls[0];
|
|
195
|
+
expect(manifest).toEqual(MOCK_BLOCK_HASH);
|
|
196
|
+
expect(extendedAttributes).toBeDefined();
|
|
197
|
+
expect(nodeKeys).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('stream integrity', () => {
|
|
201
|
+
it('should throw IntegrityError when stream size does not match expectedSize', async () => {
|
|
202
|
+
const uploader = createUploader();
|
|
203
|
+
metadata.expectedSize = 5;
|
|
204
|
+
const stream = createStream([1, 2, 3]); // only 3 bytes
|
|
205
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
206
|
+
await expect(controller.completion()).rejects.toThrow(errors_1.IntegrityError);
|
|
207
|
+
await expect(controller.completion()).rejects.toMatchObject({
|
|
208
|
+
debug: { actual: 3, expected: 5 },
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('should throw IntegrityError when stream sha1 does not match expectedSha1', async () => {
|
|
212
|
+
const uploader = createUploader();
|
|
213
|
+
metadata.expectedSha1 = 'a'.repeat(40); // wrong sha1
|
|
214
|
+
const stream = createStream([1, 2, 3]);
|
|
215
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
216
|
+
await expect(controller.completion()).rejects.toThrow(errors_1.IntegrityError);
|
|
217
|
+
await expect(controller.completion()).rejects.toMatchObject({
|
|
218
|
+
debug: expect.objectContaining({
|
|
219
|
+
expectedSha1: 'a'.repeat(40),
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('zero-byte file', () => {
|
|
225
|
+
it('should upload zero-byte file without calling encryptBlock and pass undefined block to manager.uploadFile', async () => {
|
|
226
|
+
metadata.expectedSize = 0;
|
|
227
|
+
const uploader = createUploader();
|
|
228
|
+
const stream = createStream([]);
|
|
229
|
+
const onProgress = jest.fn();
|
|
230
|
+
const controller = await uploader.uploadFromStream(stream, [], onProgress);
|
|
231
|
+
const result = await controller.completion();
|
|
232
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
233
|
+
expect(cryptoService.encryptBlock).not.toHaveBeenCalled();
|
|
234
|
+
expect(uploadManager.uploadFile).toHaveBeenCalledWith(parentFolderUid, mockNodeCrypto, metadata, expect.objectContaining({
|
|
235
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
236
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
237
|
+
}), undefined, []);
|
|
238
|
+
expect(cryptoService.commitFile).toHaveBeenCalledWith(expect.anything(), new Uint8Array(0), expect.any(String));
|
|
239
|
+
expect(onFinish).toHaveBeenCalled();
|
|
240
|
+
expect(onProgress).toHaveBeenCalledWith(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('SmallFileRevisionUploader', () => {
|
|
245
|
+
let telemetry;
|
|
246
|
+
let apiService;
|
|
247
|
+
let cryptoService;
|
|
248
|
+
let uploadManager;
|
|
249
|
+
let metadata;
|
|
250
|
+
let onFinish;
|
|
251
|
+
let abortController;
|
|
252
|
+
const nodeUid = 'nodeUid';
|
|
253
|
+
const mockNodeKeys = {
|
|
254
|
+
key: {},
|
|
255
|
+
contentKeyPacket: new Uint8Array(10),
|
|
256
|
+
contentKeyPacketSessionKey: {},
|
|
257
|
+
signingKeys: { email: 'test@test.com', addressId: 'addr', nameAndPassphraseSigningKey: {}, contentSigningKey: {} },
|
|
258
|
+
};
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
261
|
+
telemetry = {
|
|
262
|
+
getLoggerForRevision: jest.fn().mockReturnValue({
|
|
263
|
+
debug: jest.fn(),
|
|
264
|
+
info: jest.fn(),
|
|
265
|
+
warn: jest.fn(),
|
|
266
|
+
error: jest.fn(),
|
|
267
|
+
}),
|
|
268
|
+
getLoggerForSmallUpload: jest.fn().mockReturnValue({
|
|
269
|
+
debug: jest.fn(),
|
|
270
|
+
info: jest.fn(),
|
|
271
|
+
warn: jest.fn(),
|
|
272
|
+
error: jest.fn(),
|
|
273
|
+
}),
|
|
274
|
+
logBlockVerificationError: jest.fn(),
|
|
275
|
+
uploadFailed: jest.fn(),
|
|
276
|
+
uploadFinished: jest.fn(),
|
|
277
|
+
uploadInitFailed: jest.fn(),
|
|
278
|
+
};
|
|
279
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
280
|
+
apiService = {};
|
|
281
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
282
|
+
cryptoService = {
|
|
283
|
+
encryptThumbnail: jest.fn().mockImplementation(async (_nodeKeys, thumbnail) => ({
|
|
284
|
+
type: thumbnail.type,
|
|
285
|
+
encryptedData: new Uint8Array(thumbnail.thumbnail),
|
|
286
|
+
originalSize: thumbnail.thumbnail.length,
|
|
287
|
+
encryptedSize: thumbnail.thumbnail.length + 100,
|
|
288
|
+
hash: 'thumbnailHash',
|
|
289
|
+
})),
|
|
290
|
+
encryptBlock: jest.fn().mockImplementation(async (verifyBlock, _, block) => {
|
|
291
|
+
await verifyBlock(block);
|
|
292
|
+
return {
|
|
293
|
+
index: 0,
|
|
294
|
+
encryptedData: block,
|
|
295
|
+
armoredSignature: 'mockBlockSignature',
|
|
296
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
297
|
+
originalSize: block.length,
|
|
298
|
+
encryptedSize: block.length + 100,
|
|
299
|
+
hash: 'blockHash',
|
|
300
|
+
hashPromise: Promise.resolve(MOCK_BLOCK_HASH),
|
|
301
|
+
};
|
|
302
|
+
}),
|
|
303
|
+
verifyBlock: jest.fn().mockResolvedValue({ verificationToken: MOCK_VERIFICATION_TOKEN }),
|
|
304
|
+
commitFile: jest.fn().mockResolvedValue({
|
|
305
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
306
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
307
|
+
}),
|
|
308
|
+
};
|
|
309
|
+
uploadManager = {
|
|
310
|
+
getExistingFileNodeCrypto: jest.fn().mockResolvedValue(mockNodeKeys),
|
|
311
|
+
uploadSmallRevision: jest.fn().mockResolvedValue({
|
|
312
|
+
nodeUid: 'nodeUid',
|
|
313
|
+
nodeRevisionUid: 'nodeRevisionUid',
|
|
314
|
+
}),
|
|
315
|
+
};
|
|
316
|
+
metadata = {
|
|
317
|
+
expectedSize: 3,
|
|
318
|
+
mediaType: 'application/octet-stream',
|
|
319
|
+
};
|
|
320
|
+
onFinish = jest.fn();
|
|
321
|
+
abortController = new AbortController();
|
|
322
|
+
});
|
|
323
|
+
function createUploader() {
|
|
324
|
+
return new smallFileUploader_1.SmallFileRevisionUploader(telemetry, apiService, cryptoService, uploadManager, metadata, onFinish, abortController.signal, nodeUid);
|
|
325
|
+
}
|
|
326
|
+
it('should get node crypto, build payloads, and call uploadSmallRevision', async () => {
|
|
327
|
+
const uploader = createUploader();
|
|
328
|
+
const stream = createStream([1, 2, 3]);
|
|
329
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
330
|
+
const result = await controller.completion();
|
|
331
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
332
|
+
expect(cryptoService.encryptBlock).toHaveBeenCalledWith(expect.any(Function), expect.anything(), Uint8Array.from([1, 2, 3]), 0);
|
|
333
|
+
expect(uploadManager.getExistingFileNodeCrypto).toHaveBeenCalledWith(nodeUid);
|
|
334
|
+
expect(uploadManager.uploadSmallRevision).toHaveBeenCalledWith(nodeUid, mockNodeKeys, expect.objectContaining({
|
|
335
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
336
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
337
|
+
}), expect.objectContaining({
|
|
338
|
+
encryptedData: expect.any(Uint8Array),
|
|
339
|
+
armoredSignature: 'mockBlockSignature',
|
|
340
|
+
verificationToken: MOCK_VERIFICATION_TOKEN,
|
|
341
|
+
}), []);
|
|
342
|
+
});
|
|
343
|
+
it('should upload zero-byte revision without calling encryptBlock and pass undefined block to uploadSmallRevision', async () => {
|
|
344
|
+
metadata.expectedSize = 0;
|
|
345
|
+
const uploader = createUploader();
|
|
346
|
+
const stream = createStream([]);
|
|
347
|
+
const controller = await uploader.uploadFromStream(stream, [], undefined);
|
|
348
|
+
const result = await controller.completion();
|
|
349
|
+
expect(result).toEqual({ nodeUid: 'nodeUid', nodeRevisionUid: 'nodeRevisionUid' });
|
|
350
|
+
expect(cryptoService.encryptBlock).not.toHaveBeenCalled();
|
|
351
|
+
expect(uploadManager.uploadSmallRevision).toHaveBeenCalledWith(nodeUid, mockNodeKeys, expect.objectContaining({
|
|
352
|
+
armoredManifestSignature: 'mockManifestSignature',
|
|
353
|
+
armoredExtendedAttributes: 'mockExtendedAttributes',
|
|
354
|
+
}), undefined, []);
|
|
355
|
+
expect(cryptoService.commitFile).toHaveBeenCalledWith(expect.anything(), new Uint8Array(0), expect.any(String));
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
//# sourceMappingURL=smallFileUploader.test.js.map
|