@eluvio/elv-client-js 4.2.27 → 4.2.29
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/package.json +1 -1
- package/src/EthClient.js +2 -1
- package/src/client/ContentAccess.js +70 -1
- package/src/client/Files.js +413 -208
- package/src/client/LiveStream.js +32 -16
- package/utilities/MigrateEncryptionConkForNewUser.js +62 -0
- package/utilities/ObjectAddFiles.js +29 -9
- package/utilities/lib/concerns/CloudFile.js +2 -1
- package/utilities/lib/concerns/LocalFile.js +2 -1
package/package.json
CHANGED
package/src/EthClient.js
CHANGED
|
@@ -309,6 +309,7 @@ class EthClient {
|
|
|
309
309
|
methodName,
|
|
310
310
|
methodArgs,
|
|
311
311
|
value,
|
|
312
|
+
overrides={},
|
|
312
313
|
timeout,
|
|
313
314
|
formatArguments=true,
|
|
314
315
|
cacheContract=true,
|
|
@@ -319,7 +320,6 @@ class EthClient {
|
|
|
319
320
|
if(!abi) { abi = await this.client.ContractAbi({contractAddress}); }
|
|
320
321
|
|
|
321
322
|
const contract = this.Contract({contractAddress, abi, cacheContract, overrideCachedContract});
|
|
322
|
-
|
|
323
323
|
// Make method call
|
|
324
324
|
const createMethodCall = await this.CallContractMethod({
|
|
325
325
|
contract,
|
|
@@ -327,6 +327,7 @@ class EthClient {
|
|
|
327
327
|
methodName,
|
|
328
328
|
methodArgs,
|
|
329
329
|
value,
|
|
330
|
+
overrides,
|
|
330
331
|
formatArguments,
|
|
331
332
|
cacheContract
|
|
332
333
|
});
|
|
@@ -9,6 +9,7 @@ const objectPath = require("object-path");
|
|
|
9
9
|
|
|
10
10
|
const HttpClient = require("../HttpClient");
|
|
11
11
|
const ContentObjectAudit = require("../ContentObjectAudit");
|
|
12
|
+
const bs58 = require("bs58");
|
|
12
13
|
|
|
13
14
|
const {
|
|
14
15
|
ValidateLibrary,
|
|
@@ -17,6 +18,7 @@ const {
|
|
|
17
18
|
ValidatePartHash,
|
|
18
19
|
ValidateWriteToken,
|
|
19
20
|
ValidateParameters,
|
|
21
|
+
ValidatePresence,
|
|
20
22
|
} = require("../Validation");
|
|
21
23
|
|
|
22
24
|
const MergeWith = require("lodash/mergeWith");
|
|
@@ -2713,7 +2715,6 @@ exports.LinkData = async function({libraryId, objectId, versionHash, writeToken,
|
|
|
2713
2715
|
);
|
|
2714
2716
|
};
|
|
2715
2717
|
|
|
2716
|
-
|
|
2717
2718
|
/* Encryption */
|
|
2718
2719
|
|
|
2719
2720
|
exports.CreateEncryptionConk = async function({libraryId, objectId, versionHash, writeToken, createKMSConk=true}) {
|
|
@@ -2793,6 +2794,74 @@ exports.CreateEncryptionConk = async function({libraryId, objectId, versionHash,
|
|
|
2793
2794
|
return this.encryptionConks[objectId];
|
|
2794
2795
|
};
|
|
2795
2796
|
|
|
2797
|
+
/**
|
|
2798
|
+
* Encrypt the owner encryption key using the new user details provided
|
|
2799
|
+
*
|
|
2800
|
+
* @methodGroup Encryption
|
|
2801
|
+
* @namedParams
|
|
2802
|
+
* @param {string=} libraryId - ID of a library
|
|
2803
|
+
* @param {string=} objectId - ID of an object
|
|
2804
|
+
* @param {string=} versionHash - Hash of an object version
|
|
2805
|
+
* @param {string=} writeToken - The write token for the object
|
|
2806
|
+
* @param {Object=} newEncodedUserPublicKey - raw public key or base-58 encoded publicKey of the new user prefixed 'kupk'
|
|
2807
|
+
*/
|
|
2808
|
+
exports.MigrateEncryptionConkForUserProvided = async function({libraryId, objectId, versionHash, writeToken, newEncodedUserPublicKey}) {
|
|
2809
|
+
if(this.signer.remoteSigner) {
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
ValidateParameters({libraryId, objectId, versionHash});
|
|
2814
|
+
ValidateWriteToken(writeToken);
|
|
2815
|
+
|
|
2816
|
+
if(!objectId) {
|
|
2817
|
+
objectId = this.utils.DecodeVersionHash(versionHash).objectId;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
ValidatePresence("newEncodedUserPublicKey", newEncodedUserPublicKey);
|
|
2821
|
+
|
|
2822
|
+
let publicKey;
|
|
2823
|
+
if(!newEncodedUserPublicKey.startsWith("kupk")){
|
|
2824
|
+
publicKey = newEncodedUserPublicKey;
|
|
2825
|
+
} else {
|
|
2826
|
+
const publicKeyBytes = bs58.decode(newEncodedUserPublicKey.slice(4));
|
|
2827
|
+
if(publicKeyBytes.length !== 65) {
|
|
2828
|
+
publicKey = "0x" + Buffer.concat([Buffer.from([0x04]), publicKeyBytes]).toString("hex");
|
|
2829
|
+
} else {
|
|
2830
|
+
publicKey = "0x" + publicKeyBytes.toString("hex");
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Decrypt using existing user key
|
|
2835
|
+
const existingCapKey = `eluv.caps.iusr${this.utils.AddressToHash(this.signer.address)}`;
|
|
2836
|
+
const existingUserCap =
|
|
2837
|
+
await this.ContentObjectMetadata({
|
|
2838
|
+
libraryId,
|
|
2839
|
+
objectId,
|
|
2840
|
+
writeToken,
|
|
2841
|
+
metadataSubtree: existingCapKey
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
if(!existingUserCap) {
|
|
2845
|
+
throw (`No encryption conk present for ${objectId} for user ${this.signer.address}`);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
this.encryptionConks[objectId] = await this.Crypto.DecryptCap(existingUserCap, this.signer._signingKey().privateKey);
|
|
2849
|
+
|
|
2850
|
+
// Encrypt with the new key
|
|
2851
|
+
const encryptedConk = await this.Crypto.EncryptConk(this.encryptionConks[objectId], publicKey);
|
|
2852
|
+
const newUserCap = `eluv.caps.iusr${this.utils.AddressToHash(this.utils.PublicKeyToAddress(publicKey))}`;
|
|
2853
|
+
|
|
2854
|
+
await this.ReplaceMetadata({
|
|
2855
|
+
libraryId,
|
|
2856
|
+
objectId,
|
|
2857
|
+
writeToken,
|
|
2858
|
+
metadataSubtree: newUserCap,
|
|
2859
|
+
metadata: encryptedConk
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
return this.encryptionConks[objectId];
|
|
2863
|
+
};
|
|
2864
|
+
|
|
2796
2865
|
/**
|
|
2797
2866
|
* Retrieve the encryption conk for the specified object. If one has not yet been created
|
|
2798
2867
|
* and a writeToken has been specified, this method will create a new conk and
|
package/src/client/Files.js
CHANGED
|
@@ -23,6 +23,13 @@ const {
|
|
|
23
23
|
ValidateParameters
|
|
24
24
|
} = require("../Validation");
|
|
25
25
|
|
|
26
|
+
const JobStatus = Object.freeze({
|
|
27
|
+
STOPPED: "STOPPED",
|
|
28
|
+
FAILED: "FAILED",
|
|
29
|
+
IN_PROGRESS:"IN_PROGRESS",
|
|
30
|
+
COMPLETED: "COMPLETED"
|
|
31
|
+
});
|
|
32
|
+
|
|
26
33
|
|
|
27
34
|
/* Files */
|
|
28
35
|
|
|
@@ -52,8 +59,9 @@ exports.ListFiles = async function({libraryId, objectId, path = "", versionHash,
|
|
|
52
59
|
});
|
|
53
60
|
};
|
|
54
61
|
|
|
62
|
+
|
|
55
63
|
/**
|
|
56
|
-
*
|
|
64
|
+
* Upload or resume copy/reference files from S3 to a content object
|
|
57
65
|
*
|
|
58
66
|
* S3 authentication is done by either providing an access key and secret or a presigned URL. The credentials will not be stored (neither in the client nor in the Fabric)
|
|
59
67
|
*
|
|
@@ -61,12 +69,12 @@ exports.ListFiles = async function({libraryId, objectId, path = "", versionHash,
|
|
|
61
69
|
*
|
|
62
70
|
* Expected format of fileInfo:
|
|
63
71
|
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
[
|
|
73
|
+
{
|
|
74
|
+
path: string,
|
|
75
|
+
source: string // either a full path e.g. "s3://BUCKET_NAME/path..." or just the path part without "s3://BUCKET_NAME/"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
70
78
|
*
|
|
71
79
|
* @memberof module:ElvClient/Files+Parts
|
|
72
80
|
* @methodGroup Files
|
|
@@ -82,6 +90,7 @@ exports.ListFiles = async function({libraryId, objectId, path = "", versionHash,
|
|
|
82
90
|
* @param {string=} signedUrl
|
|
83
91
|
* @param {string} encryption="none" - Encryption for uploaded files (copy only) - cgck | none
|
|
84
92
|
* @param {boolean} copy=false - If true, will copy the data from S3 into the fabric. Otherwise, a reference to the content will be made.
|
|
93
|
+
* @param {boolean} resume=false - If true, will resume the file jobs stopped
|
|
85
94
|
* @param {function=} callback - If specified, will be periodically called with current upload status
|
|
86
95
|
* - Arguments (copy): { done: boolean, uploaded: number, total: number, uploadedFiles: number, totalFiles: number, fileStatus: Object }
|
|
87
96
|
* - Arguments (reference): { done: boolean, uploadedFiles: number, totalFiles: number }
|
|
@@ -96,61 +105,41 @@ exports.UploadFilesFromS3 = async function({
|
|
|
96
105
|
accessKey,
|
|
97
106
|
secret,
|
|
98
107
|
signedUrl,
|
|
99
|
-
encryption="none",
|
|
100
|
-
copy=false,
|
|
108
|
+
encryption = "none",
|
|
109
|
+
copy = false,
|
|
110
|
+
resume = false,
|
|
101
111
|
callback
|
|
102
112
|
}) {
|
|
103
|
-
ValidateParameters({libraryId, objectId});
|
|
113
|
+
ValidateParameters({ libraryId, objectId });
|
|
104
114
|
ValidateWriteToken(writeToken);
|
|
105
115
|
|
|
106
116
|
const s3prefixRegex = /^s3:\/\/([^/]+)\//i; // for matching and extracting bucket name when full s3:// path is specified
|
|
107
117
|
// if fileInfo source paths start with s3://bucketName/, check against bucket arg passed in, and strip
|
|
108
118
|
for(let i = 0; i < fileInfo.length; i++) {
|
|
109
119
|
const fileSourcePath = fileInfo[i].source;
|
|
110
|
-
const s3prefixMatch =
|
|
120
|
+
const s3prefixMatch = s3prefixRegex.exec(fileSourcePath);
|
|
111
121
|
if(s3prefixMatch) {
|
|
112
122
|
const bucketName = s3prefixMatch[1];
|
|
113
123
|
if(bucketName !== bucket) {
|
|
114
124
|
throw Error("Full S3 file path \"" + fileSourcePath + "\" specified, but does not match provided bucket name '" + bucket + "'");
|
|
115
|
-
} else {
|
|
116
|
-
// strip prefix
|
|
117
|
-
fileInfo[i].source = fileSourcePath.replace(s3prefixRegex,"");
|
|
118
125
|
}
|
|
126
|
+
// strip prefix
|
|
127
|
+
fileInfo[i].source = fileSourcePath.replace(s3prefixRegex, "");
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
this.Log(`Copying files from S3: ${libraryId} ${objectId} ${writeToken}`);
|
|
124
|
-
} else {
|
|
125
|
-
this.Log(`Adding links to files in S3: ${libraryId} ${objectId} ${writeToken}`);
|
|
126
|
-
}
|
|
131
|
+
this.Log(`${resume ? "Resuming" : (copy ? "Copying" : "Referencing")} files from S3: ${libraryId} ${objectId} ${writeToken}`);
|
|
127
132
|
|
|
128
133
|
let encryption_key;
|
|
129
134
|
if(encryption === "cgck") {
|
|
130
|
-
let conk = await this.EncryptionConk({
|
|
131
|
-
|
|
132
|
-
objectId,
|
|
133
|
-
writeToken
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
conk = {
|
|
137
|
-
...conk,
|
|
138
|
-
secret_key: ""
|
|
139
|
-
};
|
|
140
|
-
|
|
135
|
+
let conk = await this.EncryptionConk({ libraryId, objectId, writeToken });
|
|
136
|
+
conk = { ...conk, secret_key: "" };
|
|
141
137
|
encryption_key = `kp__${this.utils.B58(Buffer.from(JSON.stringify(conk)))}`;
|
|
142
138
|
}
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
secret_access_key: secret
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
if(signedUrl) {
|
|
150
|
-
cloudCredentials = {
|
|
151
|
-
signed_url: signedUrl
|
|
152
|
-
};
|
|
153
|
-
}
|
|
140
|
+
const cloudCredentials = signedUrl
|
|
141
|
+
? { signed_url: signedUrl }
|
|
142
|
+
: { access_key_id: accessKey, secret_access_key: secret };
|
|
154
143
|
|
|
155
144
|
const remoteAccess = {
|
|
156
145
|
protocol: "s3",
|
|
@@ -191,55 +180,94 @@ exports.UploadFilesFromS3 = async function({
|
|
|
191
180
|
}
|
|
192
181
|
});
|
|
193
182
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
183
|
+
const trackUploadStatus = async ({ uploadId }) => {
|
|
184
|
+
// eslint-disable-next-line no-constant-condition
|
|
185
|
+
while(true) {
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
187
|
+
const status = await this.UploadStatus({ libraryId, objectId, writeToken, uploadId });
|
|
188
|
+
|
|
189
|
+
if(status.errors && status.errors.length >= 1) {
|
|
190
|
+
throw new Error(status.errors.join("\n"));
|
|
191
|
+
} else if(status.error) {
|
|
192
|
+
this.Log(`S3 file upload failed:\n${JSON.stringify(status, null, 2)}`);
|
|
193
|
+
throw new Error(status.error);
|
|
194
|
+
} else if(status.status.toLowerCase() === "failed") {
|
|
195
|
+
throw new Error("File upload failed");
|
|
196
|
+
}
|
|
200
197
|
|
|
201
|
-
|
|
198
|
+
let done = false;
|
|
199
|
+
if(copy) {
|
|
200
|
+
done = status.ingest_copy.done;
|
|
201
|
+
if(callback) {
|
|
202
|
+
const progress = status.ingest_copy.progress;
|
|
203
|
+
|
|
204
|
+
callback({
|
|
205
|
+
done,
|
|
206
|
+
uploaded: progress.bytes.completed,
|
|
207
|
+
total: progress.bytes.total,
|
|
208
|
+
uploadedFiles: progress.files.completed,
|
|
209
|
+
totalFiles: progress.files.total,
|
|
210
|
+
fileStatus: progress.files.details
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
done = status.add_reference.done;
|
|
215
|
+
if(callback) {
|
|
216
|
+
const progress = status.add_reference.progress;
|
|
217
|
+
|
|
218
|
+
callback({
|
|
219
|
+
done,
|
|
220
|
+
uploadedFiles: progress.completed,
|
|
221
|
+
totalFiles: progress.total,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
202
225
|
|
|
203
|
-
|
|
204
|
-
throw status.errors.join("\n");
|
|
205
|
-
} else if(status.error) {
|
|
206
|
-
this.Log(`S3 file upload failed:\n${JSON.stringify(status, null, 2)}`);
|
|
207
|
-
throw status.error;
|
|
208
|
-
} else if(status.status.toLowerCase() === "failed") {
|
|
209
|
-
throw "File upload failed";
|
|
226
|
+
if(done) break;
|
|
210
227
|
}
|
|
228
|
+
};
|
|
211
229
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
callback({
|
|
220
|
-
done,
|
|
221
|
-
uploaded: progress.bytes.completed,
|
|
222
|
-
total: progress.bytes.total,
|
|
223
|
-
uploadedFiles: progress.files.completed,
|
|
224
|
-
totalFiles: progress.files.total,
|
|
225
|
-
fileStatus: progress.files.details
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
done = status.add_reference.done;
|
|
230
|
+
if(resume) {
|
|
231
|
+
const jobIds = await this.ListFilesJob({ libraryId, objectId, writeToken, encryption });
|
|
232
|
+
const jobsByStatus = jobIds.reduce((acc, job) => {
|
|
233
|
+
acc[job.status] = acc[job.status] || new Map();
|
|
234
|
+
acc[job.status].set(job.id, true);
|
|
235
|
+
return acc;
|
|
236
|
+
}, {});
|
|
230
237
|
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
const resumeAndTrack = async (jobIds) => {
|
|
239
|
+
const responses = await this.ResumeFileUploadJob({
|
|
240
|
+
libraryId,
|
|
241
|
+
objectId,
|
|
242
|
+
writeToken,
|
|
243
|
+
ops,
|
|
244
|
+
defaults,
|
|
245
|
+
encryption,
|
|
246
|
+
stoppedOrFailedJobIds: jobIds
|
|
247
|
+
});
|
|
248
|
+
await this.utils.LimitedMap(5, responses, async res => {
|
|
249
|
+
this.Log(`Tracking resumed job id: ${res.id}`);
|
|
250
|
+
await trackUploadStatus({ uploadId: res.id });
|
|
251
|
+
});
|
|
252
|
+
};
|
|
233
253
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
totalFiles: progress.total,
|
|
238
|
-
});
|
|
254
|
+
for(const status of [JobStatus.STOPPED, JobStatus.FAILED]) {
|
|
255
|
+
if(jobsByStatus[status]){
|
|
256
|
+
await resumeAndTrack([...jobsByStatus[status].keys()]);
|
|
239
257
|
}
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
const trackOnlyEntries = Object.entries(jobsByStatus)
|
|
261
|
+
.filter(([status]) => [JobStatus.IN_PROGRESS, JobStatus.COMPLETED].includes(status))
|
|
262
|
+
.flatMap(([status, jobs]) => [...jobs.keys()].map(jobId => ({ status, jobId })));
|
|
263
|
+
|
|
264
|
+
await this.utils.LimitedMap(5, trackOnlyEntries, async ({ status, jobId }) => {
|
|
265
|
+
this.Log(`Tracking ${status.toLowerCase()} job id: ${jobId}`);
|
|
266
|
+
await trackUploadStatus({ uploadId: jobId });
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
const { id: uploadId } = await this.CreateFileUploadJob({ libraryId, objectId, writeToken, ops, defaults });
|
|
270
|
+
await trackUploadStatus({ uploadId });
|
|
243
271
|
}
|
|
244
272
|
};
|
|
245
273
|
|
|
@@ -248,14 +276,14 @@ exports.UploadFilesFromS3 = async function({
|
|
|
248
276
|
*
|
|
249
277
|
* Expected format of fileInfo:
|
|
250
278
|
*
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
279
|
+
[
|
|
280
|
+
{
|
|
281
|
+
path: string,
|
|
282
|
+
mime_type: string,
|
|
283
|
+
size: number,
|
|
284
|
+
data: File | ArrayBuffer | Buffer | File Descriptor (Node)
|
|
285
|
+
}
|
|
286
|
+
]
|
|
259
287
|
*
|
|
260
288
|
*
|
|
261
289
|
* @memberof module:ElvClient/Files+Parts
|
|
@@ -265,11 +293,12 @@ exports.UploadFilesFromS3 = async function({
|
|
|
265
293
|
* @param {string} objectId - ID of the object
|
|
266
294
|
* @param {string} writeToken - Write token of the draft
|
|
267
295
|
* @param {Array<object>} fileInfo - List of files to upload, including their size, type, and contents
|
|
296
|
+
* @param {string} resume - Resume the upload jobs
|
|
268
297
|
* @param {string} encryption="none" - Encryption for uploaded files - cgck | none
|
|
269
298
|
* @param {function=} callback - If specified, will be called after each job segment is finished with the current upload progress
|
|
270
299
|
* - Format: {"filename1": {uploaded: number, total: number}, ...}
|
|
271
300
|
*/
|
|
272
|
-
exports.UploadFiles = async function({libraryId, objectId, writeToken, fileInfo, encryption="none", callback}) {
|
|
301
|
+
exports.UploadFiles = async function({libraryId, objectId, writeToken, fileInfo, resume=false,encryption="none", callback}) {
|
|
273
302
|
ValidateParameters({libraryId, objectId});
|
|
274
303
|
ValidateWriteToken(writeToken);
|
|
275
304
|
ValidatePresence("fileInfo", fileInfo);
|
|
@@ -312,160 +341,253 @@ exports.UploadFiles = async function({libraryId, objectId, writeToken, fileInfo,
|
|
|
312
341
|
|
|
313
342
|
this.Log(fileInfo);
|
|
314
343
|
|
|
315
|
-
|
|
344
|
+
// On a fresh upload nothing has happened yet, so 0% is accurate. On resume, the real starting
|
|
345
|
+
// point isn't known until job status is fetched below, so skip this to avoid a misleading
|
|
346
|
+
// initial 0% line ahead of the actual (possibly much higher) baseline.
|
|
347
|
+
if(callback && !resume) {
|
|
316
348
|
callback(progress);
|
|
317
349
|
}
|
|
318
350
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
objectId,
|
|
322
|
-
writeToken,
|
|
323
|
-
ops: fileInfo,
|
|
324
|
-
encryption
|
|
325
|
-
});
|
|
351
|
+
let idJobMap = new Map();
|
|
352
|
+
if(resume) {
|
|
353
|
+
const ids = await this.ListFilesJob({ libraryId, objectId, writeToken, encryption });
|
|
326
354
|
|
|
327
|
-
|
|
328
|
-
|
|
355
|
+
const jobsByStatus = ids.reduce((acc, job) => {
|
|
356
|
+
acc[job.status] = acc[job.status] || [];
|
|
357
|
+
acc[job.status].push(job.id);
|
|
358
|
+
return acc;
|
|
359
|
+
}, {});
|
|
329
360
|
|
|
330
|
-
|
|
331
|
-
|
|
361
|
+
const stoppedJobIds = jobsByStatus.STOPPED || [];
|
|
362
|
+
const failedJobIds = jobsByStatus.FAILED || [];
|
|
363
|
+
const inProgressJobIds = jobsByStatus.IN_PROGRESS || [];
|
|
332
364
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
365
|
+
const resumeJobIds = [...stoppedJobIds, ...failedJobIds, ...inProgressJobIds];
|
|
366
|
+
if(resumeJobIds.length === 0) {
|
|
367
|
+
this.Log("No job Ids to resume");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
336
370
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
371
|
+
const resumeJobs = async(jobs) => {
|
|
372
|
+
await this.ResumeFileUploadJob({
|
|
373
|
+
libraryId,
|
|
374
|
+
objectId,
|
|
375
|
+
writeToken,
|
|
376
|
+
ops: fileInfo,
|
|
377
|
+
encryption,
|
|
378
|
+
stoppedOrFailedJobIds: jobs
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// resume stopped and failed jobs
|
|
383
|
+
for(const jobsToResume of [stoppedJobIds, failedJobIds]){
|
|
384
|
+
if(jobsToResume.length > 0){
|
|
385
|
+
await resumeJobs(jobsToResume);
|
|
343
386
|
}
|
|
387
|
+
}
|
|
344
388
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
389
|
+
// for each uploadId, make a map containing jobIds
|
|
390
|
+
const uploadJobsResults = await this.utils.LimitedMap(5, resumeJobIds, async uploadId => {
|
|
391
|
+
const result = await this.ListFilesUploadJobs({
|
|
348
392
|
libraryId,
|
|
349
393
|
objectId,
|
|
350
394
|
writeToken,
|
|
351
|
-
|
|
352
|
-
|
|
395
|
+
jobId: uploadId,
|
|
396
|
+
encryption
|
|
353
397
|
});
|
|
398
|
+
return result.jobs;
|
|
399
|
+
});
|
|
400
|
+
resumeJobIds.forEach((uploadId, i) => idJobMap.set(uploadId, uploadJobsResults[i]));
|
|
401
|
+
|
|
402
|
+
} else {
|
|
403
|
+
const jobResponse = await this.CreateFileUploadJob({
|
|
404
|
+
libraryId,
|
|
405
|
+
objectId,
|
|
406
|
+
writeToken,
|
|
407
|
+
ops: fileInfo,
|
|
408
|
+
encryption
|
|
409
|
+
});
|
|
410
|
+
idJobMap.set(jobResponse.id, jobResponse.jobs);
|
|
411
|
+
}
|
|
354
412
|
|
|
355
|
-
|
|
356
|
-
|
|
413
|
+
// How far encryption can get ahead of upload (500 MB)
|
|
414
|
+
const bufferSize = 500 * 1024 * 1024;
|
|
357
415
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
416
|
+
// Process each uploadId and its jobs
|
|
417
|
+
for(const [uploadId, jobs] of idJobMap.entries()) {
|
|
418
|
+
this.Log(`Upload ID: ${uploadId}`);
|
|
419
|
+
this.Log(jobs.length);
|
|
420
|
+
|
|
421
|
+
// Each upload ID gets its own counters
|
|
422
|
+
let prepared = 0;
|
|
423
|
+
let uploaded = 0;
|
|
424
|
+
let jobSpecs = [];
|
|
425
|
+
|
|
426
|
+
// `${jobId}:${path}` pairs already reflected in progress by the resume baseline scan below,
|
|
427
|
+
// so the normal per-chunk completion credit further down doesn't double-count them
|
|
428
|
+
// It stores the jobId whose remaining bytes = 0
|
|
429
|
+
let alreadyCreditedKeys = new Set();
|
|
430
|
+
|
|
431
|
+
if(resume && callback) {
|
|
432
|
+
// Establish the true resume baseline across ALL chunks up front, before showing any
|
|
433
|
+
// progress. Crediting chunks one at a time as PrepareJobs reaches them (sequentially, each
|
|
434
|
+
// with its own round trip) would make an already-mostly-done file look like it's slowly
|
|
435
|
+
// re-climbing from scratch instead of immediately reflecting what's already been received.
|
|
436
|
+
await this.utils.LimitedMap(5, jobs, async jobId => {
|
|
437
|
+
const job = await this.UploadJobStatus({ libraryId, objectId, writeToken, uploadId, jobId });
|
|
438
|
+
|
|
439
|
+
for(const fileInfo of job.files) {
|
|
440
|
+
// When encrypted, the top-level rem/skip are sentinel (-1) - the real, already-received
|
|
441
|
+
// byte counts for this chunk are under `encrypted` (same lookup UploadFileData uses)
|
|
442
|
+
const fileStatus = (encryption && encryption !== "none") ? fileInfo.encrypted : fileInfo;
|
|
443
|
+
if(fileStatus.rem === 0) {
|
|
444
|
+
alreadyCreditedKeys.add(`${jobId}:${fileInfo.path}`);
|
|
445
|
+
progress[fileInfo.path] = {
|
|
446
|
+
...progress[fileInfo.path],
|
|
447
|
+
uploaded: progress[fileInfo.path].uploaded + fileInfo.len
|
|
448
|
+
};
|
|
449
|
+
}
|
|
366
450
|
}
|
|
451
|
+
});
|
|
367
452
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
453
|
+
callback(progress);
|
|
454
|
+
}
|
|
371
455
|
|
|
372
|
-
|
|
456
|
+
// Insert the data to upload into the job spec, encrypting if necessary
|
|
457
|
+
const PrepareJobs = async () => {
|
|
458
|
+
for(let j = 0; j < jobs.length; j++) {
|
|
459
|
+
while(prepared - uploaded > bufferSize) {
|
|
460
|
+
// Wait for more data to be uploaded
|
|
461
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
462
|
+
}
|
|
373
463
|
|
|
374
|
-
|
|
375
|
-
|
|
464
|
+
// Retrieve job info
|
|
465
|
+
const jobId = jobs[j];
|
|
466
|
+
let job = await this.UploadJobStatus({
|
|
467
|
+
libraryId,
|
|
468
|
+
objectId,
|
|
469
|
+
writeToken,
|
|
470
|
+
uploadId,
|
|
471
|
+
jobId
|
|
472
|
+
});
|
|
376
473
|
|
|
377
|
-
|
|
474
|
+
for(let f = 0; f < job.files.length; f++) {
|
|
475
|
+
const fileInfo = job.files[f];
|
|
476
|
+
|
|
477
|
+
let data;
|
|
478
|
+
if(typeof fileDataMap[fileInfo.path] === "number") {
|
|
479
|
+
// File descriptor - Read data from file
|
|
480
|
+
data = Buffer.alloc(fileInfo.len);
|
|
481
|
+
fs.readSync(fileDataMap[fileInfo.path], data, 0, fileInfo.len, fileInfo.off);
|
|
482
|
+
} else {
|
|
483
|
+
// Full data - Slice requested chunk
|
|
484
|
+
data = fileDataMap[fileInfo.path].slice(fileInfo.off, fileInfo.off + fileInfo.len);
|
|
485
|
+
}
|
|
378
486
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
};
|
|
487
|
+
if(encryption === "cgck") {
|
|
488
|
+
data = await this.Crypto.Encrypt(conk, data);
|
|
489
|
+
}
|
|
383
490
|
|
|
384
|
-
|
|
385
|
-
while(!jobSpecs[j]) {
|
|
386
|
-
// Wait for more jobs to be prepared
|
|
387
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
388
|
-
}
|
|
491
|
+
job.files[f].data = data;
|
|
389
492
|
|
|
390
|
-
|
|
391
|
-
|
|
493
|
+
prepared += fileInfo.len;
|
|
494
|
+
}
|
|
392
495
|
|
|
393
|
-
|
|
394
|
-
for(let f = 0; f < files.length; f++) {
|
|
395
|
-
const fileInfo = files[f];
|
|
496
|
+
jobSpecs[j] = job;
|
|
396
497
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
await this.UploadFileData({
|
|
402
|
-
libraryId,
|
|
403
|
-
objectId,
|
|
404
|
-
writeToken,
|
|
405
|
-
uploadId: id,
|
|
406
|
-
jobId,
|
|
407
|
-
filePath: fileInfo.path,
|
|
408
|
-
fileData: fileInfo.data,
|
|
409
|
-
encryption
|
|
410
|
-
});
|
|
498
|
+
// Wait for a bit to let upload start
|
|
499
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
500
|
+
}
|
|
411
501
|
|
|
412
|
-
|
|
413
|
-
} catch(error) {
|
|
414
|
-
this.Log(error, true);
|
|
502
|
+
};
|
|
415
503
|
|
|
416
|
-
|
|
504
|
+
const UploadJob = async (jobId, j) => {
|
|
505
|
+
while(!jobSpecs[j]) {
|
|
506
|
+
// Wait for more jobs to be prepared
|
|
507
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
508
|
+
}
|
|
417
509
|
|
|
418
|
-
|
|
419
|
-
|
|
510
|
+
const jobSpec = jobSpecs[j];
|
|
511
|
+
const files = jobSpec.files;
|
|
512
|
+
|
|
513
|
+
// Upload each item
|
|
514
|
+
for(let f = 0; f < files.length; f++) {
|
|
515
|
+
const fileInfo = files[f];
|
|
516
|
+
|
|
517
|
+
let retries = 0;
|
|
518
|
+
let succeeded = false;
|
|
519
|
+
do {
|
|
520
|
+
try {
|
|
521
|
+
await this.UploadFileData({
|
|
522
|
+
libraryId,
|
|
523
|
+
objectId,
|
|
524
|
+
writeToken,
|
|
525
|
+
uploadId,
|
|
526
|
+
jobId,
|
|
527
|
+
filePath: fileInfo.path,
|
|
528
|
+
fileData: fileInfo.data,
|
|
529
|
+
encryption
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
succeeded = true;
|
|
533
|
+
} catch(error) {
|
|
534
|
+
this.Log(error, true);
|
|
535
|
+
|
|
536
|
+
retries += 1;
|
|
537
|
+
|
|
538
|
+
if(retries >= 10) {
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
await new Promise(resolve => setTimeout(resolve, 10 * retries * 1000));
|
|
420
543
|
}
|
|
544
|
+
} while(!succeeded && retries < 10);
|
|
421
545
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} while(!succeeded && retries < 10);
|
|
546
|
+
delete jobSpecs[j].files[f].data;
|
|
547
|
+
uploaded += fileInfo.len;
|
|
425
548
|
|
|
426
|
-
|
|
427
|
-
|
|
549
|
+
if(callback && !alreadyCreditedKeys.has(`${jobId}:${fileInfo.path}`)) {
|
|
550
|
+
progress[fileInfo.path] = {
|
|
551
|
+
...progress[fileInfo.path],
|
|
552
|
+
uploaded: progress[fileInfo.path].uploaded + fileInfo.len
|
|
553
|
+
};
|
|
428
554
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
...progress[fileInfo.path],
|
|
432
|
-
uploaded: progress[fileInfo.path].uploaded + fileInfo.len
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
callback(progress);
|
|
555
|
+
callback(progress);
|
|
556
|
+
}
|
|
436
557
|
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Preparing jobs is done asynchronously
|
|
561
|
+
PrepareJobs().catch(e => { throw e; });
|
|
562
|
+
|
|
563
|
+
// Upload the first several chunks in sequence, to determine average upload rate
|
|
564
|
+
const rateTestJobs = Math.min(3, jobs.length);
|
|
565
|
+
let rates = [];
|
|
566
|
+
for(let j = 0; j < rateTestJobs; j++) {
|
|
567
|
+
const start = new Date().getTime();
|
|
568
|
+
await UploadJob(jobs[j], j);
|
|
569
|
+
const elapsed = (new Date().getTime() - start) / 1000;
|
|
570
|
+
const size = jobSpecs[j].files.map(file => file.len).reduce((length, total) => length + total, 0);
|
|
571
|
+
rates.push(size / elapsed / (1024 * 1024));
|
|
437
572
|
}
|
|
438
|
-
};
|
|
439
573
|
|
|
440
|
-
|
|
441
|
-
PrepareJobs().catch(e => {
|
|
442
|
-
throw e;
|
|
443
|
-
});
|
|
574
|
+
const averageRate = rates.reduce((mbps, total) => mbps + total, 0) / rateTestJobs;
|
|
444
575
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
576
|
+
// Upload remaining jobs in parallel
|
|
577
|
+
const concurrentUploads = Math.min(5, Math.ceil(averageRate / 2));
|
|
578
|
+
await this.utils.LimitedMap(
|
|
579
|
+
concurrentUploads,
|
|
580
|
+
jobs,
|
|
581
|
+
async (jobId, j) => {
|
|
582
|
+
if(j < rateTestJobs) { return; }
|
|
583
|
+
try {
|
|
584
|
+
await UploadJob(jobId, j);
|
|
585
|
+
} catch(err) {
|
|
586
|
+
throw Error (`Failed upload for job ${jobId}: ${err}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
);
|
|
454
590
|
}
|
|
455
|
-
|
|
456
|
-
const averageRate = rates.reduce((mbps, total) => mbps + total, 0) / rateTestJobs;
|
|
457
|
-
|
|
458
|
-
// Upload remaining jobs in parallel
|
|
459
|
-
const concurrentUploads = Math.min(5, Math.ceil(averageRate / 2));
|
|
460
|
-
await this.utils.LimitedMap(
|
|
461
|
-
concurrentUploads,
|
|
462
|
-
jobs,
|
|
463
|
-
async (jobId, j) => {
|
|
464
|
-
if(j < rateTestJobs) { return; }
|
|
465
|
-
|
|
466
|
-
await UploadJob(jobId, j);
|
|
467
|
-
}
|
|
468
|
-
);
|
|
469
591
|
};
|
|
470
592
|
|
|
471
593
|
exports.CreateFileUploadJob = async function({libraryId, objectId, writeToken, ops, defaults={}, encryption="none"}) {
|
|
@@ -497,12 +619,84 @@ exports.CreateFileUploadJob = async function({libraryId, objectId, writeToken, o
|
|
|
497
619
|
});
|
|
498
620
|
};
|
|
499
621
|
|
|
622
|
+
exports.ListFilesJob = async function({libraryId, objectId, writeToken, encryption="none"}) {
|
|
623
|
+
ValidateParameters({libraryId, objectId});
|
|
624
|
+
ValidateWriteToken(writeToken);
|
|
625
|
+
|
|
626
|
+
this.Log(`List file jobs: ${libraryId} ${objectId} ${writeToken}`);
|
|
627
|
+
|
|
628
|
+
const path = UrlJoin( "q", writeToken, "file_jobs");
|
|
629
|
+
|
|
630
|
+
return this.HttpClient.RequestJsonBody({
|
|
631
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true, encryption}),
|
|
632
|
+
method: "GET",
|
|
633
|
+
path: path,
|
|
634
|
+
allowFailover: false
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
exports.ListFilesUploadJobs = async function({libraryId, objectId, writeToken, jobId, encryption="none"}) {
|
|
639
|
+
ValidateParameters({libraryId, objectId});
|
|
640
|
+
ValidateWriteToken(writeToken);
|
|
641
|
+
|
|
642
|
+
this.Log(`List file upload jobs: ${libraryId} ${objectId} ${writeToken}`);
|
|
643
|
+
|
|
644
|
+
const path = UrlJoin("qlibs", libraryId, "q", writeToken, "file_jobs", jobId, "uploads");
|
|
645
|
+
|
|
646
|
+
return this.HttpClient.RequestJsonBody({
|
|
647
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true, encryption}),
|
|
648
|
+
method: "GET",
|
|
649
|
+
path: path,
|
|
650
|
+
allowFailover: false
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
exports.ResumeFileUploadJob = async function({libraryId, objectId, writeToken, ops, defaults={}, encryption="none", stoppedOrFailedJobIds=[]}) {
|
|
655
|
+
ValidateParameters({libraryId, objectId});
|
|
656
|
+
ValidateWriteToken(writeToken);
|
|
657
|
+
|
|
658
|
+
if(stoppedOrFailedJobIds.length === 0) {
|
|
659
|
+
this.Log("No stopped/failed jobs found");
|
|
660
|
+
throw new Error("No STOPPED/FAILED jobs found");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
this.Log(`Resuming file upload job: ${libraryId} ${objectId} ${writeToken}`);
|
|
664
|
+
|
|
665
|
+
if(encryption === "cgck") {
|
|
666
|
+
defaults.encryption = { scheme: "cgck" };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const body = {
|
|
670
|
+
seq: 0,
|
|
671
|
+
seq_complete: true,
|
|
672
|
+
clear_resolve: true,
|
|
673
|
+
defaults,
|
|
674
|
+
ops
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// resume and change jobId status from 'STOPPED' to 'IN_PROGRESS'
|
|
678
|
+
const responses = [];
|
|
679
|
+
this.Log(`Stopped or Failed job IDs: ${stoppedOrFailedJobIds.join(",")}`);
|
|
680
|
+
for(const jobId of stoppedOrFailedJobIds) {
|
|
681
|
+
const path = UrlJoin("q", writeToken, "file_jobs" , jobId, "resume");
|
|
682
|
+
|
|
683
|
+
let res = await this.HttpClient.RequestJsonBody({
|
|
684
|
+
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true, encryption}),
|
|
685
|
+
method: "PUT",
|
|
686
|
+
path: path,
|
|
687
|
+
body,
|
|
688
|
+
allowFailover: false
|
|
689
|
+
});
|
|
690
|
+
responses.push({message: res, id: jobId});
|
|
691
|
+
}
|
|
692
|
+
return responses;
|
|
693
|
+
};
|
|
694
|
+
|
|
500
695
|
exports.UploadStatus = async function({libraryId, objectId, writeToken, uploadId}) {
|
|
501
696
|
ValidateParameters({libraryId, objectId});
|
|
502
697
|
ValidateWriteToken(writeToken);
|
|
503
698
|
|
|
504
699
|
const path = UrlJoin("q", writeToken, "file_jobs", uploadId);
|
|
505
|
-
|
|
506
700
|
return this.utils.ResponseToJson(
|
|
507
701
|
this.HttpClient.Request({
|
|
508
702
|
headers: await this.authClient.AuthorizationHeader({libraryId, objectId, update: true}),
|
|
@@ -571,7 +765,7 @@ exports.UploadFileData = async function({libraryId, objectId, writeToken, encryp
|
|
|
571
765
|
|
|
572
766
|
let path = UrlJoin("q", writeToken, "file_jobs", uploadId, jobId);
|
|
573
767
|
|
|
574
|
-
|
|
768
|
+
const response = await this.utils.ResponseToJson(
|
|
575
769
|
this.HttpClient.Request({
|
|
576
770
|
method: "POST",
|
|
577
771
|
path: path,
|
|
@@ -585,6 +779,17 @@ exports.UploadFileData = async function({libraryId, objectId, writeToken, encryp
|
|
|
585
779
|
allowRetry: false
|
|
586
780
|
})
|
|
587
781
|
);
|
|
782
|
+
|
|
783
|
+
if(response && response.part && !response.part.hash) {
|
|
784
|
+
// The request succeeded but the server received fewer bytes than the part needed to seal
|
|
785
|
+
// (e.g. a dropped/truncated connection) - part.hash is only populated once finalized, so an
|
|
786
|
+
// empty hash here means this chunk isn't actually done despite the 200. Throw so the caller's
|
|
787
|
+
// retry loop re-reads the job status (updated skip/rem) and resends the missing remainder,
|
|
788
|
+
// instead of the chunk silently being counted as complete.
|
|
789
|
+
throw new Error(`Upload did not finalize part for ${filePath} (job ${jobId})`);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return response;
|
|
588
793
|
};
|
|
589
794
|
|
|
590
795
|
exports.FinalizeUploadJob = async function({libraryId, objectId, writeToken}) {
|
package/src/client/LiveStream.js
CHANGED
|
@@ -1090,9 +1090,9 @@ exports.StreamStatus = async function({name, showParams=false, writeToken}) {
|
|
|
1090
1090
|
status.recordingStatus = lroStatus.custom.status;
|
|
1091
1091
|
}
|
|
1092
1092
|
} catch(error) {
|
|
1093
|
-
console.log("LRO Status (failed): ", error.response
|
|
1093
|
+
console.log("LRO Status (failed): ", error.response?.statusCode);
|
|
1094
1094
|
status.state = "stopped";
|
|
1095
|
-
status.error = error.response;
|
|
1095
|
+
status.error = error.response || error.message;
|
|
1096
1096
|
return status;
|
|
1097
1097
|
}
|
|
1098
1098
|
|
|
@@ -3532,6 +3532,13 @@ exports.OutputsList = async function({libraryId, objectId, includeState=true}) {
|
|
|
3532
3532
|
method: "live/outputs",
|
|
3533
3533
|
constant: true
|
|
3534
3534
|
});
|
|
3535
|
+
} catch(error) {
|
|
3536
|
+
if(error.status === 404) {
|
|
3537
|
+
// Stream object may have been deleted
|
|
3538
|
+
return {};
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
throw error;
|
|
3535
3542
|
} finally {
|
|
3536
3543
|
restore();
|
|
3537
3544
|
}
|
|
@@ -3544,17 +3551,22 @@ exports.OutputsList = async function({libraryId, objectId, includeState=true}) {
|
|
|
3544
3551
|
const streamId = value.input?.stream;
|
|
3545
3552
|
if(!streamId) { return; }
|
|
3546
3553
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3554
|
+
try {
|
|
3555
|
+
const [streamLibraryId, streamStatus] = await Promise.all([
|
|
3556
|
+
this.ContentObjectLibraryId({objectId: streamId}),
|
|
3557
|
+
this.StreamStatus({name: streamId})
|
|
3558
|
+
]);
|
|
3559
|
+
|
|
3560
|
+
value.input.name = await this.ContentObjectMetadata({
|
|
3561
|
+
libraryId: streamLibraryId,
|
|
3562
|
+
objectId: streamId,
|
|
3563
|
+
metadataSubtree: "/public/name",
|
|
3564
|
+
});
|
|
3565
|
+
value.input.status = streamStatus?.state;
|
|
3566
|
+
} catch(error) {
|
|
3567
|
+
// Stream object may have been deleted
|
|
3568
|
+
value.input.status = "unavailable";
|
|
3569
|
+
}
|
|
3558
3570
|
}
|
|
3559
3571
|
);
|
|
3560
3572
|
|
|
@@ -3569,9 +3581,13 @@ exports.OutputsList = async function({libraryId, objectId, includeState=true}) {
|
|
|
3569
3581
|
|
|
3570
3582
|
const nodeUrls = {};
|
|
3571
3583
|
await this.utils.LimitedMap(10, nodeIds, async nodeId => {
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3584
|
+
try {
|
|
3585
|
+
const nodes = await this.SpaceNodes({matchNodeId: nodeId});
|
|
3586
|
+
const fabricUrl = nodes?.[0]?.services?.fabric_api?.urls?.[0];
|
|
3587
|
+
if(fabricUrl) { nodeUrls[nodeId] = fabricUrl; }
|
|
3588
|
+
} catch(error) {
|
|
3589
|
+
this.Log(`Failed to resolve node ${nodeId}: ${error.message}`, true);
|
|
3590
|
+
}
|
|
3575
3591
|
});
|
|
3576
3592
|
|
|
3577
3593
|
// Rewrite fabric-generated srt_pull URLs to the egress host, and group outputs by node
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const {NewOpt} = require("./lib/options");
|
|
2
|
+
const Utility = require("./lib/Utility");
|
|
3
|
+
|
|
4
|
+
const Client = require("./lib/concerns/Client");
|
|
5
|
+
const Edit = require("./lib/concerns/Edit");
|
|
6
|
+
|
|
7
|
+
class MigrateEncryptionConkForNewUser extends Utility {
|
|
8
|
+
blueprint() {
|
|
9
|
+
return {
|
|
10
|
+
concerns: [Client, Edit],
|
|
11
|
+
options: [
|
|
12
|
+
NewOpt("objectId",
|
|
13
|
+
{
|
|
14
|
+
descTemplate: "object Id",
|
|
15
|
+
type: "string",
|
|
16
|
+
demand: true
|
|
17
|
+
}),
|
|
18
|
+
NewOpt("publicKey",
|
|
19
|
+
{
|
|
20
|
+
descTemplate: "new user public key with/without encoded form",
|
|
21
|
+
type: "string",
|
|
22
|
+
demand: true
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async body() {
|
|
29
|
+
const logger = this.logger;
|
|
30
|
+
const {objectId, publicKey} = this.args;
|
|
31
|
+
|
|
32
|
+
const client = await this.concerns.Client.get();
|
|
33
|
+
const libraryId = await client.ContentObjectLibraryId({objectId});
|
|
34
|
+
|
|
35
|
+
const res = await client.EditContentObject({libraryId, objectId});
|
|
36
|
+
|
|
37
|
+
await client.MigrateEncryptionConkForUserProvided({
|
|
38
|
+
libraryId,
|
|
39
|
+
objectId,
|
|
40
|
+
writeToken: res.writeToken,
|
|
41
|
+
newEncodedUserPublicKey: publicKey
|
|
42
|
+
});
|
|
43
|
+
logger.log("Migration of encryption conk is complete...");
|
|
44
|
+
|
|
45
|
+
const versionHash = await this.concerns.Edit.finalize({
|
|
46
|
+
libraryId,
|
|
47
|
+
objectId,
|
|
48
|
+
writeToken:res.writeToken
|
|
49
|
+
});
|
|
50
|
+
logger.log("version hash", versionHash);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
header() {
|
|
54
|
+
return `Add encryption conk for user provided '${this.args.objectId}':`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if(require.main === module) {
|
|
59
|
+
Utility.cmdLineInvoke(MigrateEncryptionConkForNewUser);
|
|
60
|
+
} else {
|
|
61
|
+
module.exports = MigrateEncryptionConkForNewUser;
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Create new production master from specified file(s)
|
|
2
2
|
|
|
3
|
-
const {ModOpt, StdOpt} = require("./lib/options");
|
|
3
|
+
const {ModOpt, StdOpt, NewOpt} = require("./lib/options");
|
|
4
4
|
const Utility = require("./lib/Utility");
|
|
5
5
|
|
|
6
6
|
const ArgNoWait = require("./lib/concerns/ArgNoWait");
|
|
@@ -16,14 +16,19 @@ class ObjectAddFiles extends Utility {
|
|
|
16
16
|
concerns: [Logger, ExistObj, Edit, ArgNoWait, LocalFile, CloudFile],
|
|
17
17
|
options: [
|
|
18
18
|
ModOpt("files", {X: "to add"}),
|
|
19
|
-
StdOpt("encrypt", {X: "uploaded files"})
|
|
19
|
+
StdOpt("encrypt", {X: "uploaded files"}),
|
|
20
|
+
NewOpt("resume", {
|
|
21
|
+
descTemplate: "If specified, resume jobs for the given write token (--resume 'write_token')",
|
|
22
|
+
group: "Main",
|
|
23
|
+
type: "string"
|
|
24
|
+
})
|
|
20
25
|
]
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
async body() {
|
|
25
30
|
const logger = this.logger;
|
|
26
|
-
const {encrypt, noWait} = this.args;
|
|
31
|
+
const {encrypt, noWait, resume: resumeArg} = this.args;
|
|
27
32
|
|
|
28
33
|
let access;
|
|
29
34
|
if(this.args.s3Reference || this.args.s3Copy) access = this.concerns.CloudFile.credentialSet();
|
|
@@ -35,10 +40,22 @@ class ObjectAddFiles extends Utility {
|
|
|
35
40
|
|
|
36
41
|
const {libraryId, objectId} = await this.concerns.ExistObj.argsProc();
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
let writeToken;
|
|
44
|
+
let resume=false;
|
|
45
|
+
if(resumeArg) {
|
|
46
|
+
writeToken = resumeArg;
|
|
47
|
+
resume=true;
|
|
48
|
+
} else {
|
|
49
|
+
// create new write token
|
|
50
|
+
writeToken = await this.concerns.Edit.getWriteToken({
|
|
51
|
+
libraryId,
|
|
52
|
+
objectId
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
logger.data("WRITE_TOKEN", writeToken);
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.log("Using write token:", writeToken);
|
|
58
|
+
|
|
42
59
|
|
|
43
60
|
if(access) {
|
|
44
61
|
await this.concerns.CloudFile.add({
|
|
@@ -47,7 +64,8 @@ class ObjectAddFiles extends Utility {
|
|
|
47
64
|
writeToken,
|
|
48
65
|
access,
|
|
49
66
|
fileInfo,
|
|
50
|
-
encrypt
|
|
67
|
+
encrypt,
|
|
68
|
+
resume,
|
|
51
69
|
});
|
|
52
70
|
} else {
|
|
53
71
|
await this.concerns.LocalFile.add({
|
|
@@ -55,12 +73,14 @@ class ObjectAddFiles extends Utility {
|
|
|
55
73
|
objectId,
|
|
56
74
|
writeToken,
|
|
57
75
|
fileInfo,
|
|
58
|
-
encrypt
|
|
76
|
+
encrypt,
|
|
77
|
+
resume,
|
|
59
78
|
});
|
|
60
79
|
// Close file handles
|
|
61
80
|
this.concerns.LocalFile.closeFileHandles(fileHandles);
|
|
62
81
|
}
|
|
63
82
|
|
|
83
|
+
// finalize the write token
|
|
64
84
|
const hash = await this.concerns.Edit.finalize({
|
|
65
85
|
libraryId,
|
|
66
86
|
noWait,
|
|
@@ -95,7 +95,7 @@ const New = context => {
|
|
|
95
95
|
);
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
const add = async ({libraryId, objectId, writeToken, files, access, encrypt, copy}) => {
|
|
98
|
+
const add = async ({libraryId, objectId, writeToken, files, access, encrypt, copy, resume}) => {
|
|
99
99
|
if(kindOf(copy) === "undefined") copy = isCopy();
|
|
100
100
|
files = files || context.args.files;
|
|
101
101
|
const groupedFiles = groupByPathMatch(access, files);
|
|
@@ -125,6 +125,7 @@ const New = context => {
|
|
|
125
125
|
secret,
|
|
126
126
|
signedUrl,
|
|
127
127
|
copy,
|
|
128
|
+
resume,
|
|
128
129
|
callback,
|
|
129
130
|
encryption: encrypt ? "cgck" : "none"
|
|
130
131
|
});
|
|
@@ -19,7 +19,7 @@ const blueprint = {
|
|
|
19
19
|
|
|
20
20
|
const New = context => {
|
|
21
21
|
|
|
22
|
-
const add = async ({libraryId, objectId, writeToken, fileInfo, encrypt}) => {
|
|
22
|
+
const add = async ({libraryId, objectId, writeToken, fileInfo, encrypt, resume}) => {
|
|
23
23
|
const client = await context.concerns.Client.get();
|
|
24
24
|
await client.UploadFiles({
|
|
25
25
|
libraryId,
|
|
@@ -27,6 +27,7 @@ const New = context => {
|
|
|
27
27
|
writeToken,
|
|
28
28
|
fileInfo,
|
|
29
29
|
callback,
|
|
30
|
+
resume,
|
|
30
31
|
encryption: encrypt ? "cgck" : "none"
|
|
31
32
|
});
|
|
32
33
|
};
|