@sanity/export 3.37.4 → 3.38.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/package.json +9 -4
- package/src/AssetHandler.js +64 -46
- package/src/constants.js +22 -0
- package/src/export.js +136 -118
- package/src/getDocumentsStream.js +4 -4
- package/src/rejectOnApiError.js +7 -1
- package/src/requestStream.js +12 -6
- package/src/util/extractFirstError.js +14 -0
- package/src/validateOptions.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/export",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.0",
|
|
4
4
|
"description": "Export Sanity documents and assets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"lint": "eslint .",
|
|
30
|
-
"test": "jest"
|
|
30
|
+
"test": "jest --verbose"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@sanity/client": "^6.15.20",
|
|
33
34
|
"@sanity/util": "3.37.2",
|
|
34
35
|
"archiver": "^7.0.0",
|
|
35
36
|
"debug": "^4.3.4",
|
|
@@ -38,7 +39,9 @@
|
|
|
38
39
|
"mississippi": "^4.0.0",
|
|
39
40
|
"p-queue": "^2.3.0",
|
|
40
41
|
"rimraf": "^3.0.2",
|
|
41
|
-
"split2": "^4.2.0"
|
|
42
|
+
"split2": "^4.2.0",
|
|
43
|
+
"tar": "^7.0.1",
|
|
44
|
+
"yaml": "^2.4.2"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@jest/globals": "^29.7.0",
|
|
@@ -48,9 +51,11 @@
|
|
|
48
51
|
"eslint-config-sanity": "^7.1.2",
|
|
49
52
|
"eslint-plugin-prettier": "^5.1.3",
|
|
50
53
|
"jest": "^29.7.0",
|
|
54
|
+
"nock": "^13.5.4",
|
|
51
55
|
"prettier": "^3.2.5",
|
|
52
56
|
"prettier-plugin-packagejson": "^2.5.0",
|
|
53
|
-
"string-to-stream": "^1.1.0"
|
|
57
|
+
"string-to-stream": "^1.1.0",
|
|
58
|
+
"tar": "^7.0.1"
|
|
54
59
|
},
|
|
55
60
|
"engines": {
|
|
56
61
|
"node": ">=18"
|
package/src/AssetHandler.js
CHANGED
|
@@ -9,29 +9,11 @@ const pkg = require('../package.json')
|
|
|
9
9
|
const debug = require('./debug')
|
|
10
10
|
const requestStream = require('./requestStream')
|
|
11
11
|
const rimraf = require('./util/rimraf')
|
|
12
|
+
const {ASSET_DOWNLOAD_MAX_RETRIES, ASSET_DOWNLOAD_CONCURRENCY} = require('./constants')
|
|
12
13
|
|
|
13
14
|
const EXCLUDE_PROPS = ['_id', '_type', 'assetId', 'extension', 'mimeType', 'path', 'url']
|
|
14
15
|
const ACTION_REMOVE = 'remove'
|
|
15
16
|
const ACTION_REWRITE = 'rewrite'
|
|
16
|
-
const ASSET_DOWNLOAD_CONCURRENCY = 8
|
|
17
|
-
|
|
18
|
-
const retryHelper = (times, fn, onError) => {
|
|
19
|
-
let attempt = 0
|
|
20
|
-
const caller = (...args) => {
|
|
21
|
-
return fn(...args).catch((err) => {
|
|
22
|
-
if (onError) {
|
|
23
|
-
onError(err, attempt)
|
|
24
|
-
}
|
|
25
|
-
if (attempt < times) {
|
|
26
|
-
attempt++
|
|
27
|
-
return caller(...args)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
throw err
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
return caller
|
|
34
|
-
}
|
|
35
17
|
|
|
36
18
|
class AssetHandler {
|
|
37
19
|
constructor(options) {
|
|
@@ -47,6 +29,7 @@ class AssetHandler {
|
|
|
47
29
|
this.assetMap = {}
|
|
48
30
|
this.filesWritten = 0
|
|
49
31
|
this.queueSize = 0
|
|
32
|
+
this.maxRetries = options.maxRetries || ASSET_DOWNLOAD_MAX_RETRIES
|
|
50
33
|
this.queue = options.queue || new PQueue({concurrency})
|
|
51
34
|
|
|
52
35
|
this.rejectedError = null
|
|
@@ -123,26 +106,42 @@ class AssetHandler {
|
|
|
123
106
|
this.queueSize++
|
|
124
107
|
this.downloading.push(assetDoc.url)
|
|
125
108
|
|
|
126
|
-
const doDownload =
|
|
127
|
-
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
109
|
+
const doDownload = async () => {
|
|
110
|
+
let dlError
|
|
111
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
112
|
+
try {
|
|
113
|
+
return await this.downloadAsset(assetDoc, dstPath)
|
|
114
|
+
} catch (err) {
|
|
115
|
+
debug(
|
|
116
|
+
`Error downloading asset %s (destination: %s), attempt %d`,
|
|
117
|
+
assetDoc._id,
|
|
118
|
+
dstPath,
|
|
119
|
+
attempt,
|
|
120
|
+
err,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
dlError = err
|
|
124
|
+
|
|
125
|
+
if ('statusCode' in err && err.statusCode >= 400 && err.statusCode < 500) {
|
|
126
|
+
// Don't retry on client errors
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw dlError
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.queue
|
|
135
|
+
.add(() =>
|
|
136
|
+
doDownload().catch((err) => {
|
|
137
|
+
debug('Failed to download the asset, aborting download', err)
|
|
138
|
+
this.queue.clear()
|
|
139
|
+
this.reject(err)
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
.catch((error) => {
|
|
143
|
+
debug('Queued task failed', error)
|
|
144
|
+
})
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
maybeCreateAssetDirs() {
|
|
@@ -163,7 +162,13 @@ class AssetHandler {
|
|
|
163
162
|
const isImage = assetDoc._type === 'sanity.imageAsset'
|
|
164
163
|
|
|
165
164
|
const url = parseUrl(assetDoc.url, true)
|
|
166
|
-
if (
|
|
165
|
+
if (
|
|
166
|
+
isImage &&
|
|
167
|
+
token &&
|
|
168
|
+
(['cdn.sanity.io', 'cdn.sanity.work'].includes(url.hostname) ||
|
|
169
|
+
// used in tests. use a very specific port to avoid conflicts
|
|
170
|
+
url.host === 'localhost:43216')
|
|
171
|
+
) {
|
|
167
172
|
headers.Authorization = `Bearer ${token}`
|
|
168
173
|
url.query = {...(url.query || {}), dlRaw: 'true'}
|
|
169
174
|
}
|
|
@@ -191,15 +196,13 @@ class AssetHandler {
|
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
if (stream.statusCode !== 200) {
|
|
194
|
-
|
|
199
|
+
let errMsg
|
|
195
200
|
try {
|
|
196
201
|
const err = await tryGetErrorFromStream(stream)
|
|
197
|
-
|
|
202
|
+
errMsg = `Referenced asset URL "${url}" returned HTTP ${stream.statusCode}`
|
|
198
203
|
if (err) {
|
|
199
|
-
errMsg = `${errMsg}
|
|
204
|
+
errMsg = `${errMsg}: ${err}`
|
|
200
205
|
}
|
|
201
|
-
|
|
202
|
-
throw new Error(errMsg)
|
|
203
206
|
} catch (err) {
|
|
204
207
|
const message = 'Failed to parse error response from asset stream'
|
|
205
208
|
if (typeof err.message === 'string') {
|
|
@@ -210,6 +213,10 @@ class AssetHandler {
|
|
|
210
213
|
|
|
211
214
|
throw new Error(message, {cause: err})
|
|
212
215
|
}
|
|
216
|
+
|
|
217
|
+
const streamError = new Error(errMsg)
|
|
218
|
+
streamError.statusCode = stream.statusCode
|
|
219
|
+
throw streamError
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
this.maybeCreateAssetDirs()
|
|
@@ -385,9 +392,20 @@ function writeHashedStream(filePath, stream) {
|
|
|
385
392
|
|
|
386
393
|
function tryGetErrorFromStream(stream) {
|
|
387
394
|
return new Promise((resolve, reject) => {
|
|
388
|
-
|
|
395
|
+
let receivedData = false
|
|
396
|
+
|
|
397
|
+
miss.pipe(stream, miss.concat(parse), (err) => {
|
|
398
|
+
if (err) {
|
|
399
|
+
reject(err)
|
|
400
|
+
} else if (!receivedData) {
|
|
401
|
+
// Resolve with null if no data was received, to let the caller
|
|
402
|
+
// know we couldn't parse the error.
|
|
403
|
+
resolve(null)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
389
406
|
|
|
390
407
|
function parse(body) {
|
|
408
|
+
receivedData = true
|
|
391
409
|
try {
|
|
392
410
|
const parsed = JSON.parse(body.toString('utf8'))
|
|
393
411
|
resolve(parsed.message || parsed.error || null)
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
exports.DOCUMENT_STREAM_MAX_RETRIES = 5
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How many retries to attempt when downloading an asset.
|
|
12
|
+
* User overridable as `options.maxAssetRetries`.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
exports.ASSET_DOWNLOAD_MAX_RETRIES = 10
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How many concurrent asset downloads to allow.
|
|
19
|
+
* User overridable as `options.assetConcurrency`.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
exports.ASSET_DOWNLOAD_CONCURRENCY = 8
|
package/src/export.js
CHANGED
|
@@ -20,12 +20,16 @@ const validateOptions = require('./validateOptions')
|
|
|
20
20
|
|
|
21
21
|
const noop = () => null
|
|
22
22
|
|
|
23
|
-
function exportDataset(opts) {
|
|
23
|
+
async function exportDataset(opts) {
|
|
24
24
|
const options = validateOptions(opts)
|
|
25
25
|
const onProgress = options.onProgress || noop
|
|
26
26
|
const archive = archiver('tar', {
|
|
27
27
|
gzip: true,
|
|
28
|
-
gzipOptions: {
|
|
28
|
+
gzipOptions: {
|
|
29
|
+
level: options.compress
|
|
30
|
+
? zlib.constants.Z_DEFAULT_COMPRESSION
|
|
31
|
+
: zlib.constants.Z_NO_COMPRESSION,
|
|
32
|
+
},
|
|
29
33
|
})
|
|
30
34
|
|
|
31
35
|
const slugDate = new Date()
|
|
@@ -45,10 +49,11 @@ function exportDataset(opts) {
|
|
|
45
49
|
tmpDir,
|
|
46
50
|
prefix,
|
|
47
51
|
concurrency: options.assetConcurrency,
|
|
52
|
+
maxRetries: options.maxAssetRetries,
|
|
48
53
|
})
|
|
49
54
|
|
|
50
|
-
debug('
|
|
51
|
-
debug('
|
|
55
|
+
debug('Downloading assets (temporarily) to %s', tmpDir)
|
|
56
|
+
debug('Downloading to %s', options.outputPath === '-' ? 'stdout' : options.outputPath)
|
|
52
57
|
|
|
53
58
|
let outputStream
|
|
54
59
|
if (isWritableStream(options.outputPath)) {
|
|
@@ -63,144 +68,157 @@ function exportDataset(opts) {
|
|
|
63
68
|
assetStreamHandler = options.assets ? assetHandler.rewriteAssets : assetHandler.stripAssets
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
reject(archiveErr)
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
debug('Archive finished!')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
debug('Getting dataset export stream')
|
|
80
|
-
onProgress({step: 'Exporting documents...'})
|
|
81
|
-
|
|
82
|
-
let documentCount = 0
|
|
83
|
-
let lastReported = Date.now()
|
|
84
|
-
const reportDocumentCount = (chunk, enc, cb) => {
|
|
85
|
-
++documentCount
|
|
71
|
+
let resolve
|
|
72
|
+
let reject
|
|
73
|
+
const result = new Promise((res, rej) => {
|
|
74
|
+
resolve = res
|
|
75
|
+
reject = rej
|
|
76
|
+
})
|
|
86
77
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
78
|
+
miss.finished(archive, async (archiveErr) => {
|
|
79
|
+
if (archiveErr) {
|
|
80
|
+
debug('Archiving errored: %s', archiveErr.stack)
|
|
81
|
+
await cleanup()
|
|
82
|
+
reject(archiveErr)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
95
85
|
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
debug('Archive finished')
|
|
87
|
+
})
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
debug('Getting dataset export stream')
|
|
90
|
+
onProgress({step: 'Exporting documents...'})
|
|
101
91
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const jsonStream = miss.pipeline(
|
|
107
|
-
inputStream,
|
|
108
|
-
logFirstChunk(),
|
|
109
|
-
split(tryParseJson),
|
|
110
|
-
rejectOnApiError(),
|
|
111
|
-
filterSystemDocuments(),
|
|
112
|
-
assetStreamHandler,
|
|
113
|
-
filterDocumentTypes(options.types),
|
|
114
|
-
options.drafts ? miss.through.obj() : filterDrafts(),
|
|
115
|
-
stringifyStream(),
|
|
116
|
-
miss.through(reportDocumentCount),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
miss.finished(jsonStream, async (err) => {
|
|
120
|
-
if (err) {
|
|
121
|
-
return
|
|
122
|
-
}
|
|
92
|
+
let documentCount = 0
|
|
93
|
+
let lastReported = Date.now()
|
|
94
|
+
const reportDocumentCount = (chunk, enc, cb) => {
|
|
95
|
+
++documentCount
|
|
123
96
|
|
|
97
|
+
const now = Date.now()
|
|
98
|
+
if (now - lastReported > 50) {
|
|
124
99
|
onProgress({
|
|
125
100
|
step: 'Exporting documents...',
|
|
126
101
|
current: documentCount,
|
|
127
|
-
total:
|
|
102
|
+
total: '?',
|
|
128
103
|
update: true,
|
|
129
104
|
})
|
|
130
105
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
106
|
+
lastReported = now
|
|
107
|
+
}
|
|
134
108
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const completed =
|
|
138
|
-
assetHandler.queueSize - assetHandler.queue.size - assetHandler.queue.pending
|
|
139
|
-
|
|
140
|
-
if (prevCompleted === completed) {
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
prevCompleted = completed
|
|
145
|
-
onProgress({
|
|
146
|
-
step: 'Downloading assets...',
|
|
147
|
-
current: completed,
|
|
148
|
-
total: assetHandler.queueSize,
|
|
149
|
-
update: true,
|
|
150
|
-
})
|
|
151
|
-
}, 500)
|
|
152
|
-
|
|
153
|
-
debug('Waiting for asset handler to complete downloads')
|
|
154
|
-
try {
|
|
155
|
-
const assetMap = await assetHandler.finish()
|
|
156
|
-
|
|
157
|
-
// Make sure we mark the progress as done (eg 100/100 instead of 99/100)
|
|
158
|
-
onProgress({
|
|
159
|
-
step: 'Downloading assets...',
|
|
160
|
-
current: assetHandler.queueSize,
|
|
161
|
-
total: assetHandler.queueSize,
|
|
162
|
-
update: true,
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
archive.append(JSON.stringify(assetMap), {name: 'assets.json', prefix})
|
|
166
|
-
clearInterval(progressInterval)
|
|
167
|
-
} catch (assetErr) {
|
|
168
|
-
clearInterval(progressInterval)
|
|
169
|
-
await cleanup()
|
|
170
|
-
reject(assetErr)
|
|
171
|
-
return
|
|
172
|
-
}
|
|
109
|
+
cb(null, chunk)
|
|
110
|
+
}
|
|
173
111
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
112
|
+
const inputStream = await getDocumentsStream(options)
|
|
113
|
+
debug('Got HTTP %d', inputStream.statusCode)
|
|
114
|
+
debug('Response headers: %o', inputStream.headers)
|
|
115
|
+
|
|
116
|
+
const jsonStream = miss.pipeline(
|
|
117
|
+
inputStream,
|
|
118
|
+
logFirstChunk(),
|
|
119
|
+
split(tryParseJson),
|
|
120
|
+
rejectOnApiError(),
|
|
121
|
+
filterSystemDocuments(),
|
|
122
|
+
assetStreamHandler,
|
|
123
|
+
filterDocumentTypes(options.types),
|
|
124
|
+
options.drafts ? miss.through.obj() : filterDrafts(),
|
|
125
|
+
stringifyStream(),
|
|
126
|
+
miss.through(reportDocumentCount),
|
|
127
|
+
)
|
|
177
128
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
129
|
+
miss.finished(jsonStream, async (err) => {
|
|
130
|
+
if (err) {
|
|
131
|
+
debug('Export stream error: ', err)
|
|
132
|
+
reject(err)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
182
135
|
|
|
183
|
-
|
|
184
|
-
|
|
136
|
+
debug('Export stream completed')
|
|
137
|
+
onProgress({
|
|
138
|
+
step: 'Exporting documents...',
|
|
139
|
+
current: documentCount,
|
|
140
|
+
total: documentCount,
|
|
141
|
+
update: true,
|
|
185
142
|
})
|
|
186
143
|
|
|
187
|
-
|
|
188
|
-
|
|
144
|
+
if (!options.raw && options.assets) {
|
|
145
|
+
onProgress({step: 'Downloading assets...'})
|
|
146
|
+
}
|
|
189
147
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
148
|
+
let prevCompleted = 0
|
|
149
|
+
const progressInterval = setInterval(() => {
|
|
150
|
+
const completed =
|
|
151
|
+
assetHandler.queueSize - assetHandler.queue.size - assetHandler.queue.pending
|
|
193
152
|
|
|
194
|
-
if (
|
|
195
|
-
resolve()
|
|
153
|
+
if (prevCompleted === completed) {
|
|
196
154
|
return
|
|
197
155
|
}
|
|
198
156
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
157
|
+
prevCompleted = completed
|
|
158
|
+
onProgress({
|
|
159
|
+
step: 'Downloading assets...',
|
|
160
|
+
current: completed,
|
|
161
|
+
total: assetHandler.queueSize,
|
|
162
|
+
update: true,
|
|
163
|
+
})
|
|
164
|
+
}, 500)
|
|
165
|
+
|
|
166
|
+
debug('Waiting for asset handler to complete downloads')
|
|
167
|
+
try {
|
|
168
|
+
const assetMap = await assetHandler.finish()
|
|
169
|
+
|
|
170
|
+
// Make sure we mark the progress as done (eg 100/100 instead of 99/100)
|
|
171
|
+
onProgress({
|
|
172
|
+
step: 'Downloading assets...',
|
|
173
|
+
current: assetHandler.queueSize,
|
|
174
|
+
total: assetHandler.queueSize,
|
|
175
|
+
update: true,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
archive.append(JSON.stringify(assetMap), {name: 'assets.json', prefix})
|
|
179
|
+
clearInterval(progressInterval)
|
|
180
|
+
} catch (assetErr) {
|
|
181
|
+
clearInterval(progressInterval)
|
|
182
|
+
await cleanup()
|
|
183
|
+
reject(assetErr)
|
|
184
|
+
return
|
|
202
185
|
}
|
|
186
|
+
|
|
187
|
+
// Add all downloaded assets to archive
|
|
188
|
+
archive.directory(path.join(tmpDir, 'files'), `${prefix}/files`, {store: true})
|
|
189
|
+
archive.directory(path.join(tmpDir, 'images'), `${prefix}/images`, {store: true})
|
|
190
|
+
|
|
191
|
+
debug('Finalizing archive, flushing streams')
|
|
192
|
+
onProgress({step: 'Adding assets to archive...'})
|
|
193
|
+
await archive.finalize()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
archive.on('warning', (err) => {
|
|
197
|
+
debug('Archive warning: %s', err.message)
|
|
203
198
|
})
|
|
199
|
+
|
|
200
|
+
archive.append(jsonStream, {name: 'data.ndjson', prefix})
|
|
201
|
+
miss.pipe(archive, outputStream, onComplete)
|
|
202
|
+
|
|
203
|
+
async function onComplete(err) {
|
|
204
|
+
onProgress({step: 'Clearing temporary files...'})
|
|
205
|
+
await cleanup()
|
|
206
|
+
|
|
207
|
+
if (!err) {
|
|
208
|
+
resolve({
|
|
209
|
+
outputPath: options.outputPath,
|
|
210
|
+
documentCount,
|
|
211
|
+
assetCount: assetHandler.filesWritten,
|
|
212
|
+
})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
debug('Error during streaming: %s', err.stack)
|
|
217
|
+
assetHandler.clear()
|
|
218
|
+
reject(err)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
function isWritableStream(val) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
const pkg = require('../package.json')
|
|
2
2
|
const requestStream = require('./requestStream')
|
|
3
3
|
|
|
4
|
-
module.exports = (
|
|
4
|
+
module.exports = (options) => {
|
|
5
5
|
// Sanity client doesn't handle streams natively since we want to support node/browser
|
|
6
6
|
// with same API. We're just using it here to get hold of URLs and tokens.
|
|
7
|
-
const url = client.getUrl(`/data/export/${dataset}`)
|
|
8
|
-
const token = client.config().token
|
|
7
|
+
const url = options.client.getUrl(`/data/export/${options.dataset}`)
|
|
8
|
+
const token = options.client.config().token
|
|
9
9
|
const headers = {
|
|
10
10
|
'User-Agent': `${pkg.name}@${pkg.version}`,
|
|
11
11
|
...(token ? {Authorization: `Bearer ${token}`} : {}),
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
return requestStream({url, headers})
|
|
14
|
+
return requestStream({url, headers, maxRetries: options.maxRetries})
|
|
15
15
|
}
|
package/src/rejectOnApiError.js
CHANGED
|
@@ -3,7 +3,13 @@ const miss = require('mississippi')
|
|
|
3
3
|
module.exports = () =>
|
|
4
4
|
miss.through.obj((doc, enc, callback) => {
|
|
5
5
|
if (doc.error && doc.statusCode) {
|
|
6
|
-
callback(
|
|
6
|
+
callback(
|
|
7
|
+
new Error(
|
|
8
|
+
['Export', `HTTP ${doc.statusCode}`, doc.error, doc.message]
|
|
9
|
+
.filter((part) => typeof part === 'string')
|
|
10
|
+
.join(': '),
|
|
11
|
+
),
|
|
12
|
+
)
|
|
7
13
|
return
|
|
8
14
|
}
|
|
9
15
|
|
package/src/requestStream.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
const {getIt} = require('get-it')
|
|
2
2
|
const {keepAlive, promise} = require('get-it/middleware')
|
|
3
3
|
const debug = require('./debug')
|
|
4
|
+
const {extractFirstError} = require('./util/extractFirstError')
|
|
5
|
+
const {DOCUMENT_STREAM_MAX_RETRIES} = require('./constants')
|
|
4
6
|
|
|
5
7
|
const request = getIt([keepAlive(), promise({onlyBody: true})])
|
|
6
8
|
const socketsWithTimeout = new WeakSet()
|
|
7
9
|
|
|
8
10
|
const CONNECTION_TIMEOUT = 15 * 1000 // 15 seconds
|
|
9
11
|
const READ_TIMEOUT = 3 * 60 * 1000 // 3 minutes
|
|
10
|
-
const
|
|
12
|
+
const RETRY_DELAY_MS = 1500 // 1.5 seconds
|
|
11
13
|
|
|
12
14
|
function delay(ms) {
|
|
13
15
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -15,8 +17,11 @@ function delay(ms) {
|
|
|
15
17
|
|
|
16
18
|
/* eslint-disable no-await-in-loop, max-depth */
|
|
17
19
|
module.exports = async (options) => {
|
|
20
|
+
const maxRetries =
|
|
21
|
+
typeof options.maxRetries === 'number' ? options.maxRetries : DOCUMENT_STREAM_MAX_RETRIES
|
|
22
|
+
|
|
18
23
|
let error
|
|
19
|
-
for (let i = 0; i <
|
|
24
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
20
25
|
try {
|
|
21
26
|
const response = await request({
|
|
22
27
|
...options,
|
|
@@ -33,23 +38,24 @@ module.exports = async (options) => {
|
|
|
33
38
|
socketsWithTimeout.add(response.connection)
|
|
34
39
|
response.connection.setTimeout(READ_TIMEOUT, () => {
|
|
35
40
|
response.destroy(
|
|
36
|
-
new Error(`Read timeout: No data received on socket for ${READ_TIMEOUT} ms`),
|
|
41
|
+
new Error(`Export: Read timeout: No data received on socket for ${READ_TIMEOUT} ms`),
|
|
37
42
|
)
|
|
38
43
|
})
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
return response
|
|
42
47
|
} catch (err) {
|
|
43
|
-
error = err
|
|
48
|
+
error = extractFirstError(err)
|
|
44
49
|
|
|
45
50
|
if (err.response && err.response.statusCode && err.response.statusCode < 500) {
|
|
46
51
|
break
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
debug('Error, retrying after
|
|
50
|
-
await delay(
|
|
54
|
+
debug('Error, retrying after %d ms: %s', RETRY_DELAY_MS, error.message)
|
|
55
|
+
await delay(RETRY_DELAY_MS)
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
error.message = `Export: Failed to fetch ${options.url}: ${error.message}`
|
|
54
60
|
throw error
|
|
55
61
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
exports.extractFirstError = function extractFirstError(err) {
|
|
2
|
+
if (
|
|
3
|
+
// eslint-disable-next-line no-undef
|
|
4
|
+
((typeof AggregateError !== 'undefined' && err instanceof AggregateError) ||
|
|
5
|
+
('name' in err && err.name === 'AggregateError')) &&
|
|
6
|
+
Array.isArray(err.errors) &&
|
|
7
|
+
err.errors.length > 0 &&
|
|
8
|
+
'message' in err.errors[0]
|
|
9
|
+
) {
|
|
10
|
+
return err.errors[0]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return err
|
|
14
|
+
}
|
package/src/validateOptions.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
const defaults = require('lodash/defaults')
|
|
2
|
+
const {DOCUMENT_STREAM_MAX_RETRIES, ASSET_DOWNLOAD_MAX_RETRIES} = require('./constants')
|
|
2
3
|
|
|
3
4
|
const clientMethods = ['getUrl', 'config']
|
|
4
5
|
const booleanFlags = ['assets', 'raw', 'compress', 'drafts']
|
|
6
|
+
const numberFlags = ['maxAssetRetries', 'maxRetries', 'assetConcurrency']
|
|
5
7
|
const exportDefaults = {
|
|
6
8
|
compress: true,
|
|
7
9
|
drafts: true,
|
|
8
10
|
assets: true,
|
|
9
11
|
raw: false,
|
|
12
|
+
maxRetries: DOCUMENT_STREAM_MAX_RETRIES,
|
|
13
|
+
maxAssetRetries: ASSET_DOWNLOAD_MAX_RETRIES,
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
function validateOptions(opts) {
|
|
@@ -42,6 +46,12 @@ function validateOptions(opts) {
|
|
|
42
46
|
}
|
|
43
47
|
})
|
|
44
48
|
|
|
49
|
+
numberFlags.forEach((flag) => {
|
|
50
|
+
if (typeof options[flag] !== 'undefined' && typeof options[flag] !== 'number') {
|
|
51
|
+
throw new Error(`Flag ${flag} must be a number if specified`)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
45
55
|
if (!options.outputPath) {
|
|
46
56
|
throw new Error('outputPath must be specified (- for stdout)')
|
|
47
57
|
}
|