@transloadit/node 4.3.0 → 4.4.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 +58 -4
- package/dist/Transloadit.d.ts +30 -4
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +84 -22
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/goldenTemplates.d.ts +52 -0
- package/dist/alphalib/goldenTemplates.d.ts.map +1 -0
- package/dist/alphalib/goldenTemplates.js +46 -0
- package/dist/alphalib/goldenTemplates.js.map +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/upload.d.ts +22 -0
- package/dist/cli/commands/upload.d.ts.map +1 -0
- package/dist/cli/commands/upload.js +95 -0
- package/dist/cli/commands/upload.js.map +1 -0
- package/dist/inputFiles.d.ts +41 -0
- package/dist/inputFiles.d.ts.map +1 -0
- package/dist/inputFiles.js +214 -0
- package/dist/inputFiles.js.map +1 -0
- package/dist/robots.d.ts +38 -0
- package/dist/robots.d.ts.map +1 -0
- package/dist/robots.js +230 -0
- package/dist/robots.js.map +1 -0
- package/dist/tus.d.ts +5 -1
- package/dist/tus.d.ts.map +1 -1
- package/dist/tus.js +80 -6
- package/dist/tus.js.map +1 -1
- package/package.json +2 -2
- package/src/Transloadit.ts +140 -27
- package/src/alphalib/goldenTemplates.ts +53 -0
- package/src/cli/commands/index.ts +4 -0
- package/src/cli/commands/upload.ts +129 -0
- package/src/inputFiles.ts +278 -0
- package/src/robots.ts +317 -0
- package/src/tus.ts +91 -5
package/src/Transloadit.ts
CHANGED
|
@@ -52,7 +52,7 @@ import type {
|
|
|
52
52
|
import { lintAssemblyInstructions as lintAssemblyInstructionsInternal } from './lintAssemblyInstructions.ts'
|
|
53
53
|
import PaginationStream from './PaginationStream.ts'
|
|
54
54
|
import PollingTimeoutError from './PollingTimeoutError.ts'
|
|
55
|
-
import type { Stream } from './tus.ts'
|
|
55
|
+
import type { Stream, UploadBehavior } from './tus.ts'
|
|
56
56
|
import { sendTusRequest } from './tus.ts'
|
|
57
57
|
|
|
58
58
|
// See https://github.com/sindresorhus/got/tree/v11.8.6?tab=readme-ov-file#errors
|
|
@@ -66,11 +66,30 @@ export {
|
|
|
66
66
|
TimeoutError,
|
|
67
67
|
UploadError,
|
|
68
68
|
} from 'got'
|
|
69
|
-
|
|
69
|
+
export { goldenTemplates } from './alphalib/goldenTemplates.ts'
|
|
70
70
|
export type { AssemblyStatus } from './alphalib/types/assemblyStatus.ts'
|
|
71
71
|
export * from './apiTypes.ts'
|
|
72
72
|
export { InconsistentResponseError, ApiError }
|
|
73
|
+
export { mergeTemplateContent } from './alphalib/templateMerge.ts'
|
|
74
|
+
export type {
|
|
75
|
+
Base64Strategy,
|
|
76
|
+
InputFile,
|
|
77
|
+
PrepareInputFilesOptions,
|
|
78
|
+
PrepareInputFilesResult,
|
|
79
|
+
UploadInput,
|
|
80
|
+
UrlStrategy,
|
|
81
|
+
} from './inputFiles.ts'
|
|
82
|
+
export { prepareInputFiles } from './inputFiles.ts'
|
|
73
83
|
export type { LintAssemblyInstructionsResult, LintFatalLevel } from './lintAssemblyInstructions.ts'
|
|
84
|
+
export type {
|
|
85
|
+
RobotHelp,
|
|
86
|
+
RobotHelpOptions,
|
|
87
|
+
RobotListItem,
|
|
88
|
+
RobotListOptions,
|
|
89
|
+
RobotListResult,
|
|
90
|
+
RobotParamHelp,
|
|
91
|
+
} from './robots.ts'
|
|
92
|
+
export { getRobotHelp, listRobots } from './robots.ts'
|
|
74
93
|
|
|
75
94
|
const log = debug('transloadit')
|
|
76
95
|
const logWarn = debug('transloadit:warn')
|
|
@@ -80,6 +99,12 @@ export interface UploadProgress {
|
|
|
80
99
|
totalBytes?: number | undefined
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
export type { UploadBehavior }
|
|
103
|
+
|
|
104
|
+
export type AssemblyStatusWithUploadUrls = AssemblyStatus & {
|
|
105
|
+
upload_urls?: Record<string, string>
|
|
106
|
+
}
|
|
107
|
+
|
|
83
108
|
const { version } = packageJson
|
|
84
109
|
|
|
85
110
|
export type AssemblyProgress = (assembly: AssemblyStatus) => void
|
|
@@ -157,6 +182,7 @@ interface AssemblyUploadOptions {
|
|
|
157
182
|
uploads?: {
|
|
158
183
|
[name: string]: Readable | IntoStreamInput
|
|
159
184
|
}
|
|
185
|
+
uploadBehavior?: UploadBehavior
|
|
160
186
|
waitForCompletion?: boolean
|
|
161
187
|
chunkSize?: number
|
|
162
188
|
uploadConcurrency?: number
|
|
@@ -173,6 +199,10 @@ interface AssemblyUploadOptions {
|
|
|
173
199
|
export interface CreateAssemblyOptions extends AssemblyUploadOptions {
|
|
174
200
|
params?: CreateAssemblyParams
|
|
175
201
|
assemblyId?: string
|
|
202
|
+
/**
|
|
203
|
+
* Expected number of tus uploads when files will be uploaded separately.
|
|
204
|
+
*/
|
|
205
|
+
expectedUploads?: number
|
|
176
206
|
}
|
|
177
207
|
|
|
178
208
|
export interface ResumeAssemblyUploadsOptions extends AssemblyUploadOptions {
|
|
@@ -237,7 +267,7 @@ export interface SmartCDNUrlOptions {
|
|
|
237
267
|
export type Fields = Record<string, string | number>
|
|
238
268
|
|
|
239
269
|
// A special promise that lets the user immediately get the assembly ID (synchronously before the request is sent)
|
|
240
|
-
interface CreateAssemblyPromise extends Promise<
|
|
270
|
+
interface CreateAssemblyPromise extends Promise<AssemblyStatusWithUploadUrls> {
|
|
241
271
|
assemblyId: string
|
|
242
272
|
}
|
|
243
273
|
|
|
@@ -273,9 +303,19 @@ function checkResult<T>(result: T | { error: string }): asserts result is T {
|
|
|
273
303
|
}
|
|
274
304
|
}
|
|
275
305
|
|
|
276
|
-
|
|
306
|
+
type AuthKeySecret = {
|
|
277
307
|
authKey: string
|
|
278
308
|
authSecret: string
|
|
309
|
+
authToken?: undefined
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
type AuthToken = {
|
|
313
|
+
authToken: string
|
|
314
|
+
authKey?: string
|
|
315
|
+
authSecret?: string
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
type BaseOptions = {
|
|
279
319
|
endpoint?: string
|
|
280
320
|
maxRetries?: number
|
|
281
321
|
timeout?: number
|
|
@@ -283,11 +323,15 @@ export interface Options {
|
|
|
283
323
|
validateResponses?: boolean
|
|
284
324
|
}
|
|
285
325
|
|
|
326
|
+
export type Options = BaseOptions & (AuthKeySecret | AuthToken)
|
|
327
|
+
|
|
286
328
|
export class Transloadit {
|
|
287
329
|
private _authKey: string
|
|
288
330
|
|
|
289
331
|
private _authSecret: string
|
|
290
332
|
|
|
333
|
+
private _authToken: string | null
|
|
334
|
+
|
|
291
335
|
private _endpoint: string
|
|
292
336
|
|
|
293
337
|
private _maxRetries: number
|
|
@@ -301,20 +345,26 @@ export class Transloadit {
|
|
|
301
345
|
private _validateResponses = false
|
|
302
346
|
|
|
303
347
|
constructor(opts: Options) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (opts.authSecret == null) {
|
|
309
|
-
throw new Error('Please provide an authSecret')
|
|
310
|
-
}
|
|
348
|
+
const rawToken = typeof opts?.authToken === 'string' ? opts.authToken.trim() : ''
|
|
349
|
+
const hasToken = rawToken.length > 0
|
|
311
350
|
|
|
312
351
|
if (opts.endpoint?.endsWith('/')) {
|
|
313
352
|
throw new Error('Trailing slash in endpoint is not allowed')
|
|
314
353
|
}
|
|
315
354
|
|
|
316
|
-
|
|
317
|
-
|
|
355
|
+
if (!hasToken) {
|
|
356
|
+
if (opts?.authKey == null) {
|
|
357
|
+
throw new Error('Please provide an authKey')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (opts.authSecret == null) {
|
|
361
|
+
throw new Error('Please provide an authSecret')
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this._authKey = opts.authKey ?? ''
|
|
366
|
+
this._authSecret = opts.authSecret ?? ''
|
|
367
|
+
this._authToken = hasToken ? rawToken : null
|
|
318
368
|
this._endpoint = opts.endpoint || 'https://api2.transloadit.com'
|
|
319
369
|
this._maxRetries = opts.maxRetries != null ? opts.maxRetries : 5
|
|
320
370
|
this._defaultTimeout = opts.timeout != null ? opts.timeout : 60000
|
|
@@ -350,7 +400,9 @@ export class Transloadit {
|
|
|
350
400
|
files = {},
|
|
351
401
|
uploads = {},
|
|
352
402
|
assemblyId,
|
|
403
|
+
expectedUploads,
|
|
353
404
|
signal,
|
|
405
|
+
uploadBehavior = 'await',
|
|
354
406
|
} = opts
|
|
355
407
|
|
|
356
408
|
// Keep track of how long the request took
|
|
@@ -406,30 +458,38 @@ export class Transloadit {
|
|
|
406
458
|
const streamErrorPromise = createStreamErrorPromise(allStreamsMap)
|
|
407
459
|
|
|
408
460
|
const createAssemblyAndUpload = async () => {
|
|
409
|
-
const
|
|
461
|
+
const totalExpectedUploads =
|
|
462
|
+
expectedUploads == null ? allStreams.length : Math.max(expectedUploads, allStreams.length)
|
|
463
|
+
|
|
464
|
+
const result: AssemblyStatusWithUploadUrls = await this._remoteJson({
|
|
410
465
|
urlSuffix,
|
|
411
466
|
method: 'post',
|
|
412
467
|
timeout: { request: timeout },
|
|
413
468
|
params,
|
|
414
469
|
fields: {
|
|
415
|
-
tus_num_expected_upload_files:
|
|
470
|
+
tus_num_expected_upload_files: totalExpectedUploads,
|
|
416
471
|
},
|
|
417
472
|
signal,
|
|
418
473
|
})
|
|
419
474
|
checkResult(result)
|
|
420
475
|
|
|
421
476
|
if (Object.keys(allStreamsMap).length > 0) {
|
|
422
|
-
await sendTusRequest({
|
|
477
|
+
const { uploadUrls } = await sendTusRequest({
|
|
423
478
|
streamsMap: allStreamsMap,
|
|
424
479
|
assembly: result,
|
|
425
480
|
onProgress: onUploadProgress,
|
|
426
481
|
requestedChunkSize,
|
|
427
482
|
uploadConcurrency,
|
|
428
483
|
signal,
|
|
484
|
+
uploadBehavior,
|
|
429
485
|
})
|
|
486
|
+
if (uploadBehavior !== 'await' && Object.keys(uploadUrls).length > 0) {
|
|
487
|
+
result.upload_urls = uploadUrls
|
|
488
|
+
}
|
|
430
489
|
}
|
|
431
490
|
|
|
432
|
-
|
|
491
|
+
const shouldWaitForCompletion = waitForCompletion && uploadBehavior === 'await'
|
|
492
|
+
if (!shouldWaitForCompletion) return result
|
|
433
493
|
|
|
434
494
|
if (result.assembly_id == null) {
|
|
435
495
|
throw new InconsistentResponseError(
|
|
@@ -478,7 +538,9 @@ export class Transloadit {
|
|
|
478
538
|
})
|
|
479
539
|
}
|
|
480
540
|
|
|
481
|
-
async resumeAssemblyUploads(
|
|
541
|
+
async resumeAssemblyUploads(
|
|
542
|
+
opts: ResumeAssemblyUploadsOptions,
|
|
543
|
+
): Promise<AssemblyStatusWithUploadUrls> {
|
|
482
544
|
const {
|
|
483
545
|
assemblyUrl,
|
|
484
546
|
files = {},
|
|
@@ -490,12 +552,16 @@ export class Transloadit {
|
|
|
490
552
|
onUploadProgress = () => {},
|
|
491
553
|
onAssemblyProgress = () => {},
|
|
492
554
|
signal,
|
|
555
|
+
uploadBehavior = 'await',
|
|
493
556
|
} = opts
|
|
494
557
|
|
|
495
558
|
const startTimeMs = getHrTimeMs()
|
|
496
559
|
|
|
497
560
|
getAssemblyIdFromUrl(assemblyUrl)
|
|
498
|
-
const assembly = await this._fetchAssemblyStatus({
|
|
561
|
+
const assembly: AssemblyStatusWithUploadUrls = await this._fetchAssemblyStatus({
|
|
562
|
+
url: assemblyUrl,
|
|
563
|
+
signal,
|
|
564
|
+
})
|
|
499
565
|
const statusUrl = assembly.assembly_ssl_url ?? assembly.assembly_url ?? assemblyUrl
|
|
500
566
|
|
|
501
567
|
const finishedKeys = new Set<string>()
|
|
@@ -571,13 +637,25 @@ export class Transloadit {
|
|
|
571
637
|
onProgress: onUploadProgress,
|
|
572
638
|
signal,
|
|
573
639
|
uploadUrls: uploadUrlsByLabel,
|
|
640
|
+
uploadBehavior,
|
|
574
641
|
})
|
|
575
642
|
|
|
576
643
|
await Promise.race([uploadPromise, streamErrorPromise])
|
|
644
|
+
const { uploadUrls } = await uploadPromise
|
|
645
|
+
if (uploadBehavior !== 'await' && Object.keys(uploadUrls).length > 0) {
|
|
646
|
+
assembly.upload_urls = uploadUrls
|
|
647
|
+
}
|
|
577
648
|
}
|
|
578
649
|
|
|
579
|
-
const latestAssembly = await this._fetchAssemblyStatus({
|
|
580
|
-
|
|
650
|
+
const latestAssembly: AssemblyStatusWithUploadUrls = await this._fetchAssemblyStatus({
|
|
651
|
+
url: statusUrl,
|
|
652
|
+
signal,
|
|
653
|
+
})
|
|
654
|
+
if (uploadBehavior !== 'await' && assembly.upload_urls) {
|
|
655
|
+
latestAssembly.upload_urls = assembly.upload_urls
|
|
656
|
+
}
|
|
657
|
+
const shouldWaitForCompletion = waitForCompletion && uploadBehavior === 'await'
|
|
658
|
+
if (!shouldWaitForCompletion) return latestAssembly
|
|
581
659
|
|
|
582
660
|
if (latestAssembly.assembly_id == null) {
|
|
583
661
|
throw new InconsistentResponseError(
|
|
@@ -701,6 +779,7 @@ export class Transloadit {
|
|
|
701
779
|
const { assembly_ssl_url: url } = await this.getAssembly(assemblyId)
|
|
702
780
|
const rawResult = await this._remoteJson<Record<string, unknown>, OptionalAuthParams>({
|
|
703
781
|
url,
|
|
782
|
+
isTrustedUrl: true,
|
|
704
783
|
method: 'delete',
|
|
705
784
|
})
|
|
706
785
|
|
|
@@ -829,6 +908,7 @@ export class Transloadit {
|
|
|
829
908
|
const rawResult = await this._remoteJson<Record<string, unknown>, OptionalAuthParams>({
|
|
830
909
|
url,
|
|
831
910
|
urlSuffix: url ? undefined : `/assemblies/${assemblyId}`,
|
|
911
|
+
isTrustedUrl: Boolean(url),
|
|
832
912
|
signal,
|
|
833
913
|
})
|
|
834
914
|
|
|
@@ -1021,6 +1101,9 @@ export class Transloadit {
|
|
|
1021
1101
|
params: OptionalAuthParams,
|
|
1022
1102
|
algorithm?: string,
|
|
1023
1103
|
): { signature: string; params: string } {
|
|
1104
|
+
if (!this._authKey || !this._authSecret) {
|
|
1105
|
+
throw new Error('Cannot sign params without authKey and authSecret.')
|
|
1106
|
+
}
|
|
1024
1107
|
const jsonParams = this._prepareParams(params)
|
|
1025
1108
|
const signature = this._calcSignature(jsonParams, algorithm)
|
|
1026
1109
|
|
|
@@ -1031,6 +1114,9 @@ export class Transloadit {
|
|
|
1031
1114
|
* Construct a signed Smart CDN URL. See https://transloadit.com/docs/topics/signature-authentication/#smart-cdn.
|
|
1032
1115
|
*/
|
|
1033
1116
|
getSignedSmartCDNUrl(opts: SmartCDNUrlOptions): string {
|
|
1117
|
+
if (!this._authKey || !this._authSecret) {
|
|
1118
|
+
throw new Error('authKey and authSecret are required to sign Smart CDN URLs.')
|
|
1119
|
+
}
|
|
1034
1120
|
return getSignedSmartCdnUrl({
|
|
1035
1121
|
...opts,
|
|
1036
1122
|
authKey: this._authKey,
|
|
@@ -1039,15 +1125,24 @@ export class Transloadit {
|
|
|
1039
1125
|
}
|
|
1040
1126
|
|
|
1041
1127
|
private _calcSignature(toSign: string, algorithm = 'sha384'): string {
|
|
1128
|
+
if (!this._authSecret) {
|
|
1129
|
+
throw new Error('Cannot sign params without authSecret.')
|
|
1130
|
+
}
|
|
1042
1131
|
return signParamsSync(toSign, this._authSecret, algorithm)
|
|
1043
1132
|
}
|
|
1044
1133
|
|
|
1045
1134
|
// Sets the multipart/form-data for POST, PUT and DELETE requests, including
|
|
1046
1135
|
// the streams, the signed params, and any additional fields.
|
|
1047
1136
|
private _appendForm(form: FormData, params: OptionalAuthParams, fields?: Fields): void {
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1137
|
+
const shouldSign = Boolean(this._authKey && this._authSecret)
|
|
1138
|
+
let jsonParams = JSON.stringify(params ?? {})
|
|
1139
|
+
let signature: string | undefined
|
|
1140
|
+
|
|
1141
|
+
if (shouldSign) {
|
|
1142
|
+
const sigData = this.calcSignature(params)
|
|
1143
|
+
jsonParams = sigData.params
|
|
1144
|
+
signature = sigData.signature
|
|
1145
|
+
}
|
|
1051
1146
|
|
|
1052
1147
|
form.append('params', jsonParams)
|
|
1053
1148
|
|
|
@@ -1057,16 +1152,24 @@ export class Transloadit {
|
|
|
1057
1152
|
}
|
|
1058
1153
|
}
|
|
1059
1154
|
|
|
1060
|
-
|
|
1155
|
+
if (signature) {
|
|
1156
|
+
form.append('signature', signature)
|
|
1157
|
+
}
|
|
1061
1158
|
}
|
|
1062
1159
|
|
|
1063
1160
|
// Implements HTTP GET query params, handling the case where the url already
|
|
1064
1161
|
// has params.
|
|
1065
1162
|
private _appendParamsToUrl(url: string, params: OptionalAuthParams): string {
|
|
1066
|
-
const { signature, params: jsonParams } = this.calcSignature(params)
|
|
1067
|
-
|
|
1068
1163
|
const prefix = url.indexOf('?') === -1 ? '?' : '&'
|
|
1069
1164
|
|
|
1165
|
+
const shouldSign = Boolean(this._authKey && this._authSecret)
|
|
1166
|
+
if (!shouldSign) {
|
|
1167
|
+
const jsonParams = JSON.stringify(params ?? {})
|
|
1168
|
+
return `${url}${prefix}params=${encodeURIComponent(jsonParams)}`
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const { signature, params: jsonParams } = this.calcSignature(params)
|
|
1172
|
+
|
|
1070
1173
|
return `${url}${prefix}signature=${signature}¶ms=${encodeURIComponent(jsonParams)}`
|
|
1071
1174
|
}
|
|
1072
1175
|
|
|
@@ -1102,6 +1205,7 @@ export class Transloadit {
|
|
|
1102
1205
|
private async _remoteJson<TRet, TParams extends OptionalAuthParams>(opts: {
|
|
1103
1206
|
urlSuffix?: string
|
|
1104
1207
|
url?: string
|
|
1208
|
+
isTrustedUrl?: boolean
|
|
1105
1209
|
timeout?: Delays
|
|
1106
1210
|
method?: 'delete' | 'get' | 'post' | 'put'
|
|
1107
1211
|
params?: TParams
|
|
@@ -1112,6 +1216,7 @@ export class Transloadit {
|
|
|
1112
1216
|
const {
|
|
1113
1217
|
urlSuffix,
|
|
1114
1218
|
url: urlInput,
|
|
1219
|
+
isTrustedUrl = false,
|
|
1115
1220
|
timeout = { request: this._defaultTimeout },
|
|
1116
1221
|
method = 'get',
|
|
1117
1222
|
params = {},
|
|
@@ -1123,6 +1228,13 @@ export class Transloadit {
|
|
|
1123
1228
|
// Allow providing either a `urlSuffix` or a full `url`
|
|
1124
1229
|
if (!urlSuffix && !urlInput) throw new Error('No URL provided')
|
|
1125
1230
|
let url = urlInput || `${this._endpoint}${urlSuffix}`
|
|
1231
|
+
if (urlInput && !isTrustedUrl) {
|
|
1232
|
+
const allowed = new URL(this._endpoint)
|
|
1233
|
+
const candidate = new URL(urlInput)
|
|
1234
|
+
if (allowed.origin !== candidate.origin) {
|
|
1235
|
+
throw new Error(`Untrusted URL: ${candidate.origin}`)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1126
1238
|
|
|
1127
1239
|
if (method === 'get') {
|
|
1128
1240
|
url = this._appendParamsToUrl(url, params)
|
|
@@ -1147,6 +1259,7 @@ export class Transloadit {
|
|
|
1147
1259
|
headers: {
|
|
1148
1260
|
'Transloadit-Client': `node-sdk:${version}`,
|
|
1149
1261
|
'User-Agent': undefined, // Remove got's user-agent
|
|
1262
|
+
...(this._authToken ? { Authorization: `Bearer ${this._authToken}` } : {}),
|
|
1150
1263
|
...headers,
|
|
1151
1264
|
},
|
|
1152
1265
|
responseType: 'json',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type GoldenTemplate = {
|
|
2
|
+
slug: string
|
|
3
|
+
version: string
|
|
4
|
+
description: string
|
|
5
|
+
steps: Record<string, unknown>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const goldenTemplates = {
|
|
9
|
+
'~transloadit/encode-hls-video@0.0.1': {
|
|
10
|
+
slug: '~transloadit/encode-hls-video@0.0.1',
|
|
11
|
+
version: '0.0.1',
|
|
12
|
+
description:
|
|
13
|
+
'Encode an input video into HLS renditions (270p, 360p, 540p) with an adaptive playlist.',
|
|
14
|
+
steps: {
|
|
15
|
+
':original': {
|
|
16
|
+
robot: '/upload/handle',
|
|
17
|
+
},
|
|
18
|
+
low: {
|
|
19
|
+
robot: '/video/encode',
|
|
20
|
+
use: ':original',
|
|
21
|
+
ffmpeg_stack: 'v7.0.0',
|
|
22
|
+
preset: 'hls-270p',
|
|
23
|
+
result: true,
|
|
24
|
+
turbo: true,
|
|
25
|
+
},
|
|
26
|
+
mid: {
|
|
27
|
+
robot: '/video/encode',
|
|
28
|
+
use: ':original',
|
|
29
|
+
ffmpeg_stack: 'v7.0.0',
|
|
30
|
+
preset: 'hls-360p',
|
|
31
|
+
result: true,
|
|
32
|
+
turbo: true,
|
|
33
|
+
},
|
|
34
|
+
high: {
|
|
35
|
+
robot: '/video/encode',
|
|
36
|
+
use: ':original',
|
|
37
|
+
ffmpeg_stack: 'v7.0.0',
|
|
38
|
+
preset: 'hls-540p',
|
|
39
|
+
result: true,
|
|
40
|
+
turbo: true,
|
|
41
|
+
},
|
|
42
|
+
adaptive: {
|
|
43
|
+
robot: '/video/adaptive',
|
|
44
|
+
use: {
|
|
45
|
+
steps: ['low', 'mid', 'high'],
|
|
46
|
+
bundle_steps: true,
|
|
47
|
+
},
|
|
48
|
+
technique: 'hls',
|
|
49
|
+
playlist_name: 'my_playlist.m3u8',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
} satisfies Record<string, GoldenTemplate>
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
TemplatesModifyCommand,
|
|
26
26
|
TemplatesSyncCommand,
|
|
27
27
|
} from './templates.ts'
|
|
28
|
+
import { UploadCommand } from './upload.ts'
|
|
28
29
|
|
|
29
30
|
export function createCli(): Cli {
|
|
30
31
|
const cli = new Cli({
|
|
@@ -63,5 +64,8 @@ export function createCli(): Cli {
|
|
|
63
64
|
// Notifications commands
|
|
64
65
|
cli.register(NotificationsReplayCommand)
|
|
65
66
|
|
|
67
|
+
// Uploads commands
|
|
68
|
+
cli.register(UploadCommand)
|
|
69
|
+
|
|
66
70
|
return cli
|
|
67
71
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { Command, Option } from 'clipanion'
|
|
3
|
+
import type { AssemblyStatus } from '../../alphalib/types/assemblyStatus.ts'
|
|
4
|
+
import { sendTusRequest } from '../../tus.ts'
|
|
5
|
+
import type { IOutputCtl } from '../OutputCtl.ts'
|
|
6
|
+
import { UnauthenticatedCommand } from './BaseCommand.ts'
|
|
7
|
+
|
|
8
|
+
export interface UploadOptions {
|
|
9
|
+
file: string
|
|
10
|
+
createUploadEndpoint?: string
|
|
11
|
+
resumeUploadEndpoint?: string
|
|
12
|
+
assemblyUrl: string
|
|
13
|
+
field?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const deriveEndpointFromUploadUrl = (uploadUrl: string): string => {
|
|
17
|
+
const url = new URL(uploadUrl)
|
|
18
|
+
url.pathname = url.pathname.replace(/\/[^/]*$/, '/')
|
|
19
|
+
return url.toString()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function upload(
|
|
23
|
+
output: IOutputCtl,
|
|
24
|
+
{
|
|
25
|
+
file,
|
|
26
|
+
createUploadEndpoint,
|
|
27
|
+
resumeUploadEndpoint,
|
|
28
|
+
assemblyUrl,
|
|
29
|
+
field = ':original',
|
|
30
|
+
}: UploadOptions,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const tusEndpoint =
|
|
33
|
+
createUploadEndpoint ??
|
|
34
|
+
(resumeUploadEndpoint ? deriveEndpointFromUploadUrl(resumeUploadEndpoint) : undefined)
|
|
35
|
+
|
|
36
|
+
if (!tusEndpoint) {
|
|
37
|
+
throw new Error('Provide --create-upload-endpoint or --resume-upload-endpoint.')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stream = fs.createReadStream(file)
|
|
41
|
+
const streamsMap = {
|
|
42
|
+
[field]: { path: file, stream },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const assembly: AssemblyStatus = {
|
|
46
|
+
tus_url: tusEndpoint,
|
|
47
|
+
assembly_ssl_url: assemblyUrl,
|
|
48
|
+
} as AssemblyStatus
|
|
49
|
+
|
|
50
|
+
const { uploadUrls } = await sendTusRequest({
|
|
51
|
+
streamsMap,
|
|
52
|
+
assembly,
|
|
53
|
+
requestedChunkSize: Number.POSITIVE_INFINITY,
|
|
54
|
+
uploadConcurrency: 1,
|
|
55
|
+
onProgress: () => {},
|
|
56
|
+
uploadUrls: resumeUploadEndpoint ? { [field]: resumeUploadEndpoint } : undefined,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const uploadUrl = uploadUrls[field]
|
|
60
|
+
|
|
61
|
+
output.print(`Uploaded ${file}`, {
|
|
62
|
+
status: 'ok',
|
|
63
|
+
file,
|
|
64
|
+
field,
|
|
65
|
+
assembly_url: assemblyUrl,
|
|
66
|
+
tus_endpoint: tusEndpoint,
|
|
67
|
+
resume_upload_endpoint: resumeUploadEndpoint,
|
|
68
|
+
upload_url: uploadUrl,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class UploadCommand extends UnauthenticatedCommand {
|
|
73
|
+
static override paths = [['upload']]
|
|
74
|
+
|
|
75
|
+
static override usage = Command.Usage({
|
|
76
|
+
category: 'Uploads',
|
|
77
|
+
description: 'Upload a local file to a tus endpoint for an Assembly',
|
|
78
|
+
details: `
|
|
79
|
+
Upload a local file to a tus endpoint and attach it to an existing Assembly.
|
|
80
|
+
Use --create-upload-endpoint for new uploads or --resume-upload-endpoint to resume.
|
|
81
|
+
`,
|
|
82
|
+
examples: [
|
|
83
|
+
[
|
|
84
|
+
'Upload a file to an Assembly',
|
|
85
|
+
'transloadit upload ./video.mp4 --create-upload-endpoint https://api2.transloadit.com/resumable/files/ --assembly https://api2.transloadit.com/assemblies/ASSEMBLY_ID',
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
'Resume a file upload',
|
|
89
|
+
'transloadit upload ./video.mp4 --resume-upload-endpoint https://api2.transloadit.com/resumable/files/UPLOAD_ID --assembly https://api2.transloadit.com/assemblies/ASSEMBLY_ID',
|
|
90
|
+
],
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
file = Option.String({ required: true })
|
|
95
|
+
tusEndpoint = Option.String({ required: false })
|
|
96
|
+
|
|
97
|
+
assemblyUrl = Option.String('--assembly', {
|
|
98
|
+
description: 'Assembly URL to attach this upload to',
|
|
99
|
+
required: true,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
createUploadEndpoint = Option.String('--create-upload-endpoint', {
|
|
103
|
+
description: 'Tus create endpoint (e.g. https://api2.transloadit.com/resumable/files/)',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
resumeUploadEndpoint = Option.String('--resume-upload-endpoint', {
|
|
107
|
+
description: 'Tus upload URL to resume (e.g. https://.../resumable/files/<id>)',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
field = Option.String('--field', {
|
|
111
|
+
description: 'Field name for the upload (default: :original)',
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
protected async run(): Promise<number | undefined> {
|
|
115
|
+
try {
|
|
116
|
+
await upload(this.output, {
|
|
117
|
+
file: this.file,
|
|
118
|
+
createUploadEndpoint: this.createUploadEndpoint ?? this.tusEndpoint,
|
|
119
|
+
resumeUploadEndpoint: this.resumeUploadEndpoint,
|
|
120
|
+
assemblyUrl: this.assemblyUrl,
|
|
121
|
+
field: this.field,
|
|
122
|
+
})
|
|
123
|
+
return undefined
|
|
124
|
+
} catch (err) {
|
|
125
|
+
this.output.error(err instanceof Error ? err.message : String(err))
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|