transloadit 4.12.0 → 4.13.1
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 +232 -23
- package/dist/Transloadit.d.ts +55 -1
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +143 -2
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/types/assemblyStatus.d.ts +574 -198
- package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.js +17 -0
- package/dist/alphalib/types/assemblyStatus.js.map +1 -1
- package/dist/alphalib/types/robots/_index.d.ts +205 -93
- package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_index.js +8 -0
- package/dist/alphalib/types/robots/_index.js.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts +4 -4
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.js +2 -0
- package/dist/alphalib/types/robots/_instructions-primitives.js.map +1 -1
- package/dist/alphalib/types/robots/file-serve.d.ts +12 -0
- package/dist/alphalib/types/robots/file-serve.d.ts.map +1 -1
- package/dist/alphalib/types/robots/file-serve.js +9 -3
- package/dist/alphalib/types/robots/file-serve.js.map +1 -1
- package/dist/alphalib/types/robots/transloadit-import.d.ts +195 -0
- package/dist/alphalib/types/robots/transloadit-import.d.ts.map +1 -0
- package/dist/alphalib/types/robots/transloadit-import.js +108 -0
- package/dist/alphalib/types/robots/transloadit-import.js.map +1 -0
- package/dist/alphalib/types/robots/transloadit-store.d.ts +441 -0
- package/dist/alphalib/types/robots/transloadit-store.d.ts.map +1 -0
- package/dist/alphalib/types/robots/transloadit-store.js +69 -0
- package/dist/alphalib/types/robots/transloadit-store.js.map +1 -0
- package/dist/alphalib/types/robots/video-ondemand.d.ts +8 -8
- package/dist/alphalib/types/storageAsset.d.ts +226 -0
- package/dist/alphalib/types/storageAsset.d.ts.map +1 -0
- package/dist/alphalib/types/storageAsset.js +67 -0
- package/dist/alphalib/types/storageAsset.js.map +1 -0
- package/dist/alphalib/types/template.d.ts.map +1 -1
- package/dist/alphalib/types/template.js +5 -2
- package/dist/alphalib/types/template.js.map +1 -1
- package/dist/cli/commands/BaseCommand.d.ts +3 -1
- package/dist/cli/commands/BaseCommand.d.ts.map +1 -1
- package/dist/cli/commands/BaseCommand.js +3 -2
- package/dist/cli/commands/BaseCommand.js.map +1 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +9 -4
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +21 -2
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/image.d.ts +15 -0
- package/dist/cli/commands/image.d.ts.map +1 -0
- package/dist/cli/commands/image.js +234 -0
- package/dist/cli/commands/image.js.map +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +15 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/login.d.ts +24 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +275 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/storage.d.ts +58 -0
- package/dist/cli/commands/storage.d.ts.map +1 -0
- package/dist/cli/commands/storage.js +589 -0
- package/dist/cli/commands/storage.js.map +1 -0
- package/dist/cli/commands/templates.d.ts.map +1 -1
- package/dist/cli/commands/templates.js +10 -6
- package/dist/cli/commands/templates.js.map +1 -1
- package/dist/cli/deviceLogin.d.ts +11 -0
- package/dist/cli/deviceLogin.d.ts.map +1 -0
- package/dist/cli/deviceLogin.js +187 -0
- package/dist/cli/deviceLogin.js.map +1 -0
- package/dist/cli/helpers.d.ts +23 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +159 -25
- package/dist/cli/helpers.js.map +1 -1
- package/dist/cli/secretInput.d.ts +3 -0
- package/dist/cli/secretInput.d.ts.map +1 -0
- package/dist/cli/secretInput.js +11 -0
- package/dist/cli/secretInput.js.map +1 -0
- package/dist/cli/storageCatalog.d.ts +19 -0
- package/dist/cli/storageCatalog.d.ts.map +1 -0
- package/dist/cli/storageCatalog.js +129 -0
- package/dist/cli/storageCatalog.js.map +1 -0
- package/dist/cli/storagePublic.d.ts +3 -0
- package/dist/cli/storagePublic.d.ts.map +1 -0
- package/dist/cli/storagePublic.js +15 -0
- package/dist/cli/storagePublic.js.map +1 -0
- package/dist/cli/storageReceipts.d.ts +67 -0
- package/dist/cli/storageReceipts.d.ts.map +1 -0
- package/dist/cli/storageReceipts.js +242 -0
- package/dist/cli/storageReceipts.js.map +1 -0
- package/dist/cli/storageSnippets.d.ts +11 -0
- package/dist/cli/storageSnippets.d.ts.map +1 -0
- package/dist/cli/storageSnippets.js +89 -0
- package/dist/cli/storageSnippets.js.map +1 -0
- package/dist/storageImage.d.ts +40 -0
- package/dist/storageImage.d.ts.map +1 -0
- package/dist/storageImage.js +139 -0
- package/dist/storageImage.js.map +1 -0
- package/dist/storagePublicPrefixes.d.ts +62 -0
- package/dist/storagePublicPrefixes.d.ts.map +1 -0
- package/dist/storagePublicPrefixes.js +35 -0
- package/dist/storagePublicPrefixes.js.map +1 -0
- package/dist/storageResults.d.ts +31 -0
- package/dist/storageResults.d.ts.map +1 -0
- package/dist/storageResults.js +79 -0
- package/dist/storageResults.js.map +1 -0
- package/package.json +4 -3
- package/src/Transloadit.ts +251 -4
- package/src/alphalib/types/assemblyStatus.ts +17 -0
- package/src/alphalib/types/robots/_index.ts +28 -0
- package/src/alphalib/types/robots/_instructions-primitives.ts +2 -0
- package/src/alphalib/types/robots/file-serve.ts +11 -3
- package/src/alphalib/types/robots/transloadit-import.ts +154 -0
- package/src/alphalib/types/robots/transloadit-store.ts +98 -0
- package/src/alphalib/types/storageAsset.ts +88 -0
- package/src/alphalib/types/template.ts +3 -0
- package/src/cli/commands/BaseCommand.ts +4 -2
- package/src/cli/commands/assemblies.ts +13 -5
- package/src/cli/commands/auth.ts +26 -3
- package/src/cli/commands/image.ts +282 -0
- package/src/cli/commands/index.ts +27 -1
- package/src/cli/commands/login.ts +334 -0
- package/src/cli/commands/storage.ts +733 -0
- package/src/cli/commands/templates.ts +14 -7
- package/src/cli/deviceLogin.ts +226 -0
- package/src/cli/helpers.ts +204 -27
- package/src/cli/secretInput.ts +12 -0
- package/src/cli/storageCatalog.ts +192 -0
- package/src/cli/storagePublic.ts +15 -0
- package/src/cli/storageReceipts.ts +307 -0
- package/src/cli/storageSnippets.ts +96 -0
- package/src/storageImage.ts +222 -0
- package/src/storagePublicPrefixes.ts +39 -0
- package/src/storageResults.ts +119 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import type { StoredImageReceipt } from '../../storageImage.ts'
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { createReadStream } from 'node:fs'
|
|
5
|
+
import { basename, posix, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { validateStoragePath } from '@transloadit/utils'
|
|
8
|
+
import { Command, Option } from 'clipanion'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
|
|
11
|
+
import { ApiError } from '../../ApiError.ts'
|
|
12
|
+
import { storedAssetSchema } from '../../alphalib/types/storageAsset.ts'
|
|
13
|
+
import InconsistentResponseError from '../../InconsistentResponseError.ts'
|
|
14
|
+
import { normalizeStoragePublicPrefix } from '../../storagePublicPrefixes.ts'
|
|
15
|
+
import { Transloadit } from '../../Transloadit.ts'
|
|
16
|
+
import { noticeCliCredentialSource, quoteCliArgument, resolveCliConfig } from '../helpers.ts'
|
|
17
|
+
import {
|
|
18
|
+
listStorageAssets,
|
|
19
|
+
resolveStorageWorkspace,
|
|
20
|
+
withStorageCatalog,
|
|
21
|
+
} from '../storageCatalog.ts'
|
|
22
|
+
import { storagePublicError } from '../storagePublic.ts'
|
|
23
|
+
import {
|
|
24
|
+
assertStorageCatalogOrigin,
|
|
25
|
+
assertStorageWorkspace,
|
|
26
|
+
defaultStorageCatalog,
|
|
27
|
+
readStorageCatalog,
|
|
28
|
+
storageCatalogDelivery,
|
|
29
|
+
storageTypesPath,
|
|
30
|
+
updateStorageReceipts,
|
|
31
|
+
} from '../storageReceipts.ts'
|
|
32
|
+
import { storageImageConfigAdvice, storageImagePrivateAdvice } from '../storageSnippets.ts'
|
|
33
|
+
import { ensureError } from '../types.ts'
|
|
34
|
+
import { AuthenticatedCommand, UnauthenticatedCommand } from './BaseCommand.ts'
|
|
35
|
+
|
|
36
|
+
abstract class StorageProjectCommand extends AuthenticatedCommand {
|
|
37
|
+
protected get apiOrigin(): string {
|
|
38
|
+
return new URL(this.endpoint ?? this.cliConfig.endpoint ?? 'https://api2.transloadit.com')
|
|
39
|
+
.origin
|
|
40
|
+
}
|
|
41
|
+
receipts = Option.String('--receipts', defaultStorageCatalog, {
|
|
42
|
+
description: 'Committed project catalog with workspace, public prefixes and image receipts',
|
|
43
|
+
})
|
|
44
|
+
workspace = Option.String('--workspace', {
|
|
45
|
+
description:
|
|
46
|
+
'Explicit workspace override; a different workspace never changes this project catalog',
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Publishes an explicit directory, independently of uploads or local snippet generation. */
|
|
51
|
+
export class StoragePublishCommand extends StorageProjectCommand {
|
|
52
|
+
static override paths = [['storage', 'publish']]
|
|
53
|
+
static override usage = Command.Usage({
|
|
54
|
+
category: 'Storage',
|
|
55
|
+
description: 'Declare a directory public for unsigned Smart CDN delivery',
|
|
56
|
+
})
|
|
57
|
+
prefix = Option.String({ required: true })
|
|
58
|
+
dryRun = Option.Boolean('--dry-run', false, {
|
|
59
|
+
description: 'List matching objects without publishing or changing the catalog',
|
|
60
|
+
})
|
|
61
|
+
protected async run(): Promise<number | undefined> {
|
|
62
|
+
try {
|
|
63
|
+
noticeCliCredentialSource(this.cliConfig, this.output)
|
|
64
|
+
const prefix = normalizeStoragePublicPrefix(this.prefix)
|
|
65
|
+
if (this.dryRun) {
|
|
66
|
+
const catalog = await readStorageCatalog(this.receipts)
|
|
67
|
+
const objects = await withStorageCatalog(
|
|
68
|
+
{
|
|
69
|
+
endpoint: this.endpoint,
|
|
70
|
+
workspace: this.workspace,
|
|
71
|
+
projectWorkspace: catalog?.workspace,
|
|
72
|
+
},
|
|
73
|
+
(client, workspace) => listStorageAssets(client, workspace, prefix),
|
|
74
|
+
'Publication dry run',
|
|
75
|
+
undefined,
|
|
76
|
+
this.cliConfig,
|
|
77
|
+
)
|
|
78
|
+
this.output.print(
|
|
79
|
+
`Would publish ${prefix} recursively, including future objects. Nothing changed.\n${objects.length === 0 ? 'No stored objects currently match.' : objects.map(({ path, size }) => `${JSON.stringify(path)}\t${size} bytes`).join('\n')}`,
|
|
80
|
+
{ prefix, objects },
|
|
81
|
+
)
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
await updateStorageReceipts(this.receipts, async (previous, signal) => {
|
|
85
|
+
const workspace = await resolveStorageWorkspace(
|
|
86
|
+
this,
|
|
87
|
+
this.cliConfig,
|
|
88
|
+
previous?.workspace,
|
|
89
|
+
signal,
|
|
90
|
+
)
|
|
91
|
+
if (previous?.workspace === workspace)
|
|
92
|
+
assertStorageCatalogOrigin(previous, this.apiOrigin, this.receipts)
|
|
93
|
+
const result = await this.client
|
|
94
|
+
.publishStoragePrefix(prefix, { signal })
|
|
95
|
+
.catch((cause: unknown) => {
|
|
96
|
+
signal.throwIfAborted()
|
|
97
|
+
throw new Error(storagePublicError(cause, workspace), { cause })
|
|
98
|
+
})
|
|
99
|
+
this.output.print(
|
|
100
|
+
`Published ${result.prefix}. Files under this directory can be served without signatures.`,
|
|
101
|
+
result,
|
|
102
|
+
)
|
|
103
|
+
if (previous !== undefined && previous.workspace !== workspace) {
|
|
104
|
+
this.output.notice(
|
|
105
|
+
`Catalog ${this.receipts} was not changed; it belongs to ${previous.workspace}.`,
|
|
106
|
+
)
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
...previous,
|
|
111
|
+
apiOrigin: this.apiOrigin,
|
|
112
|
+
workspace,
|
|
113
|
+
delivery:
|
|
114
|
+
previous?.delivery ?? storageCatalogDelivery(this.endpoint ?? this.cliConfig.endpoint),
|
|
115
|
+
public: [...new Set([...(previous?.public ?? []), result.prefix])],
|
|
116
|
+
images: previous?.images ?? {},
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
return undefined
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.output.error(ensureError(error).message)
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Revokes a public prefix at the origin without promising to recall cached bytes. */
|
|
128
|
+
export class StorageUnpublishCommand extends StorageProjectCommand {
|
|
129
|
+
static override paths = [['storage', 'unpublish']]
|
|
130
|
+
static override usage = Command.Usage({
|
|
131
|
+
category: 'Storage',
|
|
132
|
+
description: 'Revoke public origin access to a directory',
|
|
133
|
+
})
|
|
134
|
+
prefix = Option.String({ required: true })
|
|
135
|
+
protected async run(): Promise<number | undefined> {
|
|
136
|
+
try {
|
|
137
|
+
noticeCliCredentialSource(this.cliConfig, this.output)
|
|
138
|
+
const prefix = normalizeStoragePublicPrefix(this.prefix)
|
|
139
|
+
await updateStorageReceipts(this.receipts, async (previous, signal) => {
|
|
140
|
+
const workspace = await resolveStorageWorkspace(
|
|
141
|
+
this,
|
|
142
|
+
this.cliConfig,
|
|
143
|
+
previous?.workspace,
|
|
144
|
+
signal,
|
|
145
|
+
)
|
|
146
|
+
if (previous?.workspace === workspace)
|
|
147
|
+
assertStorageCatalogOrigin(previous, this.apiOrigin, this.receipts)
|
|
148
|
+
const result = await this.client
|
|
149
|
+
.unpublishStoragePrefix(prefix, { signal })
|
|
150
|
+
.catch((cause: unknown) => {
|
|
151
|
+
signal.throwIfAborted()
|
|
152
|
+
throw new Error(storagePublicError(cause, workspace), { cause })
|
|
153
|
+
})
|
|
154
|
+
this.output.print(
|
|
155
|
+
`Unpublished ${result.prefix}; already cached or downloaded bytes cannot be recalled.`,
|
|
156
|
+
result,
|
|
157
|
+
)
|
|
158
|
+
if (previous !== undefined && previous.workspace !== workspace) {
|
|
159
|
+
this.output.notice(
|
|
160
|
+
`Catalog ${this.receipts} was not changed; it belongs to ${previous.workspace}.`,
|
|
161
|
+
)
|
|
162
|
+
return undefined
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...previous,
|
|
166
|
+
apiOrigin: this.apiOrigin,
|
|
167
|
+
workspace,
|
|
168
|
+
public: (previous?.public ?? []).filter((prefix) => prefix !== result.prefix),
|
|
169
|
+
images: previous?.images ?? {},
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
return undefined
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.output.error(ensureError(error).message)
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Lists explicit publication policy using the signed API, not S3 discovery. */
|
|
181
|
+
export class StoragePublicationsCommand extends AuthenticatedCommand {
|
|
182
|
+
static override paths = [['storage', 'publications']]
|
|
183
|
+
static override usage = Command.Usage({
|
|
184
|
+
category: 'Storage',
|
|
185
|
+
description: 'List explicitly public Storage directories',
|
|
186
|
+
})
|
|
187
|
+
protected async run(): Promise<number | undefined> {
|
|
188
|
+
try {
|
|
189
|
+
noticeCliCredentialSource(this.cliConfig, this.output)
|
|
190
|
+
const result = await this.client.listPublicStoragePrefixes()
|
|
191
|
+
this.output.print(
|
|
192
|
+
result.public_prefixes.length === 0
|
|
193
|
+
? 'No Storage directories are public.'
|
|
194
|
+
: result.public_prefixes.map(({ prefix }) => prefix).join('\n'),
|
|
195
|
+
result,
|
|
196
|
+
)
|
|
197
|
+
return undefined
|
|
198
|
+
} catch (error) {
|
|
199
|
+
this.output.error(storagePublicError(error, this.cliConfig.authWorkspace))
|
|
200
|
+
return 1
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface CliStoredImageReceipt extends StoredImageReceipt {
|
|
206
|
+
source?: string
|
|
207
|
+
apiOrigin?: string
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function hashImageFile(
|
|
211
|
+
file: string,
|
|
212
|
+
signal: AbortSignal,
|
|
213
|
+
): Promise<{ md5hash: string; size: number }> {
|
|
214
|
+
const hash = createHash('md5')
|
|
215
|
+
let size = 0
|
|
216
|
+
// A bounded preflight lets the CLI skip an upload. storeImage still independently verifies it.
|
|
217
|
+
for await (const chunk of createReadStream(file, { signal })) {
|
|
218
|
+
hash.update(chunk)
|
|
219
|
+
size += chunk.length
|
|
220
|
+
}
|
|
221
|
+
if (size === 0) throw new Error('Cannot store an empty image')
|
|
222
|
+
signal.throwIfAborted()
|
|
223
|
+
return { md5hash: hash.digest('hex'), size }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Stores originals in order, checkpointing each verified receipt before the next upload. */
|
|
227
|
+
export class StorageStoreCommand extends StorageProjectCommand {
|
|
228
|
+
static override paths = [['storage', 'store']]
|
|
229
|
+
|
|
230
|
+
static override usage = Command.Usage({
|
|
231
|
+
category: 'Storage',
|
|
232
|
+
description: 'Store original images and save verified metadata for Image',
|
|
233
|
+
details: `
|
|
234
|
+
Uses the CLI's Assembly credentials (environment, .env or ~/.transloadit/credentials).
|
|
235
|
+
Storage writes must be enabled. Existing Storage paths conflict unless --overwrite is explicit.
|
|
236
|
+
--hashed inserts eight MD5 hex digits before the extension. Matching catalog receipts skip
|
|
237
|
+
repeat uploads; changed bytes get a fresh name. --hashed cannot be combined with --overwrite.
|
|
238
|
+
The project catalog binds workspace, published prefixes and image receipts. Each successful
|
|
239
|
+
upload is saved atomically before the next. Do not run two writers against the same catalog.
|
|
240
|
+
The catalog defaults to transloadit.images.json; --receipts selects another file.
|
|
241
|
+
`,
|
|
242
|
+
examples: [
|
|
243
|
+
['Store a hero image', 'transloadit storage store ./hero.jpg website/hero.jpg'],
|
|
244
|
+
['Store a content-addressed image', 'transloadit storage store ./hero.jpg website/ --hashed'],
|
|
245
|
+
['Store a directory of originals', 'transloadit storage store ./images/*.jpg website/'],
|
|
246
|
+
],
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
files = Option.Rest({ required: 1 })
|
|
250
|
+
destination = Option.String({ required: true })
|
|
251
|
+
hashed = Option.Boolean('--hashed', false, {
|
|
252
|
+
description: 'Add an eight-digit content hash to the filename; reuse matching catalog receipts',
|
|
253
|
+
})
|
|
254
|
+
overwrite = Option.Boolean('--overwrite', false, {
|
|
255
|
+
description: 'Explicitly replace an existing Storage path',
|
|
256
|
+
})
|
|
257
|
+
publicDelivery = Option.Boolean('--public', false, {
|
|
258
|
+
description:
|
|
259
|
+
'Publish the destination directory recursively, including current and future objects',
|
|
260
|
+
})
|
|
261
|
+
protected async run(): Promise<number | undefined> {
|
|
262
|
+
const file = resolve(this.receipts)
|
|
263
|
+
let stored: { receipt?: CliStoredImageReceipt } = {}
|
|
264
|
+
let destination = this.destination
|
|
265
|
+
let workspace: string | undefined
|
|
266
|
+
let saved = false
|
|
267
|
+
let published: string | undefined
|
|
268
|
+
try {
|
|
269
|
+
noticeCliCredentialSource(this.cliConfig, this.output)
|
|
270
|
+
if (this.hashed && this.overwrite)
|
|
271
|
+
throw new Error(
|
|
272
|
+
'--hashed cannot be combined with --overwrite; changed bytes get a new name',
|
|
273
|
+
)
|
|
274
|
+
const apiOrigin = this.apiOrigin
|
|
275
|
+
const uploaded = new Map<string, CliStoredImageReceipt>()
|
|
276
|
+
if (this.files.length > 1 && !this.destination.endsWith('/'))
|
|
277
|
+
throw new Error('Multiple images need a directory destination ending in /')
|
|
278
|
+
const inputs = this.files.map((input) => ({
|
|
279
|
+
file: input,
|
|
280
|
+
path: this.destination.endsWith('/')
|
|
281
|
+
? `${this.destination}${basename(input)}`
|
|
282
|
+
: this.destination,
|
|
283
|
+
}))
|
|
284
|
+
for (const input of inputs) {
|
|
285
|
+
if (file === resolve(input.file))
|
|
286
|
+
throw new Error('The receipts file cannot be the input image')
|
|
287
|
+
if (resolve(storageTypesPath(file)) === resolve(input.file))
|
|
288
|
+
throw new Error('The generated declarations file cannot be the input image')
|
|
289
|
+
validateStoragePath(input.path)
|
|
290
|
+
}
|
|
291
|
+
if (!this.hashed && new Set(inputs.map((input) => input.path)).size !== inputs.length)
|
|
292
|
+
throw new Error(
|
|
293
|
+
'Input image basenames collide in the destination directory; rename them first',
|
|
294
|
+
)
|
|
295
|
+
const publicPrefix = this.publicDelivery
|
|
296
|
+
? normalizeStoragePublicPrefix(
|
|
297
|
+
this.destination.slice(0, this.destination.lastIndexOf('/') + 1),
|
|
298
|
+
)
|
|
299
|
+
: undefined
|
|
300
|
+
let setupPrinted = false
|
|
301
|
+
for (const input of inputs) {
|
|
302
|
+
let publicImage = false
|
|
303
|
+
let unchanged = false
|
|
304
|
+
destination = input.path
|
|
305
|
+
stored = {}
|
|
306
|
+
saved = false
|
|
307
|
+
await updateStorageReceipts(
|
|
308
|
+
file,
|
|
309
|
+
async (receipts, signal) => {
|
|
310
|
+
workspace ??= await resolveStorageWorkspace(
|
|
311
|
+
this,
|
|
312
|
+
this.cliConfig,
|
|
313
|
+
receipts?.workspace,
|
|
314
|
+
signal,
|
|
315
|
+
)
|
|
316
|
+
assertStorageWorkspace(workspace, receipts?.workspace, this.workspace)
|
|
317
|
+
if (receipts?.workspace === workspace)
|
|
318
|
+
assertStorageCatalogOrigin(receipts, apiOrigin, this.receipts)
|
|
319
|
+
signal.throwIfAborted()
|
|
320
|
+
if (this.hashed) {
|
|
321
|
+
// This pass chooses the path and deduplicates. storeImage independently verifies its
|
|
322
|
+
// upload input; sharing a caller-supplied checksum would need an immutable-file contract.
|
|
323
|
+
const { md5hash, size } = await hashImageFile(input.file, signal)
|
|
324
|
+
const extension = posix.extname(destination)
|
|
325
|
+
destination = `${destination.slice(0, destination.length - extension.length)}.${md5hash.slice(0, 8)}${extension}`
|
|
326
|
+
validateStoragePath(destination)
|
|
327
|
+
// Explicit workspace overrides leave the catalog alone; still deduplicate this batch.
|
|
328
|
+
const candidate =
|
|
329
|
+
uploaded.get(destination) ??
|
|
330
|
+
(receipts?.workspace === workspace && Object.hasOwn(receipts.images, destination)
|
|
331
|
+
? receipts.images[destination]
|
|
332
|
+
: undefined)
|
|
333
|
+
if (candidate !== undefined) {
|
|
334
|
+
const previous = hashedReceiptSchema.safeParse(candidate)
|
|
335
|
+
if (!previous.success || previous.data.path !== destination)
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Catalog receipt for ${JSON.stringify(destination)} is incomplete or does not match this file. Run transloadit storage receipts sync ${quoteCliArgument(destination)} --receipts ${quoteCliArgument(this.receipts)}${this.endpoint === undefined ? '' : ` --endpoint ${quoteCliArgument(this.endpoint)}`} to recover version identity from the same API environment, then retry; nothing uploaded.`,
|
|
338
|
+
)
|
|
339
|
+
if (previous.data.apiOrigin !== apiOrigin)
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Cannot verify that ${JSON.stringify(destination)} was stored at ${apiOrigin}. Use --receipts for a separate catalog for this API environment; nothing uploaded.`,
|
|
342
|
+
)
|
|
343
|
+
if (previous.data.md5hash !== md5hash || previous.data.size !== size)
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Stored bytes for ${JSON.stringify(destination)} differ from this file. An older deployment may have transformed the upload, or the short hashes collided. Restoring the same receipt will not help; choose another destination basename. Nothing uploaded or replaced.`,
|
|
346
|
+
)
|
|
347
|
+
stored.receipt = previous.data
|
|
348
|
+
unchanged = true
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
stored.receipt ??= await this.client.storeImage(input.file, {
|
|
352
|
+
path: destination,
|
|
353
|
+
signal,
|
|
354
|
+
onReceipt: (receipt, expected, assemblyId) => {
|
|
355
|
+
const sizeMatches = receipt.size === expected.size
|
|
356
|
+
const md5Matches = receipt.md5hash === expected.md5hash
|
|
357
|
+
this.output.debug(
|
|
358
|
+
JSON.stringify({
|
|
359
|
+
assemblyId,
|
|
360
|
+
result: receipt,
|
|
361
|
+
input: expected,
|
|
362
|
+
sizeMatches,
|
|
363
|
+
md5Matches,
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
366
|
+
if (!sizeMatches || !md5Matches) {
|
|
367
|
+
const difference = sizeMatches
|
|
368
|
+
? 'same size, different MD5'
|
|
369
|
+
: `${expected.size.toLocaleString('en-US')} → ${receipt.size.toLocaleString('en-US')} bytes`
|
|
370
|
+
this.output.warn(
|
|
371
|
+
`Stored bytes differ from ${input.file} (${difference}); the workspace plan may have transformed the upload (for example, a Community-plan watermark on older deployments). The receipt describes the stored image.`,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
...(this.overwrite ? { overwrite: true } : {}),
|
|
376
|
+
})
|
|
377
|
+
if (stored.receipt.workspace !== workspace)
|
|
378
|
+
throw new Error(
|
|
379
|
+
`The stored receipt belongs to Workspace ${stored.receipt.workspace}, not ${workspace}. The project catalog was not changed; inspect the Assembly before retrying.`,
|
|
380
|
+
)
|
|
381
|
+
if (!unchanged)
|
|
382
|
+
stored.receipt = {
|
|
383
|
+
...stored.receipt,
|
|
384
|
+
...(this.hashed ? { source: basename(input.file) } : {}),
|
|
385
|
+
apiOrigin,
|
|
386
|
+
}
|
|
387
|
+
if (this.hashed) uploaded.set(destination, stored.receipt)
|
|
388
|
+
if (receipts !== undefined && receipts.workspace !== workspace) {
|
|
389
|
+
this.output.notice(
|
|
390
|
+
`Catalog ${this.receipts} was not changed; it belongs to ${receipts.workspace}. Use --receipts for a separate catalog.`,
|
|
391
|
+
)
|
|
392
|
+
return undefined
|
|
393
|
+
}
|
|
394
|
+
publicImage = receipts?.public.some((prefix) => destination.startsWith(prefix)) ?? false
|
|
395
|
+
// Replays must not reorder fields or discard application metadata from a saved receipt.
|
|
396
|
+
if (unchanged) return receipts
|
|
397
|
+
return {
|
|
398
|
+
...receipts,
|
|
399
|
+
workspace,
|
|
400
|
+
apiOrigin,
|
|
401
|
+
delivery:
|
|
402
|
+
receipts?.delivery ??
|
|
403
|
+
storageCatalogDelivery(this.endpoint ?? this.cliConfig.endpoint),
|
|
404
|
+
public: receipts?.public ?? [],
|
|
405
|
+
images: { ...receipts?.images, [stored.receipt.path]: stored.receipt },
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
() => {
|
|
409
|
+
saved = true
|
|
410
|
+
},
|
|
411
|
+
)
|
|
412
|
+
if (stored.receipt === undefined) throw new Error('Storage did not return a receipt')
|
|
413
|
+
if (publicPrefix !== undefined && published === undefined) {
|
|
414
|
+
this.output.notice(
|
|
415
|
+
`Publishing ${publicPrefix} recursively: all current and future objects under this prefix will be public.`,
|
|
416
|
+
)
|
|
417
|
+
await updateStorageReceipts(file, async (previous, signal) => {
|
|
418
|
+
if (previous?.workspace === workspace)
|
|
419
|
+
assertStorageCatalogOrigin(previous, apiOrigin, this.receipts)
|
|
420
|
+
const result = await this.client
|
|
421
|
+
.publishStoragePrefix(publicPrefix, { signal })
|
|
422
|
+
.catch((cause: unknown) => {
|
|
423
|
+
signal.throwIfAborted()
|
|
424
|
+
throw new Error(storagePublicError(cause, workspace), { cause })
|
|
425
|
+
})
|
|
426
|
+
published = result.prefix
|
|
427
|
+
if (previous === undefined || previous.workspace !== workspace) return undefined
|
|
428
|
+
return { ...previous, public: [...new Set([...previous.public, result.prefix])] }
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
const receipt = stored.receipt
|
|
432
|
+
publicImage ||= published !== undefined && receipt.path.startsWith(published)
|
|
433
|
+
const blur =
|
|
434
|
+
saved && publicImage && receipt.thumbhash !== undefined && receipt.hasAlpha !== true
|
|
435
|
+
// A foreign catalog says nothing about this destination's policy or rendering setup.
|
|
436
|
+
const setupAdvice =
|
|
437
|
+
!saved || setupPrinted
|
|
438
|
+
? ''
|
|
439
|
+
: `${publicImage ? '' : storageImagePrivateAdvice(receipt.path, this.receipts === defaultStorageCatalog ? undefined : this.receipts)}${await storageImageConfigAdvice()}`
|
|
440
|
+
setupPrinted ||= saved
|
|
441
|
+
const attribute = (value: string): string =>
|
|
442
|
+
value
|
|
443
|
+
.replaceAll('&', '&')
|
|
444
|
+
.replaceAll('"', '"')
|
|
445
|
+
.replaceAll('<', '<')
|
|
446
|
+
.replaceAll('>', '>')
|
|
447
|
+
const src = attribute(receipt.path)
|
|
448
|
+
const alt = attribute(
|
|
449
|
+
basename(receipt.source ?? receipt.path)
|
|
450
|
+
.replace(/\.[^.]+$/, '')
|
|
451
|
+
.replaceAll(/[-_]+/g, ' '),
|
|
452
|
+
)
|
|
453
|
+
const renderAdvice = saved
|
|
454
|
+
? `Render it with <Image storage src="${src}" alt="${alt}" width={${Math.min(receipt.width, 960)}}${blur ? ' placeholder="blur"' : ''} />\nReplace alt with a description (or an empty string for a decorative image).${setupAdvice}`
|
|
455
|
+
: 'Use --receipts for a separate catalog before rendering images from this workspace.'
|
|
456
|
+
this.output.print(
|
|
457
|
+
`${unchanged ? `Unchanged ${receipt.path}; no upload needed.\n` : ''}${saved ? `Saved ${receipt.path} in ${this.receipts}. Commit this catalog and ${storageTypesPath(this.receipts)}.` : `Stored ${receipt.path}; the different-workspace project catalog was left unchanged.`}\n${renderAdvice}`,
|
|
458
|
+
receipt,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
return undefined
|
|
462
|
+
} catch (error) {
|
|
463
|
+
const failure = ensureError(error)
|
|
464
|
+
if (stored.receipt !== undefined) {
|
|
465
|
+
this.output.error(
|
|
466
|
+
[
|
|
467
|
+
failure.message,
|
|
468
|
+
...(published === undefined
|
|
469
|
+
? []
|
|
470
|
+
: [
|
|
471
|
+
`The server prefix ${published} remains public; use storage unpublish deliberately if needed.`,
|
|
472
|
+
]),
|
|
473
|
+
saved
|
|
474
|
+
? `Receipt saved in ${this.receipts}. No further files were uploaded. Do not re-upload this object.`
|
|
475
|
+
: 'The object was stored successfully. Do not re-upload; recover the verified receipt below.',
|
|
476
|
+
`Receipt: ${JSON.stringify(stored.receipt)}`,
|
|
477
|
+
].join('\n'),
|
|
478
|
+
)
|
|
479
|
+
return 1
|
|
480
|
+
}
|
|
481
|
+
const recovery =
|
|
482
|
+
failure instanceof InconsistentResponseError
|
|
483
|
+
? z
|
|
484
|
+
.object({
|
|
485
|
+
assemblyId: z.string().min(1),
|
|
486
|
+
receiptCheck: z
|
|
487
|
+
.object({
|
|
488
|
+
originalCount: z.number().int().nonnegative(),
|
|
489
|
+
metadataValid: z.boolean(),
|
|
490
|
+
pathMatches: z.boolean().optional(),
|
|
491
|
+
sizeMatches: z.boolean().optional(),
|
|
492
|
+
md5Matches: z.boolean().optional(),
|
|
493
|
+
})
|
|
494
|
+
.optional(),
|
|
495
|
+
})
|
|
496
|
+
.safeParse(failure.cause)
|
|
497
|
+
: undefined
|
|
498
|
+
if (recovery?.success) {
|
|
499
|
+
this.output.debug(JSON.stringify(recovery.data))
|
|
500
|
+
const options = [
|
|
501
|
+
`--receipts ${quoteCliArgument(this.receipts)}`,
|
|
502
|
+
...(this.endpoint ? [`--endpoint ${quoteCliArgument(this.endpoint)}`] : []),
|
|
503
|
+
...(this.workspace ? [`--workspace ${quoteCliArgument(this.workspace)}`] : []),
|
|
504
|
+
].join(' ')
|
|
505
|
+
this.output.error(
|
|
506
|
+
[
|
|
507
|
+
failure.message,
|
|
508
|
+
`Destination: ${JSON.stringify(destination)}`,
|
|
509
|
+
`Assembly ID: ${JSON.stringify(recovery.data.assemblyId)}`,
|
|
510
|
+
...(recovery.data.receiptCheck === undefined
|
|
511
|
+
? []
|
|
512
|
+
: [
|
|
513
|
+
'The Assembly did not return usable receipt metadata. Do not re-upload; inspect Storage and recover its metadata:',
|
|
514
|
+
'The commands below require the native Storage catalog API. On an older deployment without these routes, inspect the Assembly in Console and restore a verified catalog receipt.',
|
|
515
|
+
// A filename prefix also works for root objects without scanning the workspace.
|
|
516
|
+
`transloadit storage ls ${quoteCliArgument(destination)} ${options}`,
|
|
517
|
+
`transloadit storage receipts sync ${quoteCliArgument(destination)} ${options}`,
|
|
518
|
+
]),
|
|
519
|
+
].join('\n'),
|
|
520
|
+
)
|
|
521
|
+
return 1
|
|
522
|
+
}
|
|
523
|
+
if (failure instanceof ApiError && failure.code === 'TRANSLOADIT_STORE_CONFLICT') {
|
|
524
|
+
this.output.error(
|
|
525
|
+
this.hashed
|
|
526
|
+
? `Storage destination ${JSON.stringify(destination)} already exists, but no matching catalog receipt proves its contents. Restore its catalog receipt or choose another destination basename; the object was not replaced.`
|
|
527
|
+
: `Storage destination ${JSON.stringify(destination)} already exists. Choose a fresh name; use --overwrite only if you deliberately want to replace that object.`,
|
|
528
|
+
)
|
|
529
|
+
return 1
|
|
530
|
+
}
|
|
531
|
+
this.output.error(failure.message)
|
|
532
|
+
return 1
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Lists version-pinned metadata through the authenticated native Storage catalog. */
|
|
538
|
+
export class StorageListCommand extends UnauthenticatedCommand {
|
|
539
|
+
static override paths = [['storage', 'ls']]
|
|
540
|
+
static override usage = Command.Usage({
|
|
541
|
+
category: 'Storage',
|
|
542
|
+
description: 'List stored paths and sizes; include asset/version identities with --json',
|
|
543
|
+
details:
|
|
544
|
+
'Uses an Auth Key with dam:read or dam:write scope and bounded native catalog pages. Verifies the Workspace against the selected credentials. No Assembly, S3 request or original download is needed.',
|
|
545
|
+
examples: [['List website images', 'transloadit storage ls website/']],
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
prefix = Option.String({ required: true })
|
|
549
|
+
workspace = Option.String('--workspace', {
|
|
550
|
+
description: 'Explicit workspace slug (otherwise discovered from this Auth Key)',
|
|
551
|
+
})
|
|
552
|
+
receipts = Option.String('--receipts', defaultStorageCatalog, {
|
|
553
|
+
description: 'Project catalog whose workspace must match the selected credentials',
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
protected async run(): Promise<number | undefined> {
|
|
557
|
+
try {
|
|
558
|
+
const catalog = await readStorageCatalog(this.receipts)
|
|
559
|
+
const objects = await withStorageCatalog(
|
|
560
|
+
{
|
|
561
|
+
endpoint: this.endpoint,
|
|
562
|
+
workspace: this.workspace,
|
|
563
|
+
projectWorkspace: catalog?.workspace,
|
|
564
|
+
},
|
|
565
|
+
(client, workspace) => listStorageAssets(client, workspace, this.prefix),
|
|
566
|
+
'Storage listing',
|
|
567
|
+
this.output,
|
|
568
|
+
)
|
|
569
|
+
this.output.print(
|
|
570
|
+
objects.length === 0
|
|
571
|
+
? 'No stored objects match this prefix.'
|
|
572
|
+
: objects
|
|
573
|
+
.map((object) => `${JSON.stringify(object.path)}\t${object.size} bytes`)
|
|
574
|
+
.join('\n'),
|
|
575
|
+
objects,
|
|
576
|
+
)
|
|
577
|
+
return undefined
|
|
578
|
+
} catch (error) {
|
|
579
|
+
this.output.error(ensureError(error).message)
|
|
580
|
+
return 1
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const uploadEvidenceSchema = storedAssetSchema.extend({
|
|
586
|
+
thumbhash: z.string().max(48).optional(),
|
|
587
|
+
hasAlpha: z.boolean().optional(),
|
|
588
|
+
source: z.string().optional(),
|
|
589
|
+
apiOrigin: z.string().url().optional(),
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
const hashedReceiptSchema = uploadEvidenceSchema.extend({
|
|
593
|
+
md5hash: storedAssetSchema.shape.md5hash.unwrap(),
|
|
594
|
+
width: storedAssetSchema.shape.width.unwrap(),
|
|
595
|
+
height: storedAssetSchema.shape.height.unwrap(),
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
/** Recovers pinned receipts from bounded native catalog pages without downloading originals. */
|
|
599
|
+
export class StorageReceiptsSyncCommand extends UnauthenticatedCommand {
|
|
600
|
+
static override paths = [['storage', 'receipts', 'sync']]
|
|
601
|
+
static override usage = Command.Usage({
|
|
602
|
+
category: 'Storage',
|
|
603
|
+
description: 'Rebuild saved rendering metadata from the Storage catalog',
|
|
604
|
+
details: `
|
|
605
|
+
Uses native catalog pages and dam:write scope to also recover declared public prefixes.
|
|
606
|
+
Adds or refreshes matched paths; never prunes unmatched entries. Local placeholder metadata
|
|
607
|
+
survives only for the same retained version. Every matched image must have catalog dimensions.
|
|
608
|
+
Any failure preserves the previous file. No Assembly, original download or remote write occurs.
|
|
609
|
+
`,
|
|
610
|
+
examples: [['Recover website images', 'transloadit storage receipts sync website/']],
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
prefix = Option.String({ required: true })
|
|
614
|
+
workspace = Option.String('--workspace', {
|
|
615
|
+
description: 'Explicit workspace slug (otherwise discovered from this Auth Key)',
|
|
616
|
+
})
|
|
617
|
+
receipts = Option.String('--receipts', defaultStorageCatalog, {
|
|
618
|
+
description: 'JSON rendering catalog to update atomically (default: transloadit.images.json)',
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
protected async run(): Promise<number | undefined> {
|
|
622
|
+
try {
|
|
623
|
+
let count = 0
|
|
624
|
+
let synced: Record<string, unknown> = {}
|
|
625
|
+
let catalogUpdated = false
|
|
626
|
+
const config = resolveCliConfig()
|
|
627
|
+
await updateStorageReceipts(resolve(this.receipts), async (previous, signal) => {
|
|
628
|
+
let actualWorkspace: string | undefined
|
|
629
|
+
let apiOrigin: string | undefined
|
|
630
|
+
synced = await withStorageCatalog(
|
|
631
|
+
{
|
|
632
|
+
endpoint: this.endpoint,
|
|
633
|
+
workspace: this.workspace,
|
|
634
|
+
projectWorkspace: previous?.workspace,
|
|
635
|
+
signal,
|
|
636
|
+
},
|
|
637
|
+
async (client, workspace, endpoint) => {
|
|
638
|
+
actualWorkspace = workspace
|
|
639
|
+
apiOrigin = endpoint
|
|
640
|
+
if (previous?.workspace === workspace)
|
|
641
|
+
assertStorageCatalogOrigin(previous, endpoint, this.receipts)
|
|
642
|
+
const assets = await listStorageAssets(client, workspace, this.prefix, signal)
|
|
643
|
+
const entries = assets.map((asset) => {
|
|
644
|
+
try {
|
|
645
|
+
validateStoragePath(asset.path)
|
|
646
|
+
} catch (cause) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Storage path ${JSON.stringify(asset.path)} is valid for listing but unsupported by the image renderer. Rename it before recovering an image catalog.`,
|
|
649
|
+
{ cause },
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
if (asset.width === undefined || asset.height === undefined) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Storage image ${JSON.stringify(asset.path)} needs positive catalog width and height. Select an image-only prefix and backfill missing dimensions before retrying.`,
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
const evidence = uploadEvidenceSchema.safeParse(
|
|
658
|
+
previous?.workspace === workspace && Object.hasOwn(previous.images, asset.path)
|
|
659
|
+
? previous.images[asset.path]
|
|
660
|
+
: undefined,
|
|
661
|
+
)
|
|
662
|
+
const retained =
|
|
663
|
+
evidence.success &&
|
|
664
|
+
evidence.data.asset_id === asset.asset_id &&
|
|
665
|
+
evidence.data.version_id === asset.version_id
|
|
666
|
+
? evidence.data
|
|
667
|
+
: {}
|
|
668
|
+
return [asset.path, { ...retained, ...asset, apiOrigin: endpoint }]
|
|
669
|
+
})
|
|
670
|
+
count = entries.length
|
|
671
|
+
return Object.fromEntries(entries)
|
|
672
|
+
},
|
|
673
|
+
'Storage receipt sync',
|
|
674
|
+
this.output,
|
|
675
|
+
config,
|
|
676
|
+
)
|
|
677
|
+
if (actualWorkspace === undefined) throw new Error('Storage did not identify a workspace')
|
|
678
|
+
if (previous !== undefined && previous.workspace !== actualWorkspace) {
|
|
679
|
+
this.output.notice(
|
|
680
|
+
`Catalog ${this.receipts} was not changed; it belongs to ${previous.workspace}. Use --receipts for a separate catalog.`,
|
|
681
|
+
)
|
|
682
|
+
return undefined
|
|
683
|
+
}
|
|
684
|
+
// Recover policy with the same key/endpoint as catalog reads, never an unrelated bearer
|
|
685
|
+
// token or a folder-name guess. Neither half is committed if this read fails.
|
|
686
|
+
if (config.credentials === undefined) throw new Error('Storage credentials are missing')
|
|
687
|
+
const policyClient = new Transloadit({
|
|
688
|
+
...config.credentials,
|
|
689
|
+
endpoint: new URL(
|
|
690
|
+
this.endpoint ?? config.credentialsEndpoint ?? 'https://api2.transloadit.com',
|
|
691
|
+
).origin,
|
|
692
|
+
maxRetries: 0,
|
|
693
|
+
})
|
|
694
|
+
const policy = await policyClient
|
|
695
|
+
.listPublicStoragePrefixes({
|
|
696
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(60_000)]),
|
|
697
|
+
})
|
|
698
|
+
.catch((error: unknown) => {
|
|
699
|
+
signal.throwIfAborted()
|
|
700
|
+
throw new Error(
|
|
701
|
+
'Recovery incomplete: could not read the server public prefixes. Check the API endpoint and Auth Key dam:write scope, then retry. The existing catalog was preserved.',
|
|
702
|
+
{ cause: error },
|
|
703
|
+
)
|
|
704
|
+
})
|
|
705
|
+
catalogUpdated = true
|
|
706
|
+
if (policy.public_prefixes.length === 0)
|
|
707
|
+
this.output.notice(
|
|
708
|
+
'No public prefixes are declared on the server. For public delivery, deliberately publish a directory with storage publish; otherwise configure authorize for private images. Sync never publishes files.',
|
|
709
|
+
)
|
|
710
|
+
return {
|
|
711
|
+
...previous,
|
|
712
|
+
apiOrigin,
|
|
713
|
+
workspace: actualWorkspace,
|
|
714
|
+
delivery:
|
|
715
|
+
previous?.delivery ??
|
|
716
|
+
storageCatalogDelivery(this.endpoint ?? config.credentialsEndpoint),
|
|
717
|
+
public: policy.public_prefixes.map(({ prefix }) => prefix),
|
|
718
|
+
images: { ...previous?.images, ...synced },
|
|
719
|
+
}
|
|
720
|
+
})
|
|
721
|
+
this.output.print(
|
|
722
|
+
catalogUpdated
|
|
723
|
+
? `Synced ${count} rendering receipts and public policy to ${this.receipts}. Unmatched entries were preserved. Commit this file before building.`
|
|
724
|
+
: `Read ${count} rendering receipts. Catalog unchanged because it belongs to another workspace.`,
|
|
725
|
+
synced,
|
|
726
|
+
)
|
|
727
|
+
return undefined
|
|
728
|
+
} catch (error) {
|
|
729
|
+
this.output.error(ensureError(error).message)
|
|
730
|
+
return 1
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|