@sanity/export 5.0.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AssetHandler.d.ts +47 -0
- package/dist/AssetHandler.d.ts.map +1 -0
- package/dist/AssetHandler.js +384 -0
- package/dist/AssetHandler.js.map +1 -0
- package/dist/constants.d.ts +45 -0
- package/dist/constants.d.ts.map +1 -0
- package/{src → dist}/constants.js +13 -18
- package/dist/constants.js.map +1 -0
- package/dist/debug.d.ts +3 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +3 -0
- package/dist/debug.js.map +1 -0
- package/dist/export.d.ts +43 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +269 -0
- package/dist/export.js.map +1 -0
- package/dist/filterDocumentTypes.d.ts +3 -0
- package/dist/filterDocumentTypes.d.ts.map +1 -0
- package/dist/filterDocumentTypes.js +16 -0
- package/dist/filterDocumentTypes.js.map +1 -0
- package/dist/filterDocuments.d.ts +3 -0
- package/dist/filterDocuments.d.ts.map +1 -0
- package/dist/filterDocuments.js +36 -0
- package/dist/filterDocuments.js.map +1 -0
- package/dist/getDocumentCursorStream.d.ts +4 -0
- package/dist/getDocumentCursorStream.d.ts.map +1 -0
- package/dist/getDocumentCursorStream.js +85 -0
- package/dist/getDocumentCursorStream.js.map +1 -0
- package/dist/getDocumentsStream.d.ts +5 -0
- package/dist/getDocumentsStream.d.ts.map +1 -0
- package/dist/getDocumentsStream.js +28 -0
- package/dist/getDocumentsStream.js.map +1 -0
- package/dist/getUserAgent.d.ts +2 -0
- package/dist/getUserAgent.d.ts.map +1 -0
- package/dist/getUserAgent.js +12 -0
- package/dist/getUserAgent.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/logFirstChunk.d.ts +3 -0
- package/dist/logFirstChunk.d.ts.map +1 -0
- package/dist/logFirstChunk.js +14 -0
- package/dist/logFirstChunk.js.map +1 -0
- package/dist/options.d.ts +14 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +97 -0
- package/dist/options.js.map +1 -0
- package/dist/rejectOnApiError.d.ts +3 -0
- package/dist/rejectOnApiError.d.ts.map +1 -0
- package/dist/rejectOnApiError.js +35 -0
- package/dist/rejectOnApiError.js.map +1 -0
- package/dist/requestStream.d.ts +3 -0
- package/dist/requestStream.d.ts.map +1 -0
- package/dist/requestStream.js +48 -0
- package/dist/requestStream.js.map +1 -0
- package/dist/stringifyStream.d.ts +3 -0
- package/dist/stringifyStream.d.ts.map +1 -0
- package/dist/stringifyStream.js +5 -0
- package/dist/stringifyStream.js.map +1 -0
- package/dist/tryParseJson.d.ts +10 -0
- package/dist/tryParseJson.d.ts.map +1 -0
- package/dist/tryParseJson.js +36 -0
- package/dist/tryParseJson.js.map +1 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util/delay.d.ts +2 -0
- package/dist/util/delay.d.ts.map +1 -0
- package/dist/util/delay.js +4 -0
- package/dist/util/delay.js.map +1 -0
- package/dist/util/extractFirstError.d.ts +2 -0
- package/dist/util/extractFirstError.d.ts.map +1 -0
- package/dist/util/extractFirstError.js +22 -0
- package/dist/util/extractFirstError.js.map +1 -0
- package/dist/util/friendlyError.d.ts +2 -0
- package/dist/util/friendlyError.d.ts.map +1 -0
- package/dist/util/friendlyError.js +49 -0
- package/dist/util/friendlyError.js.map +1 -0
- package/dist/util/streamHelpers.d.ts +10 -0
- package/dist/util/streamHelpers.d.ts.map +1 -0
- package/dist/util/streamHelpers.js +99 -0
- package/dist/util/streamHelpers.js.map +1 -0
- package/package.json +22 -7
- package/src/{AssetHandler.js → AssetHandler.ts} +174 -99
- package/src/constants.ts +50 -0
- package/src/debug.ts +3 -0
- package/src/{export.js → export.ts} +110 -70
- package/src/filterDocumentTypes.ts +21 -0
- package/src/filterDocuments.ts +55 -0
- package/src/{getDocumentCursorStream.js → getDocumentCursorStream.ts} +37 -18
- package/src/{getDocumentsStream.js → getDocumentsStream.ts} +16 -6
- package/src/{getUserAgent.js → getUserAgent.ts} +8 -3
- package/src/index.ts +11 -0
- package/src/{logFirstChunk.js → logFirstChunk.ts} +6 -4
- package/src/options.ts +138 -0
- package/src/rejectOnApiError.ts +62 -0
- package/src/requestStream.ts +81 -0
- package/src/stringifyStream.ts +7 -0
- package/src/{tryParseJson.js → tryParseJson.ts} +29 -17
- package/src/types.ts +274 -0
- package/src/util/{delay.js → delay.ts} +1 -1
- package/src/util/extractFirstError.ts +31 -0
- package/src/util/friendlyError.ts +75 -0
- package/src/util/{streamHelpers.js → streamHelpers.ts} +35 -18
- package/src/debug.js +0 -3
- package/src/filterDocumentTypes.js +0 -18
- package/src/filterDocuments.js +0 -33
- package/src/rejectOnApiError.js +0 -31
- package/src/requestStream.js +0 -64
- package/src/stringifyStream.js +0 -5
- package/src/util/extractFirstError.js +0 -14
- package/src/util/friendlyError.js +0 -58
- package/src/validateOptions.js +0 -113
|
@@ -4,8 +4,9 @@ import {join as joinPath} from 'node:path'
|
|
|
4
4
|
import {pipeline} from 'node:stream/promises'
|
|
5
5
|
|
|
6
6
|
import PQueue from 'p-queue'
|
|
7
|
-
import {rimraf} from 'rimraf'
|
|
8
7
|
|
|
8
|
+
import {delay} from './util/delay.js'
|
|
9
|
+
import {through, throughObj} from './util/streamHelpers.js'
|
|
9
10
|
import {
|
|
10
11
|
ASSET_DOWNLOAD_CONCURRENCY,
|
|
11
12
|
ASSET_DOWNLOAD_MAX_RETRIES,
|
|
@@ -14,16 +15,70 @@ import {
|
|
|
14
15
|
import {debug} from './debug.js'
|
|
15
16
|
import {getUserAgent} from './getUserAgent.js'
|
|
16
17
|
import {requestStream} from './requestStream.js'
|
|
17
|
-
import {
|
|
18
|
-
|
|
18
|
+
import type {
|
|
19
|
+
AssetDocument,
|
|
20
|
+
AssetMap,
|
|
21
|
+
AssetMetadata,
|
|
22
|
+
ResponseStream,
|
|
23
|
+
SanityClientLike,
|
|
24
|
+
SanityDocument,
|
|
25
|
+
} from './types.js'
|
|
26
|
+
import {rm} from 'node:fs/promises'
|
|
19
27
|
|
|
20
28
|
const EXCLUDE_PROPS = ['_id', '_type', 'assetId', 'extension', 'mimeType', 'path', 'url']
|
|
21
|
-
const ACTION_REMOVE = 'remove'
|
|
22
|
-
const ACTION_REWRITE = 'rewrite'
|
|
29
|
+
const ACTION_REMOVE = 'remove' as const
|
|
30
|
+
const ACTION_REWRITE = 'rewrite' as const
|
|
31
|
+
|
|
32
|
+
type AssetAction = typeof ACTION_REMOVE | typeof ACTION_REWRITE
|
|
33
|
+
|
|
34
|
+
interface AssetHandlerOptions {
|
|
35
|
+
client: SanityClientLike
|
|
36
|
+
tmpDir: string
|
|
37
|
+
prefix?: string
|
|
38
|
+
concurrency?: number
|
|
39
|
+
maxRetries?: number
|
|
40
|
+
retryDelayMs?: number
|
|
41
|
+
queue?: PQueue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface AssetRequestOptions {
|
|
45
|
+
url: string
|
|
46
|
+
headers: Record<string, string>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AssetField {
|
|
50
|
+
asset: {
|
|
51
|
+
_ref: string
|
|
52
|
+
}
|
|
53
|
+
[key: string]: unknown
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface RewrittenAssetField {
|
|
57
|
+
_sanityAsset: string
|
|
58
|
+
[key: string]: unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DownloadError extends Error {
|
|
62
|
+
statusCode?: number
|
|
63
|
+
}
|
|
23
64
|
|
|
24
65
|
export class AssetHandler {
|
|
25
|
-
|
|
26
|
-
|
|
66
|
+
client: SanityClientLike
|
|
67
|
+
tmpDir: string
|
|
68
|
+
assetDirsCreated: boolean
|
|
69
|
+
downloading: string[]
|
|
70
|
+
assetsSeen: Map<string, string>
|
|
71
|
+
assetMap: AssetMap
|
|
72
|
+
filesWritten: number
|
|
73
|
+
queueSize: number
|
|
74
|
+
maxRetries: number
|
|
75
|
+
retryDelayMs: number | undefined
|
|
76
|
+
queue: PQueue
|
|
77
|
+
rejectedError: Error | null
|
|
78
|
+
reject: (err: Error) => void
|
|
79
|
+
|
|
80
|
+
constructor(options: AssetHandlerOptions) {
|
|
81
|
+
const concurrency = options.concurrency ?? ASSET_DOWNLOAD_CONCURRENCY
|
|
27
82
|
debug('Using asset download concurrency of %d', concurrency)
|
|
28
83
|
|
|
29
84
|
this.client = options.client
|
|
@@ -35,23 +90,23 @@ export class AssetHandler {
|
|
|
35
90
|
this.assetMap = {}
|
|
36
91
|
this.filesWritten = 0
|
|
37
92
|
this.queueSize = 0
|
|
38
|
-
this.maxRetries = options.maxRetries
|
|
93
|
+
this.maxRetries = options.maxRetries ?? ASSET_DOWNLOAD_MAX_RETRIES
|
|
39
94
|
this.retryDelayMs = options.retryDelayMs
|
|
40
|
-
this.queue = options.queue
|
|
95
|
+
this.queue = options.queue ?? new PQueue({concurrency})
|
|
41
96
|
|
|
42
97
|
this.rejectedError = null
|
|
43
|
-
this.reject = (err) => {
|
|
98
|
+
this.reject = (err: Error): void => {
|
|
44
99
|
this.rejectedError = err
|
|
45
100
|
}
|
|
46
101
|
}
|
|
47
102
|
|
|
48
|
-
clear() {
|
|
103
|
+
clear(): void {
|
|
49
104
|
this.assetsSeen.clear()
|
|
50
105
|
this.queue.clear()
|
|
51
106
|
this.queueSize = 0
|
|
52
107
|
}
|
|
53
108
|
|
|
54
|
-
finish() {
|
|
109
|
+
finish(): Promise<AssetMap> {
|
|
55
110
|
return new Promise((resolve, reject) => {
|
|
56
111
|
if (this.rejectedError) {
|
|
57
112
|
reject(this.rejectedError)
|
|
@@ -59,28 +114,31 @@ export class AssetHandler {
|
|
|
59
114
|
}
|
|
60
115
|
|
|
61
116
|
this.reject = reject
|
|
62
|
-
this.queue.onIdle().then(() => resolve(this.assetMap))
|
|
117
|
+
void this.queue.onIdle().then(() => resolve(this.assetMap))
|
|
63
118
|
})
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
// Called when we want to download all assets to local filesystem and rewrite documents to hold
|
|
67
122
|
// placeholder asset references (_sanityAsset: 'image@file:///local/path')
|
|
68
|
-
rewriteAssets = throughObj(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
rewriteAssets = throughObj(
|
|
124
|
+
(doc: SanityDocument | AssetDocument, _enc: BufferEncoding, callback) => {
|
|
125
|
+
if (['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)) {
|
|
126
|
+
const assetDoc = doc as AssetDocument
|
|
127
|
+
const type = doc._type === 'sanity.imageAsset' ? 'image' : 'file'
|
|
128
|
+
const filePath = `${type}s/${generateFilename(doc._id)}`
|
|
129
|
+
this.assetsSeen.set(doc._id, type)
|
|
130
|
+
this.queueAssetDownload(assetDoc, filePath)
|
|
131
|
+
callback()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
77
134
|
|
|
78
|
-
|
|
79
|
-
|
|
135
|
+
callback(null, this.findAndModify(doc, ACTION_REWRITE))
|
|
136
|
+
},
|
|
137
|
+
)
|
|
80
138
|
|
|
81
139
|
// Called in the case where we don't _want_ assets, so basically just remove all asset documents
|
|
82
140
|
// as well as references to assets (*.asset._ref ^= (image|file)-)
|
|
83
|
-
stripAssets = throughObj(
|
|
141
|
+
stripAssets = throughObj((doc: SanityDocument, _enc: BufferEncoding, callback) => {
|
|
84
142
|
if (['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)) {
|
|
85
143
|
callback()
|
|
86
144
|
return
|
|
@@ -91,7 +149,7 @@ export class AssetHandler {
|
|
|
91
149
|
|
|
92
150
|
// Called when we are using raw export mode along with `assets: false`, where we simply
|
|
93
151
|
// want to skip asset documents but retain asset references (useful for data mangling)
|
|
94
|
-
skipAssets = throughObj((doc,
|
|
152
|
+
skipAssets = throughObj((doc: SanityDocument, _enc: BufferEncoding, callback) => {
|
|
95
153
|
const isAsset = ['sanity.imageAsset', 'sanity.fileAsset'].includes(doc._type)
|
|
96
154
|
if (isAsset) {
|
|
97
155
|
callback()
|
|
@@ -101,9 +159,9 @@ export class AssetHandler {
|
|
|
101
159
|
callback(null, doc)
|
|
102
160
|
})
|
|
103
161
|
|
|
104
|
-
noop = throughObj((doc,
|
|
162
|
+
noop = throughObj((doc: SanityDocument, _enc: BufferEncoding, callback) => callback(null, doc))
|
|
105
163
|
|
|
106
|
-
queueAssetDownload(assetDoc, dstPath) {
|
|
164
|
+
queueAssetDownload(assetDoc: AssetDocument, dstPath: string): void {
|
|
107
165
|
if (!assetDoc.url) {
|
|
108
166
|
debug('Asset document "%s" does not have a URL property, skipping', assetDoc._id)
|
|
109
167
|
return
|
|
@@ -113,20 +171,21 @@ export class AssetHandler {
|
|
|
113
171
|
this.queueSize++
|
|
114
172
|
this.downloading.push(assetDoc.url)
|
|
115
173
|
|
|
116
|
-
const doDownload = async () => {
|
|
117
|
-
let dlError
|
|
174
|
+
const doDownload = async (): Promise<boolean> => {
|
|
175
|
+
let dlError: DownloadError | undefined
|
|
118
176
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
119
177
|
try {
|
|
120
178
|
return await this.downloadAsset(assetDoc, dstPath)
|
|
121
179
|
} catch (err) {
|
|
180
|
+
const downloadError = err as DownloadError
|
|
122
181
|
// Ignore inaccessible assets
|
|
123
|
-
switch (
|
|
182
|
+
switch (downloadError.statusCode) {
|
|
124
183
|
case 401:
|
|
125
184
|
case 403:
|
|
126
185
|
case 404:
|
|
127
186
|
console.warn(
|
|
128
187
|
`⚠ Asset failed with HTTP %d (ignoring): %s`,
|
|
129
|
-
|
|
188
|
+
downloadError.statusCode,
|
|
130
189
|
assetDoc._id,
|
|
131
190
|
)
|
|
132
191
|
return true
|
|
@@ -141,59 +200,62 @@ export class AssetHandler {
|
|
|
141
200
|
err,
|
|
142
201
|
)
|
|
143
202
|
|
|
144
|
-
dlError =
|
|
203
|
+
dlError = downloadError
|
|
145
204
|
|
|
146
|
-
if (
|
|
205
|
+
if (
|
|
206
|
+
downloadError.statusCode &&
|
|
207
|
+
downloadError.statusCode >= 400 &&
|
|
208
|
+
downloadError.statusCode < 500
|
|
209
|
+
) {
|
|
147
210
|
// Don't retry on client errors
|
|
148
211
|
break
|
|
149
212
|
}
|
|
150
213
|
|
|
151
|
-
await delay(this.retryDelayMs
|
|
214
|
+
await delay(this.retryDelayMs ?? DEFAULT_RETRY_DELAY)
|
|
152
215
|
}
|
|
153
216
|
}
|
|
154
|
-
throw dlError
|
|
217
|
+
throw new Error(dlError?.message ?? 'Unknown error downloading asset')
|
|
155
218
|
}
|
|
156
219
|
|
|
157
220
|
this.queue
|
|
158
221
|
.add(() =>
|
|
159
|
-
doDownload().catch((err) => {
|
|
222
|
+
doDownload().catch((err: unknown) => {
|
|
160
223
|
debug('Failed to download the asset, aborting download', err)
|
|
161
224
|
this.queue.clear()
|
|
162
|
-
this.reject(err)
|
|
225
|
+
this.reject(err instanceof Error ? err : new Error(String(err)))
|
|
163
226
|
}),
|
|
164
227
|
)
|
|
165
|
-
.catch((error) => {
|
|
228
|
+
.catch((error: unknown) => {
|
|
166
229
|
debug('Queued task failed', error)
|
|
167
230
|
})
|
|
168
231
|
}
|
|
169
232
|
|
|
170
|
-
maybeCreateAssetDirs() {
|
|
233
|
+
maybeCreateAssetDirs(): void {
|
|
171
234
|
if (this.assetDirsCreated) {
|
|
172
235
|
return
|
|
173
236
|
}
|
|
174
237
|
|
|
175
|
-
/* eslint-disable no-sync */
|
|
176
238
|
mkdirSync(joinPath(this.tmpDir, 'files'), {recursive: true})
|
|
177
239
|
mkdirSync(joinPath(this.tmpDir, 'images'), {recursive: true})
|
|
178
|
-
/* eslint-enable no-sync */
|
|
179
240
|
this.assetDirsCreated = true
|
|
180
241
|
}
|
|
181
242
|
|
|
182
|
-
getAssetRequestOptions(assetDoc) {
|
|
243
|
+
getAssetRequestOptions(assetDoc: AssetDocument): AssetRequestOptions {
|
|
183
244
|
const token = this.client.config().token
|
|
184
|
-
const headers = {'User-Agent': getUserAgent()}
|
|
245
|
+
const headers: Record<string, string> = {'User-Agent': getUserAgent()}
|
|
185
246
|
const isImage = assetDoc._type === 'sanity.imageAsset'
|
|
186
247
|
|
|
187
|
-
const url = URL.parse(assetDoc.url)
|
|
248
|
+
const url = URL.parse(assetDoc.url ?? '')
|
|
188
249
|
// If we can't parse it, return as-is
|
|
189
250
|
if (!url) {
|
|
190
|
-
return {url: assetDoc.url, headers}
|
|
251
|
+
return {url: assetDoc.url ?? '', headers}
|
|
191
252
|
}
|
|
192
253
|
|
|
193
254
|
if (
|
|
194
255
|
isImage &&
|
|
195
256
|
token &&
|
|
196
|
-
(
|
|
257
|
+
(url.hostname === 'cdn.sanity.io' ||
|
|
258
|
+
url.hostname === 'cdn.sanity.work' ||
|
|
197
259
|
// used in tests. use a very specific port to avoid conflicts
|
|
198
260
|
url.host === 'localhost:43216')
|
|
199
261
|
) {
|
|
@@ -204,15 +266,14 @@ export class AssetHandler {
|
|
|
204
266
|
return {url: url.toString(), headers}
|
|
205
267
|
}
|
|
206
268
|
|
|
207
|
-
|
|
208
|
-
async downloadAsset(assetDoc, dstPath) {
|
|
269
|
+
async downloadAsset(assetDoc: AssetDocument, dstPath: string): Promise<boolean> {
|
|
209
270
|
const {url} = assetDoc
|
|
210
271
|
|
|
211
272
|
debug('Downloading asset %s', url)
|
|
212
273
|
|
|
213
274
|
const options = this.getAssetRequestOptions(assetDoc)
|
|
214
275
|
|
|
215
|
-
let stream
|
|
276
|
+
let stream: ResponseStream
|
|
216
277
|
try {
|
|
217
278
|
stream = await requestStream({
|
|
218
279
|
maxRetries: 0, // We handle retries ourselves in queueAssetDownload
|
|
@@ -220,8 +281,7 @@ export class AssetHandler {
|
|
|
220
281
|
})
|
|
221
282
|
} catch (err) {
|
|
222
283
|
const message = 'Failed to create asset stream'
|
|
223
|
-
if (
|
|
224
|
-
// try to re-assign the error message so the stack trace is more visible
|
|
284
|
+
if (err instanceof Error) {
|
|
225
285
|
err.message = `${message}: ${err.message}`
|
|
226
286
|
throw err
|
|
227
287
|
}
|
|
@@ -230,7 +290,7 @@ export class AssetHandler {
|
|
|
230
290
|
}
|
|
231
291
|
|
|
232
292
|
if (stream.statusCode !== 200) {
|
|
233
|
-
let errMsg
|
|
293
|
+
let errMsg: string
|
|
234
294
|
try {
|
|
235
295
|
const err = await tryGetErrorFromStream(stream)
|
|
236
296
|
errMsg = `Referenced asset URL "${url}" returned HTTP ${stream.statusCode}`
|
|
@@ -239,8 +299,7 @@ export class AssetHandler {
|
|
|
239
299
|
}
|
|
240
300
|
} catch (err) {
|
|
241
301
|
const message = 'Failed to parse error response from asset stream'
|
|
242
|
-
if (
|
|
243
|
-
// try to re-assign the error message so the stack trace is more visible
|
|
302
|
+
if (err instanceof Error) {
|
|
244
303
|
err.message = `${message}: ${err.message}`
|
|
245
304
|
throw err
|
|
246
305
|
}
|
|
@@ -248,8 +307,10 @@ export class AssetHandler {
|
|
|
248
307
|
throw new Error(message, {cause: err})
|
|
249
308
|
}
|
|
250
309
|
|
|
251
|
-
const streamError = new Error(errMsg)
|
|
252
|
-
|
|
310
|
+
const streamError: DownloadError = new Error(errMsg)
|
|
311
|
+
if (stream.statusCode !== undefined) {
|
|
312
|
+
streamError.statusCode = stream.statusCode
|
|
313
|
+
}
|
|
253
314
|
throw streamError
|
|
254
315
|
}
|
|
255
316
|
|
|
@@ -268,7 +329,7 @@ export class AssetHandler {
|
|
|
268
329
|
} catch (err) {
|
|
269
330
|
const message = 'Failed to write asset stream to filesystem'
|
|
270
331
|
|
|
271
|
-
if (
|
|
332
|
+
if (err instanceof Error) {
|
|
272
333
|
err.message = `${message}: ${err.message}`
|
|
273
334
|
throw err
|
|
274
335
|
}
|
|
@@ -277,9 +338,9 @@ export class AssetHandler {
|
|
|
277
338
|
}
|
|
278
339
|
|
|
279
340
|
// Verify it against our downloaded stream to make sure we have the same copy
|
|
280
|
-
const contentLength = stream.headers['content-length']
|
|
281
|
-
const remoteSha1 = stream.headers['x-sanity-sha1']
|
|
282
|
-
const remoteMd5 = stream.headers['x-sanity-md5']
|
|
341
|
+
const contentLength = stream.headers?.['content-length']
|
|
342
|
+
const remoteSha1 = stream.headers?.['x-sanity-sha1']
|
|
343
|
+
const remoteMd5 = stream.headers?.['x-sanity-md5']
|
|
283
344
|
const hasHash = Boolean(remoteSha1 || remoteMd5)
|
|
284
345
|
const method = sha1 ? 'sha1' : 'md5'
|
|
285
346
|
|
|
@@ -298,13 +359,13 @@ export class AssetHandler {
|
|
|
298
359
|
: `sha1 should be ${remoteSha1}, got ${sha1}`),
|
|
299
360
|
|
|
300
361
|
contentLength &&
|
|
301
|
-
parseInt(contentLength, 10) !== size &&
|
|
362
|
+
parseInt(String(contentLength), 10) !== size &&
|
|
302
363
|
`Asset should be ${contentLength} bytes, got ${size}`,
|
|
303
364
|
]
|
|
304
365
|
|
|
305
366
|
const detailsString = `Details:\n - ${details.filter(Boolean).join('\n - ')}`
|
|
306
367
|
|
|
307
|
-
await
|
|
368
|
+
await rm(tmpPath, {recursive: true, force: true})
|
|
308
369
|
|
|
309
370
|
throw new Error(`Failed to download asset at ${assetDoc.url}. ${detailsString}`)
|
|
310
371
|
}
|
|
@@ -327,43 +388,44 @@ export class AssetHandler {
|
|
|
327
388
|
return true
|
|
328
389
|
}
|
|
329
390
|
|
|
330
|
-
findAndModify = (item, action) => {
|
|
391
|
+
findAndModify = (item: unknown, action: AssetAction): unknown => {
|
|
331
392
|
if (Array.isArray(item)) {
|
|
332
|
-
const children = item.map((child) => this.findAndModify(child, action))
|
|
333
|
-
return children.filter(
|
|
334
|
-
return child !== null && child !== undefined
|
|
335
|
-
})
|
|
393
|
+
const children = item.map((child: unknown) => this.findAndModify(child, action))
|
|
394
|
+
return children.filter((child): child is NonNullable<typeof child> => child != null)
|
|
336
395
|
}
|
|
337
396
|
|
|
338
397
|
if (!item || typeof item !== 'object') {
|
|
339
398
|
return item
|
|
340
399
|
}
|
|
341
400
|
|
|
342
|
-
const
|
|
401
|
+
const record = item as Record<string, unknown>
|
|
402
|
+
|
|
403
|
+
const isAsset = isAssetField(record)
|
|
343
404
|
if (isAsset && action === ACTION_REMOVE) {
|
|
344
405
|
return undefined
|
|
345
406
|
}
|
|
346
407
|
|
|
347
408
|
if (isAsset && action === ACTION_REWRITE) {
|
|
348
|
-
const {asset, ...other} =
|
|
409
|
+
const {asset, ...other} = record
|
|
349
410
|
const assetId = asset._ref
|
|
350
|
-
const assetType = getAssetType(
|
|
411
|
+
const assetType = getAssetType(record)
|
|
351
412
|
const filePath = `${assetType}s/${generateFilename(assetId)}`
|
|
413
|
+
const modified = this.findAndModify(other, action)
|
|
352
414
|
return {
|
|
353
415
|
_sanityAsset: `${assetType}@file://./${filePath}`,
|
|
354
|
-
...
|
|
355
|
-
}
|
|
416
|
+
...(typeof modified === 'object' && modified !== null ? modified : {}),
|
|
417
|
+
} as RewrittenAssetField
|
|
356
418
|
}
|
|
357
419
|
|
|
358
|
-
const newItem = {}
|
|
359
|
-
const keys = Object.keys(
|
|
360
|
-
for (
|
|
361
|
-
const
|
|
362
|
-
const value = item[key]
|
|
420
|
+
const newItem: Record<string, unknown> = {}
|
|
421
|
+
const keys = Object.keys(record)
|
|
422
|
+
for (const key of keys) {
|
|
423
|
+
const value = record[key]
|
|
363
424
|
|
|
364
425
|
newItem[key] = this.findAndModify(value, action)
|
|
365
426
|
|
|
366
427
|
if (typeof newItem[key] === 'undefined') {
|
|
428
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
367
429
|
delete newItem[key]
|
|
368
430
|
}
|
|
369
431
|
}
|
|
@@ -372,38 +434,51 @@ export class AssetHandler {
|
|
|
372
434
|
}
|
|
373
435
|
}
|
|
374
436
|
|
|
375
|
-
function isAssetField(item) {
|
|
376
|
-
|
|
437
|
+
function isAssetField(item: Record<string, unknown>): item is AssetField {
|
|
438
|
+
const asset = item.asset as {_ref?: unknown} | undefined
|
|
439
|
+
return Boolean(asset?._ref && typeof asset._ref === 'string' && isSanityAsset(asset._ref))
|
|
377
440
|
}
|
|
378
441
|
|
|
379
|
-
function getAssetType(item) {
|
|
380
|
-
|
|
442
|
+
function getAssetType(item: Record<string, unknown>): string | null {
|
|
443
|
+
const asset = item.asset as {_ref?: unknown} | undefined
|
|
444
|
+
if (!asset || typeof asset._ref !== 'string') {
|
|
381
445
|
return null
|
|
382
446
|
}
|
|
383
447
|
|
|
384
|
-
const
|
|
385
|
-
return
|
|
448
|
+
const match = asset._ref.match(/^(image|file)-/)
|
|
449
|
+
return match?.[1] ?? null
|
|
386
450
|
}
|
|
387
451
|
|
|
388
|
-
function isSanityAsset(assetId) {
|
|
452
|
+
function isSanityAsset(assetId: string): boolean {
|
|
389
453
|
return (
|
|
390
454
|
/^image-[a-f0-9]{40}-\d+x\d+-[a-z]+$/.test(assetId) ||
|
|
391
455
|
/^file-[a-f0-9]{40}-[a-z0-9]+$/.test(assetId)
|
|
392
456
|
)
|
|
393
457
|
}
|
|
394
458
|
|
|
395
|
-
function generateFilename(assetId) {
|
|
396
|
-
const
|
|
397
|
-
const
|
|
459
|
+
function generateFilename(assetId: string): string {
|
|
460
|
+
const match = assetId.match(/^(image|file)-(.*?)(-[a-z]+)?$/)
|
|
461
|
+
const asset = match?.[2]
|
|
462
|
+
const ext = match?.[3]
|
|
463
|
+
const extension = (ext ?? 'bin').replace(/^-/, '')
|
|
398
464
|
return asset ? `${asset}.${extension}` : `${assetId}.bin`
|
|
399
465
|
}
|
|
400
466
|
|
|
401
|
-
|
|
467
|
+
interface HashResult {
|
|
468
|
+
size: number
|
|
469
|
+
sha1: string
|
|
470
|
+
md5: string
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function writeHashedStream(
|
|
474
|
+
filePath: string,
|
|
475
|
+
stream: NodeJS.ReadableStream,
|
|
476
|
+
): Promise<HashResult> {
|
|
402
477
|
let size = 0
|
|
403
478
|
const md5 = createHash('md5')
|
|
404
479
|
const sha1 = createHash('sha1')
|
|
405
480
|
|
|
406
|
-
const hasher = through((chunk,
|
|
481
|
+
const hasher = through((chunk, _enc, cb) => {
|
|
407
482
|
size += chunk.length
|
|
408
483
|
md5.update(chunk)
|
|
409
484
|
sha1.update(chunk)
|
|
@@ -418,12 +493,12 @@ async function writeHashedStream(filePath, stream) {
|
|
|
418
493
|
}
|
|
419
494
|
}
|
|
420
495
|
|
|
421
|
-
function tryGetErrorFromStream(stream) {
|
|
496
|
+
function tryGetErrorFromStream(stream: NodeJS.ReadableStream): Promise<string | null> {
|
|
422
497
|
return new Promise((resolve, reject) => {
|
|
423
|
-
const chunks = []
|
|
498
|
+
const chunks: Buffer[] = []
|
|
424
499
|
let receivedData = false
|
|
425
500
|
|
|
426
|
-
stream.on('data', (chunk) => {
|
|
501
|
+
stream.on('data', (chunk: Buffer) => {
|
|
427
502
|
receivedData = true
|
|
428
503
|
chunks.push(chunk)
|
|
429
504
|
})
|
|
@@ -436,8 +511,8 @@ function tryGetErrorFromStream(stream) {
|
|
|
436
511
|
|
|
437
512
|
const body = Buffer.concat(chunks)
|
|
438
513
|
try {
|
|
439
|
-
const parsed = JSON.parse(body.toString('utf8'))
|
|
440
|
-
resolve(parsed.message
|
|
514
|
+
const parsed = JSON.parse(body.toString('utf8')) as {message?: string; error?: string}
|
|
515
|
+
resolve(parsed.message ?? parsed.error ?? null)
|
|
441
516
|
} catch {
|
|
442
517
|
resolve(body.toString('utf8').slice(0, 16000))
|
|
443
518
|
}
|
|
@@ -447,12 +522,12 @@ function tryGetErrorFromStream(stream) {
|
|
|
447
522
|
})
|
|
448
523
|
}
|
|
449
524
|
|
|
450
|
-
function omit(obj, keys) {
|
|
451
|
-
const copy = {}
|
|
452
|
-
|
|
525
|
+
function omit(obj: Record<string, unknown>, keys: string[]): AssetMetadata {
|
|
526
|
+
const copy: AssetMetadata = {}
|
|
527
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
453
528
|
if (!keys.includes(key)) {
|
|
454
529
|
copy[key] = value
|
|
455
530
|
}
|
|
456
|
-
}
|
|
531
|
+
}
|
|
457
532
|
return copy
|
|
458
533
|
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many retries to attempt when retrieving the document stream.
|
|
3
|
+
* User overridable as `options.maxRetries`.
|
|
4
|
+
*
|
|
5
|
+
* Note: Only for initial connection - if download fails while streaming, we cannot easily resume.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const DOCUMENT_STREAM_MAX_RETRIES: number = 5
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How many retries to attempt when downloading an asset.
|
|
12
|
+
* User overridable as `options.maxAssetRetries`.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export const ASSET_DOWNLOAD_MAX_RETRIES: number = 10
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default delay between retries when retrieving assets or document stream.
|
|
19
|
+
* User overridable as `options.retryDelayMs`.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_RETRY_DELAY: number = 1500
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How many concurrent asset downloads to allow.
|
|
26
|
+
* User overridable as `options.assetConcurrency`.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export const ASSET_DOWNLOAD_CONCURRENCY: number = 8
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How frequently we will `debug` log while streaming the documents.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export const DOCUMENT_STREAM_DEBUG_INTERVAL: number = 10000
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How long to wait before timing out the read of a request due to inactivity.
|
|
39
|
+
* User overridable as `options.readTimeout`.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export const REQUEST_READ_TIMEOUT: number = 3 * 60 * 1000 // 3 minutes
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What mode to use when exporting documents.
|
|
46
|
+
* stream: Export all documents in the dataset in one request, this will be consistent but might be slow on large datasets.
|
|
47
|
+
* cursor: Export documents using a cursor, this might lead to inconsistent results if a mutation is performed while exporting.
|
|
48
|
+
*/
|
|
49
|
+
export const MODE_STREAM = 'stream' as const
|
|
50
|
+
export const MODE_CURSOR = 'cursor' as const
|
package/src/debug.ts
ADDED