@transloadit/node 6.7.0 → 6.9.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 +67 -3
- package/dist/Transloadit.d.ts +6 -0
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +21 -1
- package/dist/Transloadit.js.map +1 -1
- package/dist/apiTypes.d.ts +4 -0
- package/dist/apiTypes.d.ts.map +1 -1
- package/dist/cli/commands/storage.d.ts.map +1 -1
- package/dist/cli/commands/storage.js +110 -37
- package/dist/cli/commands/storage.js.map +1 -1
- package/dist/cli/storageReceipts.d.ts +80 -1
- package/dist/cli/storageReceipts.d.ts.map +1 -1
- package/dist/cli/storageReceipts.js +48 -12
- package/dist/cli/storageReceipts.js.map +1 -1
- package/dist/storageAsset.d.ts +44 -0
- package/dist/storageAsset.d.ts.map +1 -0
- package/dist/storageAsset.js +173 -0
- package/dist/storageAsset.js.map +1 -0
- package/dist/storageImage.d.ts +11 -35
- package/dist/storageImage.d.ts.map +1 -1
- package/dist/storageImage.js +21 -141
- package/dist/storageImage.js.map +1 -1
- package/package.json +2 -2
- package/src/Transloadit.ts +38 -1
- package/src/apiTypes.ts +8 -1
- package/src/cli/commands/storage.ts +141 -67
- package/src/cli/storageReceipts.ts +50 -11
- package/src/storageAsset.ts +271 -0
- package/src/storageImage.ts +43 -201
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { AssemblyStatus } from './alphalib/types/assemblyStatus.ts'
|
|
2
|
+
import type { InterpolatableRobotUploadHandleInstructionsInput } from './alphalib/types/robots/upload-handle.ts'
|
|
3
|
+
import type { StoredAsset } from './alphalib/types/storageAsset.ts'
|
|
4
|
+
import type { CreateAssemblyOptions, Transloadit } from './Transloadit.ts'
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { createReadStream } from 'node:fs'
|
|
8
|
+
|
|
9
|
+
import debug from 'debug'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
import { ApiError } from './ApiError.ts'
|
|
13
|
+
import { storedAssetSchema } from './alphalib/types/storageAsset.ts'
|
|
14
|
+
import InconsistentResponseError from './InconsistentResponseError.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Verified metadata for an original of any media type. Dimensions are optional.
|
|
18
|
+
*/
|
|
19
|
+
export interface StoredAssetReceipt extends Readonly<StoredAsset> {
|
|
20
|
+
/** Legacy catalog spelling; new server receipts use has_alpha. */
|
|
21
|
+
readonly hasAlpha?: boolean
|
|
22
|
+
readonly md5hash: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Shared SDK/CLI opt-in for server extraction; rendering remains a separate choice. */
|
|
26
|
+
export const storageImagePlaceholderSchema = z.enum(['blur', 'empty'])
|
|
27
|
+
|
|
28
|
+
/** One explicit destination, with the existing Assembly upload and polling controls. */
|
|
29
|
+
export interface StoreAssetOptions
|
|
30
|
+
extends Pick<
|
|
31
|
+
CreateAssemblyOptions,
|
|
32
|
+
'chunkSize' | 'onAssemblyProgress' | 'onUploadProgress' | 'signal' | 'timeout'
|
|
33
|
+
> {
|
|
34
|
+
/** Complete relative Storage filename. Directories and interpolation expressions are rejected. */
|
|
35
|
+
path: string
|
|
36
|
+
/** Explicit opt-in replacement of an existing path; defaults to false. */
|
|
37
|
+
overwrite?: boolean
|
|
38
|
+
/** Request a server ThumbHash. Successful extraction adds metadata usage of 20% of the file bytes. */
|
|
39
|
+
placeholder?: z.infer<typeof storageImagePlaceholderSchema>
|
|
40
|
+
/** Observe verified metadata. Not awaited; sync and async observer errors cannot undo a write. */
|
|
41
|
+
onReceipt?: (
|
|
42
|
+
receipt: StoredAssetReceipt,
|
|
43
|
+
input: StoredAssetExpectation,
|
|
44
|
+
assemblyId: string | undefined,
|
|
45
|
+
) => void | Promise<void>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Trusted upload facts used to correlate a stored result with an application-owned upload. */
|
|
49
|
+
export interface StoredAssetExpectation {
|
|
50
|
+
path: string
|
|
51
|
+
size: number
|
|
52
|
+
md5hash: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Recovers one original's receipt without re-uploading or trusting notification payloads. */
|
|
56
|
+
export interface GetStoredAssetReceiptOptions {
|
|
57
|
+
assemblyId: string
|
|
58
|
+
expected: StoredAssetExpectation
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const positiveIntegerSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
62
|
+
const receiptRequestSchema = z.object({
|
|
63
|
+
assemblyId: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
64
|
+
expected: z.object({
|
|
65
|
+
path: z.string(),
|
|
66
|
+
size: positiveIntegerSchema,
|
|
67
|
+
md5hash: z.string().regex(/^[a-f0-9]{32}$/),
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
const completedAssetSchema = z.object({
|
|
71
|
+
ok: z.literal('ASSEMBLY_COMPLETED'),
|
|
72
|
+
results: z.object({
|
|
73
|
+
':original': z.tuple([
|
|
74
|
+
storedAssetSchema.extend({
|
|
75
|
+
md5hash: storedAssetSchema.shape.md5hash.unwrap(),
|
|
76
|
+
size: positiveIntegerSchema,
|
|
77
|
+
}),
|
|
78
|
+
]),
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
81
|
+
const completedImageSchema = completedAssetSchema.extend({
|
|
82
|
+
results: z.object({
|
|
83
|
+
':original': z.tuple([
|
|
84
|
+
completedAssetSchema.shape.results.shape[':original'].items[0].extend({
|
|
85
|
+
width: positiveIntegerSchema,
|
|
86
|
+
height: positiveIntegerSchema,
|
|
87
|
+
}),
|
|
88
|
+
]),
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
function validateDestination(path: string): void {
|
|
93
|
+
if (
|
|
94
|
+
typeof path !== 'string' ||
|
|
95
|
+
path.trim() !== path ||
|
|
96
|
+
Buffer.byteLength(path) > 1024 ||
|
|
97
|
+
path.normalize('NFC') !== path ||
|
|
98
|
+
/[\p{Cc}\p{Cs}\\|]/u.test(path) ||
|
|
99
|
+
path.includes('${') ||
|
|
100
|
+
path.split('/').some((segment) => segment.trim() === '' || segment === '.' || segment === '..')
|
|
101
|
+
) {
|
|
102
|
+
throw new TypeError('Storage path must be a complete relative filename without interpolation')
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Stores one exact original; a mismatch is reported with the completed write's receipt. */
|
|
107
|
+
export function storeAsset(
|
|
108
|
+
client: Transloadit,
|
|
109
|
+
filePath: string,
|
|
110
|
+
options: StoreAssetOptions,
|
|
111
|
+
): Promise<StoredAssetReceipt> {
|
|
112
|
+
return storeOriginal(client, filePath, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Shared upload pipeline; the image entry point retains its existing transformed-byte policy. */
|
|
116
|
+
export async function storeOriginal(
|
|
117
|
+
client: Transloadit,
|
|
118
|
+
filePath: string,
|
|
119
|
+
options: StoreAssetOptions,
|
|
120
|
+
image = false,
|
|
121
|
+
): Promise<StoredAssetReceipt> {
|
|
122
|
+
const {
|
|
123
|
+
path,
|
|
124
|
+
chunkSize,
|
|
125
|
+
onAssemblyProgress,
|
|
126
|
+
onUploadProgress,
|
|
127
|
+
onReceipt,
|
|
128
|
+
overwrite,
|
|
129
|
+
signal,
|
|
130
|
+
timeout,
|
|
131
|
+
} = options
|
|
132
|
+
validateDestination(path)
|
|
133
|
+
const placeholder = storageImagePlaceholderSchema.optional().parse(options.placeholder, {
|
|
134
|
+
path: ['placeholder'],
|
|
135
|
+
})
|
|
136
|
+
if (overwrite !== undefined && typeof overwrite !== 'boolean')
|
|
137
|
+
throw new TypeError('overwrite must be a boolean')
|
|
138
|
+
signal?.throwIfAborted()
|
|
139
|
+
const checksum = createHash('md5')
|
|
140
|
+
let size = 0
|
|
141
|
+
for await (const chunk of createReadStream(filePath, { signal })) {
|
|
142
|
+
checksum.update(chunk)
|
|
143
|
+
size += chunk.length
|
|
144
|
+
}
|
|
145
|
+
if (size === 0)
|
|
146
|
+
throw new Error(image ? 'Cannot store an empty image' : 'Cannot store an empty file')
|
|
147
|
+
const md5hash = checksum.digest('hex')
|
|
148
|
+
signal?.throwIfAborted()
|
|
149
|
+
const assembly = await client.createAssembly({
|
|
150
|
+
chunkSize,
|
|
151
|
+
files: { [image ? 'image' : 'file']: filePath },
|
|
152
|
+
onAssemblyProgress,
|
|
153
|
+
onUploadProgress,
|
|
154
|
+
params: {
|
|
155
|
+
steps: {
|
|
156
|
+
...(placeholder === 'blur'
|
|
157
|
+
? {
|
|
158
|
+
':original': {
|
|
159
|
+
robot: '/upload/handle',
|
|
160
|
+
output_meta: { thumbhash: true },
|
|
161
|
+
} satisfies InterpolatableRobotUploadHandleInstructionsInput,
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
stored: {
|
|
165
|
+
robot: '/transloadit/store',
|
|
166
|
+
use: ':original',
|
|
167
|
+
path,
|
|
168
|
+
conflict_strategy: overwrite === true ? 'overwrite' : 'error',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
signal,
|
|
173
|
+
timeout,
|
|
174
|
+
waitForCompletion: true,
|
|
175
|
+
})
|
|
176
|
+
const input = { path, size, md5hash }
|
|
177
|
+
// API2 can watermark uploads before Robots run; this exact write's result describes stored bytes.
|
|
178
|
+
const receipt = validateReceipt(assembly, input, true, image)
|
|
179
|
+
const observerFailed = (): void => {
|
|
180
|
+
debug('transloadit:warn')('Ignored onReceipt observer failure after a completed Storage write')
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
// Neither a stalled observer nor its rejection may hide a successfully stored receipt.
|
|
184
|
+
void Promise.resolve(onReceipt?.({ ...receipt }, input, assembly.assembly_id)).catch(
|
|
185
|
+
observerFailed,
|
|
186
|
+
)
|
|
187
|
+
} catch {
|
|
188
|
+
observerFailed()
|
|
189
|
+
}
|
|
190
|
+
if (!image && (receipt.size !== size || receipt.md5hash !== md5hash)) {
|
|
191
|
+
throw new InconsistentResponseError(
|
|
192
|
+
'Stored bytes differ from the original file; do not upload again',
|
|
193
|
+
{
|
|
194
|
+
cause: { assemblyId: assembly.assembly_id, receipt, expected: input },
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
return receipt
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Fetches authoritative Assembly status and verifies identity and original-byte fidelity. */
|
|
202
|
+
export function getStoredAssetReceipt(
|
|
203
|
+
client: Transloadit,
|
|
204
|
+
options: GetStoredAssetReceiptOptions,
|
|
205
|
+
): Promise<StoredAssetReceipt> {
|
|
206
|
+
return getOriginalReceipt(client, options)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Shared recovery keeps image-specific validation and Assembly diagnostics together. */
|
|
210
|
+
export async function getOriginalReceipt(
|
|
211
|
+
client: Transloadit,
|
|
212
|
+
options: GetStoredAssetReceiptOptions,
|
|
213
|
+
image = false,
|
|
214
|
+
): Promise<StoredAssetReceipt> {
|
|
215
|
+
const { assemblyId, expected } = receiptRequestSchema.parse(options)
|
|
216
|
+
validateDestination(expected.path)
|
|
217
|
+
const assembly = await client.getAssembly(assemblyId)
|
|
218
|
+
if (assembly.assembly_id !== assemblyId) {
|
|
219
|
+
throw new InconsistentResponseError('The response did not match the requested Assembly', {
|
|
220
|
+
cause: { assemblyId },
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
return validateReceipt(assembly, expected, false, image)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validateReceipt(
|
|
227
|
+
assembly: AssemblyStatus,
|
|
228
|
+
expected: StoredAssetExpectation,
|
|
229
|
+
acceptTransformed = false,
|
|
230
|
+
image = false,
|
|
231
|
+
): StoredAssetReceipt {
|
|
232
|
+
const { path, size, md5hash } = expected
|
|
233
|
+
if (typeof assembly.error === 'string') throw new ApiError({ body: assembly })
|
|
234
|
+
if (assembly.ok === 'ASSEMBLY_CANCELED') {
|
|
235
|
+
throw new InconsistentResponseError('The Storage Assembly ended with ASSEMBLY_CANCELED', {
|
|
236
|
+
cause: { assemblyId: assembly.assembly_id },
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
assembly.ok === 'ASSEMBLY_UPLOADING' ||
|
|
241
|
+
assembly.ok === 'ASSEMBLY_EXECUTING' ||
|
|
242
|
+
assembly.ok === 'ASSEMBLY_REPLAYING'
|
|
243
|
+
) {
|
|
244
|
+
throw new InconsistentResponseError(`The Storage Assembly is not complete (${assembly.ok})`, {
|
|
245
|
+
cause: { assemblyId: assembly.assembly_id },
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
const parsed = (image ? completedImageSchema : completedAssetSchema).safeParse(assembly)
|
|
249
|
+
const result = parsed.success ? parsed.data.results[':original'][0] : undefined
|
|
250
|
+
const originals = assembly.results?.[':original']
|
|
251
|
+
const receiptCheck = {
|
|
252
|
+
originalCount: Array.isArray(originals) ? originals.length : 0,
|
|
253
|
+
metadataValid: parsed.success,
|
|
254
|
+
pathMatches: result === undefined ? undefined : result.path === path,
|
|
255
|
+
sizeMatches: result === undefined ? undefined : result.size === size,
|
|
256
|
+
md5Matches: result === undefined ? undefined : result.md5hash === md5hash,
|
|
257
|
+
}
|
|
258
|
+
if (
|
|
259
|
+
result === undefined ||
|
|
260
|
+
result.path !== path ||
|
|
261
|
+
(!acceptTransformed && (!receiptCheck.sizeMatches || !receiptCheck.md5Matches))
|
|
262
|
+
) {
|
|
263
|
+
throw new InconsistentResponseError(
|
|
264
|
+
`The Assembly did not return a matching Storage ${image ? 'image' : 'asset'} receipt`,
|
|
265
|
+
{
|
|
266
|
+
cause: { assemblyId: assembly.assembly_id, receiptCheck },
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
return result
|
|
271
|
+
}
|
package/src/storageImage.ts
CHANGED
|
@@ -1,46 +1,30 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type {
|
|
2
|
+
GetStoredAssetReceiptOptions,
|
|
3
|
+
StoreAssetOptions,
|
|
4
|
+
StoredAssetExpectation,
|
|
5
|
+
StoredAssetReceipt,
|
|
6
|
+
} from './storageAsset.ts'
|
|
7
|
+
import type { Transloadit } from './Transloadit.ts'
|
|
5
8
|
|
|
6
|
-
import { createHash } from 'node:crypto'
|
|
7
|
-
import { createReadStream } from 'node:fs'
|
|
8
|
-
|
|
9
|
-
import debug from 'debug'
|
|
10
9
|
import { z } from 'zod'
|
|
11
10
|
|
|
12
|
-
import { ApiError } from './ApiError.ts'
|
|
13
11
|
import { storedAssetSchema } from './alphalib/types/storageAsset.ts'
|
|
14
12
|
import InconsistentResponseError from './InconsistentResponseError.ts'
|
|
13
|
+
import { getOriginalReceipt, storeOriginal } from './storageAsset.ts'
|
|
14
|
+
|
|
15
|
+
export { storageImagePlaceholderSchema } from './storageAsset.ts'
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
* Width and height reflect EXIF auto-orientation, matching Storage preview delivery.
|
|
19
|
-
*/
|
|
20
|
-
export interface StoredImageReceipt extends Readonly<StoredAsset> {
|
|
21
|
-
/** Legacy catalog spelling; new server receipts use has_alpha. */
|
|
22
|
-
readonly hasAlpha?: boolean
|
|
17
|
+
/** Verified image metadata; dimensions reflect EXIF orientation used by preview delivery. */
|
|
18
|
+
export interface StoredImageReceipt extends StoredAssetReceipt {
|
|
23
19
|
readonly height: number
|
|
24
|
-
readonly md5hash: string
|
|
25
20
|
readonly width: number
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
export
|
|
23
|
+
export type StoredImageExpectation = StoredAssetExpectation
|
|
24
|
+
export type GetStoredImageReceiptOptions = GetStoredAssetReceiptOptions
|
|
30
25
|
|
|
31
|
-
/**
|
|
32
|
-
export interface StoreImageOptions
|
|
33
|
-
extends Pick<
|
|
34
|
-
CreateAssemblyOptions,
|
|
35
|
-
'chunkSize' | 'onAssemblyProgress' | 'onUploadProgress' | 'signal' | 'timeout'
|
|
36
|
-
> {
|
|
37
|
-
/** Complete relative Storage filename. Directories and interpolation expressions are rejected. */
|
|
38
|
-
path: string
|
|
39
|
-
/** Explicit opt-in replacement of an existing path; defaults to false. */
|
|
40
|
-
overwrite?: boolean
|
|
41
|
-
/** Request a server ThumbHash. Successful extraction adds metadata usage of 20% of the file bytes. */
|
|
42
|
-
placeholder?: z.infer<typeof storageImagePlaceholderSchema>
|
|
43
|
-
/** Observe verified metadata. Not awaited; sync and async observer errors cannot undo a write. */
|
|
26
|
+
/** Image-specific callback on the shared original upload pipeline. */
|
|
27
|
+
export interface StoreImageOptions extends Omit<StoreAssetOptions, 'onReceipt'> {
|
|
44
28
|
onReceipt?: (
|
|
45
29
|
receipt: StoredImageReceipt,
|
|
46
30
|
input: StoredImageExpectation,
|
|
@@ -48,190 +32,48 @@ export interface StoreImageOptions
|
|
|
48
32
|
) => void | Promise<void>
|
|
49
33
|
}
|
|
50
34
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
md5hash: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Recovers one original's receipt without re-uploading or trusting notification payloads. */
|
|
59
|
-
export interface GetStoredImageReceiptOptions {
|
|
60
|
-
assemblyId: string
|
|
61
|
-
expected: StoredImageExpectation
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const positiveIntegerSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
65
|
-
const receiptRequestSchema = z.object({
|
|
66
|
-
assemblyId: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
67
|
-
expected: z.object({
|
|
68
|
-
path: z.string(),
|
|
69
|
-
size: positiveIntegerSchema,
|
|
70
|
-
md5hash: z.string().regex(/^[a-f0-9]{32}$/),
|
|
71
|
-
}),
|
|
72
|
-
})
|
|
73
|
-
const completedImageSchema = z.object({
|
|
74
|
-
ok: z.literal('ASSEMBLY_COMPLETED'),
|
|
75
|
-
results: z.object({
|
|
76
|
-
':original': z.tuple([
|
|
77
|
-
storedAssetSchema.extend({
|
|
78
|
-
md5hash: storedAssetSchema.shape.md5hash.unwrap(),
|
|
79
|
-
height: positiveIntegerSchema,
|
|
80
|
-
width: positiveIntegerSchema,
|
|
81
|
-
size: positiveIntegerSchema,
|
|
82
|
-
}),
|
|
83
|
-
]),
|
|
84
|
-
}),
|
|
35
|
+
const imageReceiptSchema = storedAssetSchema.extend({
|
|
36
|
+
md5hash: storedAssetSchema.shape.md5hash.unwrap(),
|
|
37
|
+
height: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
38
|
+
width: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
85
39
|
})
|
|
86
40
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
) {
|
|
97
|
-
throw new TypeError('Storage path must be a complete relative filename without interpolation')
|
|
41
|
+
function requireImage(receipt: StoredAssetReceipt): StoredImageReceipt {
|
|
42
|
+
const image = imageReceiptSchema.safeParse(receipt)
|
|
43
|
+
if (!image.success) {
|
|
44
|
+
throw new InconsistentResponseError(
|
|
45
|
+
'The Assembly did not return a matching Storage image receipt',
|
|
46
|
+
{
|
|
47
|
+
cause: { receipt },
|
|
48
|
+
},
|
|
49
|
+
)
|
|
98
50
|
}
|
|
51
|
+
return image.data
|
|
99
52
|
}
|
|
100
53
|
|
|
101
|
-
/**
|
|
54
|
+
/** Retains the image API's transformed-byte semantics; use storeAsset for exact originals. */
|
|
102
55
|
export async function storeImage(
|
|
103
56
|
client: Transloadit,
|
|
104
57
|
filePath: string,
|
|
105
58
|
options: StoreImageOptions,
|
|
106
59
|
): Promise<StoredImageReceipt> {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
signal,
|
|
115
|
-
timeout,
|
|
116
|
-
} = options
|
|
117
|
-
validateDestination(path)
|
|
118
|
-
const placeholder = storageImagePlaceholderSchema.optional().parse(options.placeholder, {
|
|
119
|
-
path: ['placeholder'],
|
|
120
|
-
})
|
|
121
|
-
if (overwrite !== undefined && typeof overwrite !== 'boolean')
|
|
122
|
-
throw new TypeError('overwrite must be a boolean')
|
|
123
|
-
signal?.throwIfAborted()
|
|
124
|
-
const checksum = createHash('md5')
|
|
125
|
-
let size = 0
|
|
126
|
-
for await (const chunk of createReadStream(filePath, { signal })) {
|
|
127
|
-
checksum.update(chunk)
|
|
128
|
-
size += chunk.length
|
|
129
|
-
}
|
|
130
|
-
if (size === 0) throw new Error('Cannot store an empty image')
|
|
131
|
-
const md5hash = checksum.digest('hex')
|
|
132
|
-
signal?.throwIfAborted()
|
|
133
|
-
const assembly = await client.createAssembly({
|
|
134
|
-
chunkSize,
|
|
135
|
-
files: { image: filePath },
|
|
136
|
-
onAssemblyProgress,
|
|
137
|
-
onUploadProgress,
|
|
138
|
-
params: {
|
|
139
|
-
steps: {
|
|
140
|
-
...(placeholder === 'blur'
|
|
141
|
-
? {
|
|
142
|
-
':original': {
|
|
143
|
-
robot: '/upload/handle',
|
|
144
|
-
output_meta: { thumbhash: true },
|
|
145
|
-
} satisfies InterpolatableRobotUploadHandleInstructionsInput,
|
|
146
|
-
}
|
|
147
|
-
: {}),
|
|
148
|
-
stored: {
|
|
149
|
-
robot: '/transloadit/store',
|
|
150
|
-
use: ':original',
|
|
151
|
-
path,
|
|
152
|
-
conflict_strategy: overwrite === true ? 'overwrite' : 'error',
|
|
153
|
-
},
|
|
154
|
-
},
|
|
60
|
+
const receipt = await storeOriginal(
|
|
61
|
+
client,
|
|
62
|
+
filePath,
|
|
63
|
+
{
|
|
64
|
+
...options,
|
|
65
|
+
onReceipt: (asset, input, assemblyId) =>
|
|
66
|
+
options.onReceipt?.(requireImage(asset), input, assemblyId),
|
|
155
67
|
},
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
})
|
|
160
|
-
const input = { path, size, md5hash }
|
|
161
|
-
// API2 can watermark uploads before Robots run; this exact write's result describes stored bytes.
|
|
162
|
-
const receipt = validateReceipt(assembly, input, true)
|
|
163
|
-
const observerFailed = (): void => {
|
|
164
|
-
debug('transloadit:warn')('Ignored onReceipt observer failure after a completed Storage write')
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
// Neither a stalled observer nor its rejection may hide a successfully stored receipt.
|
|
168
|
-
void Promise.resolve(onReceipt?.({ ...receipt }, input, assembly.assembly_id)).catch(
|
|
169
|
-
observerFailed,
|
|
170
|
-
)
|
|
171
|
-
} catch {
|
|
172
|
-
observerFailed()
|
|
173
|
-
}
|
|
174
|
-
return receipt
|
|
68
|
+
true,
|
|
69
|
+
)
|
|
70
|
+
return requireImage(receipt)
|
|
175
71
|
}
|
|
176
72
|
|
|
177
|
-
/**
|
|
73
|
+
/** Recovers one image receipt with the same strict byte checks as generic receipt recovery. */
|
|
178
74
|
export async function getStoredImageReceipt(
|
|
179
75
|
client: Transloadit,
|
|
180
76
|
options: GetStoredImageReceiptOptions,
|
|
181
77
|
): Promise<StoredImageReceipt> {
|
|
182
|
-
|
|
183
|
-
validateDestination(expected.path)
|
|
184
|
-
const assembly = await client.getAssembly(assemblyId)
|
|
185
|
-
if (assembly.assembly_id !== assemblyId) {
|
|
186
|
-
throw new InconsistentResponseError('The response did not match the requested Assembly', {
|
|
187
|
-
cause: { assemblyId },
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
return validateReceipt(assembly, expected)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function validateReceipt(
|
|
194
|
-
assembly: AssemblyStatus,
|
|
195
|
-
expected: StoredImageExpectation,
|
|
196
|
-
acceptTransformed = false,
|
|
197
|
-
): StoredImageReceipt {
|
|
198
|
-
const { path, size, md5hash } = expected
|
|
199
|
-
if (typeof assembly.error === 'string') throw new ApiError({ body: assembly })
|
|
200
|
-
if (assembly.ok === 'ASSEMBLY_CANCELED') {
|
|
201
|
-
throw new InconsistentResponseError('The Storage Assembly ended with ASSEMBLY_CANCELED', {
|
|
202
|
-
cause: { assemblyId: assembly.assembly_id },
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
if (
|
|
206
|
-
assembly.ok === 'ASSEMBLY_UPLOADING' ||
|
|
207
|
-
assembly.ok === 'ASSEMBLY_EXECUTING' ||
|
|
208
|
-
assembly.ok === 'ASSEMBLY_REPLAYING'
|
|
209
|
-
) {
|
|
210
|
-
throw new InconsistentResponseError(`The Storage Assembly is not complete (${assembly.ok})`, {
|
|
211
|
-
cause: { assemblyId: assembly.assembly_id },
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
const parsed = completedImageSchema.safeParse(assembly)
|
|
215
|
-
const result = parsed.success ? parsed.data.results[':original'][0] : undefined
|
|
216
|
-
const originals = assembly.results?.[':original']
|
|
217
|
-
const receiptCheck = {
|
|
218
|
-
originalCount: Array.isArray(originals) ? originals.length : 0,
|
|
219
|
-
metadataValid: parsed.success,
|
|
220
|
-
pathMatches: result === undefined ? undefined : result.path === path,
|
|
221
|
-
sizeMatches: result === undefined ? undefined : result.size === size,
|
|
222
|
-
md5Matches: result === undefined ? undefined : result.md5hash === md5hash,
|
|
223
|
-
}
|
|
224
|
-
if (
|
|
225
|
-
result === undefined ||
|
|
226
|
-
result.path !== path ||
|
|
227
|
-
(!acceptTransformed && (!receiptCheck.sizeMatches || !receiptCheck.md5Matches))
|
|
228
|
-
) {
|
|
229
|
-
throw new InconsistentResponseError(
|
|
230
|
-
'The Assembly did not return a matching Storage image receipt',
|
|
231
|
-
{
|
|
232
|
-
cause: { assemblyId: assembly.assembly_id, receiptCheck },
|
|
233
|
-
},
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
return result
|
|
78
|
+
return requireImage(await getOriginalReceipt(client, options, true))
|
|
237
79
|
}
|