bajo-extra 2.6.0 → 2.7.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/docs/BajoExtra.html +1 -1
- package/docs/data/search.json +1 -1
- package/docs/global.html +1 -1
- package/docs/index.js.html +422 -125
- package/docs/scripts/core.js +477 -476
- package/docs/scripts/resize.js +36 -36
- package/docs/scripts/search.js +105 -105
- package/docs/scripts/third-party/fuse.js +1 -1
- package/docs/scripts/third-party/hljs-line-num-original.js +285 -282
- package/docs/scripts/third-party/hljs-line-num.js +1 -1
- package/docs/scripts/third-party/hljs-original.js +1202 -1195
- package/docs/scripts/third-party/hljs.js +1 -1
- package/docs/scripts/third-party/popper.js +1 -1
- package/docs/scripts/third-party/tippy.js +1 -1
- package/docs/scripts/third-party/tocbot.js +509 -508
- package/index.js +351 -120
- package/package.json +4 -2
- package/test/bajo-extra.test.js +184 -0
- package/wiki/CHANGES.md +5 -0
package/docs/index.js.html
CHANGED
|
@@ -5,138 +5,122 @@ import { createGzip, createGunzip } from 'zlib'
|
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import { Readable } from 'stream'
|
|
7
7
|
import numbro from 'numbro'
|
|
8
|
-
|
|
9
|
-
async function fetching ({ url, opts, bulk, spin }) {
|
|
10
|
-
const { setImmediate, print } = this.app.bajo
|
|
11
|
-
const { isEmpty, isFunction, has } = this.app.lib._
|
|
12
|
-
const { validationErrorMessage } = this.app.bajoDb
|
|
13
|
-
const resp = await this.fetchUrl(url, opts ?? {})
|
|
14
|
-
if (isEmpty(resp)) {
|
|
15
|
-
spin.fatal('noServerResponse')
|
|
16
|
-
return -1
|
|
17
|
-
}
|
|
18
|
-
if (bulk.abort) {
|
|
19
|
-
const aborted = await bulk.abort.call(this, resp)
|
|
20
|
-
if (aborted) {
|
|
21
|
-
spin.fatal(aborted)
|
|
22
|
-
return -1
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
let count = 0
|
|
26
|
-
const stat = { created: 0, updated: 0, skipped: 0, error: 0 }
|
|
27
|
-
bulk.dataKey = bulk.dataKey ?? 'data'
|
|
28
|
-
if (bulk.printCount === true) bulk.printCount = 100
|
|
29
|
-
const data = isFunction(bulk.dataKey) ? await bulk.dataKey.call(this, resp) : resp[bulk.dataKey]
|
|
30
|
-
if (data.length === 0) {
|
|
31
|
-
print.warn('noRecordToProcess')
|
|
32
|
-
return 0
|
|
33
|
-
}
|
|
34
|
-
spin.setText('gotRecordsProcessing%d', data.length)
|
|
35
|
-
for (let r of data) {
|
|
36
|
-
await setImmediate()
|
|
37
|
-
if (bulk.converter) r = await bulk.converter.call(this, r, bulk)
|
|
38
|
-
if (isEmpty(r)) {
|
|
39
|
-
stat.skipped++
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
const result = await bulk.handler.call(this, r, bulk)
|
|
44
|
-
if (result && has(stat, result)) stat[result]++
|
|
45
|
-
if (bulk.printCount && bulk.printCount < count && (count % bulk.printCount === 0)) print.succeed('[%s] Processed %d/%d', spin.getElapsed(), count, data.length)
|
|
46
|
-
else if (!spin.opts.isLog) spin.setText('rec%d%d', count, data.length)
|
|
47
|
-
count++
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.log(err)
|
|
50
|
-
spin.setText(validationErrorMessage(err) + ', continue')
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
print.succeed('recProcessed%s%d%d', spin.getElapsed(), count, data.length)
|
|
54
|
-
if (!bulk.noStat) print.succeed('createdUpdatedSkipped%s%d%d%d', spin.getElapsed(), stat.created, stat.updated, stat.skipped)
|
|
55
|
-
return data.length
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function handler (rec, bulk) {
|
|
59
|
-
const { isFunction, set } = this.app.lib._
|
|
60
|
-
const { recordCreate, recordFind, recordUpdate } = this.app.bajoDb
|
|
61
|
-
const save = bulk.save ?? {}
|
|
62
|
-
const current = save.current ?? {}
|
|
63
|
-
let existing
|
|
64
|
-
let record
|
|
65
|
-
let method
|
|
66
|
-
save.checkUnique = save.checkUnique ?? 'id'
|
|
67
|
-
if (['unique', 'upsert'].includes(save.mode)) {
|
|
68
|
-
const query = isFunction(save.checkUnique) ? await save.checkUnique.call(this, rec, save) : set({}, save.checkUnique, rec[save.checkUnique])
|
|
69
|
-
const resp = await recordFind(save.coll, { query, limit: 1 }, { noCache: true })
|
|
70
|
-
if (resp.length > 0) existing = resp[0]
|
|
71
|
-
}
|
|
72
|
-
if (existing) {
|
|
73
|
-
if (save.mode === 'upsert') {
|
|
74
|
-
const body = save.updateConverter ? await save.updateConverter.call(this, rec, save) : rec
|
|
75
|
-
try {
|
|
76
|
-
record = await recordUpdate(save.coll, existing.id, body)
|
|
77
|
-
method = 'updated'
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.error(err)
|
|
80
|
-
method = 'error'
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
method = 'skipped'
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
try {
|
|
87
|
-
record = await recordCreate(save.coll, rec)
|
|
88
|
-
method = 'created'
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error(err)
|
|
91
|
-
method = 'error'
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (record && current.coll && current.query) {
|
|
95
|
-
const query = await current.query.call(this, { body: rec, record, opts: save })
|
|
96
|
-
const recs = await recordFind(current.coll, { query }, { noCache: true })
|
|
97
|
-
const rc = current.converter ? await current.converter.call(this, { body: rec, record, opts: save }) : rec
|
|
98
|
-
if (rc) {
|
|
99
|
-
if (recs.length > 0) {
|
|
100
|
-
const id = recs[0].id
|
|
101
|
-
await recordUpdate(current.coll, id, rc)
|
|
102
|
-
} else {
|
|
103
|
-
await recordCreate(current.coll, rc)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return method
|
|
108
|
-
}
|
|
8
|
+
import sharp from 'sharp'
|
|
109
9
|
|
|
110
10
|
/**
|
|
111
|
-
* Plugin factory
|
|
11
|
+
* Plugin factory.
|
|
12
|
+
*
|
|
13
|
+
* **Never** call this function directly!!! It's only-meant to be called by the {@link https://ardhi.github.io/bajo|Bajo framework} during plugin initialization.
|
|
112
14
|
*
|
|
113
15
|
* @param {string} pkgName - NPM package name
|
|
114
|
-
* @returns {class}
|
|
16
|
+
* @returns {class} BajoExtra
|
|
115
17
|
*/
|
|
116
18
|
async function factory (pkgName) {
|
|
117
19
|
const me = this
|
|
118
20
|
|
|
119
21
|
/**
|
|
120
|
-
* BajoExtra class
|
|
22
|
+
* BajoExtra class definition.
|
|
121
23
|
*
|
|
122
24
|
* @class
|
|
123
25
|
*/
|
|
124
|
-
class BajoExtra extends this.app.
|
|
125
|
-
|
|
126
|
-
|
|
26
|
+
class BajoExtra extends this.app.baseClass.Base {
|
|
27
|
+
/**
|
|
28
|
+
* Constructor
|
|
29
|
+
*/
|
|
127
30
|
constructor () {
|
|
128
31
|
super(pkgName, me.app)
|
|
32
|
+
/**
|
|
33
|
+
* @property {object} config - Configuration object
|
|
34
|
+
* @property {string} config.secret - Secret key for encryption/decryption
|
|
35
|
+
* @property {object} config.fetch - Fetch configuration
|
|
36
|
+
* @property {object} config.thumbnail - Thumbnail configuration
|
|
37
|
+
*/
|
|
129
38
|
this.config = {
|
|
130
|
-
secret: 'hxKY8Eh63Op9js6ovU25qmq2DmCE9dIB',
|
|
39
|
+
secret: 'hxKY8Eh63Op9js6ovU25qmq2DmCE9dIB', // random hard coded, should be overridden by user config
|
|
131
40
|
fetch: {
|
|
132
41
|
agent: {
|
|
133
42
|
autoSelectFamilyAttemptTimeout: 1000,
|
|
134
43
|
autoSelectFamily: true
|
|
135
44
|
}
|
|
45
|
+
},
|
|
46
|
+
thumbnail: {
|
|
47
|
+
inputFormats: ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.avif', '.tiff', '.svg'],
|
|
48
|
+
outputFormats: ['.png']
|
|
136
49
|
}
|
|
137
50
|
}
|
|
138
51
|
}
|
|
139
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Private method to fetch data from a URL and save it, with optional bulk processing.
|
|
55
|
+
*
|
|
56
|
+
* @private
|
|
57
|
+
* @async
|
|
58
|
+
* @method
|
|
59
|
+
* @param {object} options - Options object
|
|
60
|
+
* @param {string} options.url - URL to fetch data from
|
|
61
|
+
* @param {object} options.bulk - Bulk processing options
|
|
62
|
+
* @param {object} options.opts - Fetch options
|
|
63
|
+
* @param {object} options.spin - Spinner object
|
|
64
|
+
* @returns {Promise<number>} - Number of records processed
|
|
65
|
+
*/
|
|
66
|
+
_fetching = async (options = {}) => {
|
|
67
|
+
const { url, bulk, opts, spin } = options
|
|
68
|
+
const { setImmediate, print } = this.app.bajo
|
|
69
|
+
const { isEmpty, isFunction, has } = this.app.lib._
|
|
70
|
+
const { validationErrorMessage } = this.app.bajoDb
|
|
71
|
+
const resp = await this.fetchUrl(url, opts ?? {})
|
|
72
|
+
if (isEmpty(resp)) {
|
|
73
|
+
spin.fatal('noServerResponse')
|
|
74
|
+
return -1
|
|
75
|
+
}
|
|
76
|
+
if (bulk.abort) {
|
|
77
|
+
const aborted = await bulk.abort.call(this, resp)
|
|
78
|
+
if (aborted) {
|
|
79
|
+
spin.fatal(aborted)
|
|
80
|
+
return -1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let count = 0
|
|
84
|
+
const stat = { created: 0, updated: 0, skipped: 0, error: 0 }
|
|
85
|
+
bulk.dataKey = bulk.dataKey ?? 'data'
|
|
86
|
+
if (bulk.printCount === true) bulk.printCount = 100
|
|
87
|
+
const data = isFunction(bulk.dataKey) ? await bulk.dataKey.call(this, resp) : resp[bulk.dataKey]
|
|
88
|
+
if (data.length === 0) {
|
|
89
|
+
print.warn('noRecordToProcess')
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
92
|
+
spin.setText('gotRecordsProcessing%d', data.length)
|
|
93
|
+
for (let r of data) {
|
|
94
|
+
await setImmediate()
|
|
95
|
+
if (bulk.converter) r = await bulk.converter.call(this, r, bulk)
|
|
96
|
+
if (isEmpty(r)) {
|
|
97
|
+
stat.skipped++
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const result = await bulk.handler.call(this, r, bulk)
|
|
102
|
+
if (result && has(stat, result)) stat[result]++
|
|
103
|
+
if (bulk.printCount && bulk.printCount < count && (count % bulk.printCount === 0)) print.succeed('[%s] Processed %d/%d', spin.getElapsed(), count, data.length)
|
|
104
|
+
else if (!spin.opts.isLog) spin.setText('rec%d%d', count, data.length)
|
|
105
|
+
count++
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.log(err)
|
|
108
|
+
spin.setText(validationErrorMessage(err) + ', continue')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
print.succeed('recProcessed%s%d%d', spin.getElapsed(), count, data.length)
|
|
112
|
+
if (!bulk.noStat) print.succeed('createdUpdatedSkipped%s%d%d%d', spin.getElapsed(), stat.created, stat.updated, stat.skipped)
|
|
113
|
+
return data.length
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format a value as bytes.
|
|
118
|
+
*
|
|
119
|
+
* @method
|
|
120
|
+
* @param {number} value - The value to format.
|
|
121
|
+
* @param {object} opts - Formatting options.
|
|
122
|
+
* @returns {string} - Formatted byte string.
|
|
123
|
+
*/
|
|
140
124
|
formatByte = (value, opts = {}) => {
|
|
141
125
|
opts.output = 'byte'
|
|
142
126
|
opts.base = 'binary'
|
|
@@ -145,18 +129,42 @@ async function factory (pkgName) {
|
|
|
145
129
|
return numbro(value).format(opts)
|
|
146
130
|
}
|
|
147
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Format a value as a floating-point number.
|
|
134
|
+
*
|
|
135
|
+
* @method
|
|
136
|
+
* @param {number} value - The value to format.
|
|
137
|
+
* @param {object} opts - Formatting options.
|
|
138
|
+
* @returns {string} - Formatted floating-point string.
|
|
139
|
+
*/
|
|
148
140
|
formatFloat = (value, opts = {}) => {
|
|
149
141
|
opts.mantissa = opts.mantissa ?? opts.scale ?? 2
|
|
150
142
|
opts.thousandSeparated = opts.thousandSeparated ?? true
|
|
151
143
|
return numbro(value).format(opts)
|
|
152
144
|
}
|
|
153
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Format a value as an integer.
|
|
148
|
+
*
|
|
149
|
+
* @method
|
|
150
|
+
* @param {number} value - The value to format.
|
|
151
|
+
* @param {object} opts - Formatting options.
|
|
152
|
+
* @returns {string} - Formatted integer string.
|
|
153
|
+
*/
|
|
154
154
|
formatInteger = (value, opts = {}) => {
|
|
155
155
|
opts.mantissa = 0
|
|
156
156
|
opts.thousandSeparated = opts.thousandSeparated ?? true
|
|
157
157
|
return numbro(value).format(opts)
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Format a value as a percentage.
|
|
162
|
+
*
|
|
163
|
+
* @method
|
|
164
|
+
* @param {number} value - The value to format.
|
|
165
|
+
* @param {object} opts - Formatting options.
|
|
166
|
+
* @returns {string} - Formatted percentage string.
|
|
167
|
+
*/
|
|
160
168
|
formatPercentage = (value, opts = {}) => {
|
|
161
169
|
opts.output = 'percent'
|
|
162
170
|
opts.mantissa = opts.mantissa ?? opts.scale ?? 2
|
|
@@ -164,7 +172,15 @@ async function factory (pkgName) {
|
|
|
164
172
|
return numbro(value).format(opts)
|
|
165
173
|
}
|
|
166
174
|
|
|
167
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Count the number of lines in a file.
|
|
177
|
+
*
|
|
178
|
+
* @async
|
|
179
|
+
* @method
|
|
180
|
+
* @param {string} file - The path to the file.
|
|
181
|
+
* @returns {Promise<number>} - The number of lines in the file.
|
|
182
|
+
* @see {@link https://stackoverflow.com/a/41439945|StackOverflow}
|
|
183
|
+
*/
|
|
168
184
|
countFileLines = async (file) => {
|
|
169
185
|
const { fs } = this.app.lib
|
|
170
186
|
return new Promise((resolve, reject) => {
|
|
@@ -185,15 +201,26 @@ async function factory (pkgName) {
|
|
|
185
201
|
})
|
|
186
202
|
}
|
|
187
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Download a file from a URL.
|
|
206
|
+
*
|
|
207
|
+
* @async
|
|
208
|
+
* @method
|
|
209
|
+
* @param {string} url - The URL to download the file from.
|
|
210
|
+
* @param {object} opts - Fetch options.
|
|
211
|
+
* @param {object} extra - Extra options for downloading.
|
|
212
|
+
* @returns {Promise<string>} - The path to the downloaded file.
|
|
213
|
+
*/
|
|
188
214
|
download = async (url, opts = {}, extra = {}) => {
|
|
189
|
-
const {
|
|
215
|
+
const { importPkg } = this.app.bajo
|
|
216
|
+
const { generateId } = this.app.lib.aneka
|
|
190
217
|
const { fetch } = await importPkg('bajoExtra:undici')
|
|
191
218
|
const { fs } = this.app.lib
|
|
192
219
|
const { isFunction, merge } = this.app.lib._
|
|
193
220
|
if (typeof opts === 'string') extra = { dir: opts }
|
|
194
221
|
const increment = await importPkg('bajo:add-filename-increment')
|
|
195
222
|
if (!extra.dir) {
|
|
196
|
-
extra.dir = `${getPluginDataDir('bajoExtra')}/download`
|
|
223
|
+
extra.dir = `${this.app.getPluginDataDir('bajoExtra')}/download`
|
|
197
224
|
fs.ensureDirSync(extra.dir)
|
|
198
225
|
}
|
|
199
226
|
if (!fs.existsSync(extra.dir)) throw this.error('dlDirNotExists%s', extra.dir)
|
|
@@ -231,14 +258,86 @@ async function factory (pkgName) {
|
|
|
231
258
|
})
|
|
232
259
|
}
|
|
233
260
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Fetch data from a URL and save it using the specified options.
|
|
263
|
+
*
|
|
264
|
+
* @async
|
|
265
|
+
* @method
|
|
266
|
+
* @param {object} options - The options for fetching and saving data.
|
|
267
|
+
* @param {string} options.url - The URL to fetch data from.
|
|
268
|
+
* @param {object} options.bulk - Bulk operation options.
|
|
269
|
+
* @param {object} options.save - Save options.
|
|
270
|
+
* @param {object} options.opts - Fetch options.
|
|
271
|
+
*/
|
|
272
|
+
fetchAndSave = async (options) => {
|
|
273
|
+
const { url, bulk, save = {}, opts = {} } = options
|
|
274
|
+
if (!this.app.dobo) throw this.error('unknownPluginOrNotLoaded%s', 'dobo')
|
|
275
|
+
bulk.save = save
|
|
276
|
+
bulk.handler = async (rec, bulk) => {
|
|
277
|
+
const { isFunction, set } = this.app.lib._
|
|
278
|
+
const { recordCreate, recordFind, recordUpdate } = this.app.bajoDb
|
|
279
|
+
const save = bulk.save ?? {}
|
|
280
|
+
const current = save.current ?? {}
|
|
281
|
+
let existing
|
|
282
|
+
let record
|
|
283
|
+
let method
|
|
284
|
+
save.checkUnique = save.checkUnique ?? 'id'
|
|
285
|
+
if (['unique', 'upsert'].includes(save.mode)) {
|
|
286
|
+
const query = isFunction(save.checkUnique) ? await save.checkUnique.call(this, rec, save) : set({}, save.checkUnique, rec[save.checkUnique])
|
|
287
|
+
const resp = await recordFind(save.coll, { query, limit: 1 }, { noCache: true })
|
|
288
|
+
if (resp.length > 0) existing = resp[0]
|
|
289
|
+
}
|
|
290
|
+
if (existing) {
|
|
291
|
+
if (save.mode === 'upsert') {
|
|
292
|
+
const body = save.updateConverter ? await save.updateConverter.call(this, rec, save) : rec
|
|
293
|
+
try {
|
|
294
|
+
record = await recordUpdate(save.coll, existing.id, body)
|
|
295
|
+
method = 'updated'
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error(err)
|
|
298
|
+
method = 'error'
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
method = 'skipped'
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
try {
|
|
305
|
+
record = await recordCreate(save.coll, rec)
|
|
306
|
+
method = 'created'
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(err)
|
|
309
|
+
method = 'error'
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (record && current.coll && current.query) {
|
|
313
|
+
const query = await current.query.call(this, { body: rec, record, opts: save })
|
|
314
|
+
const recs = await recordFind(current.coll, { query }, { noCache: true })
|
|
315
|
+
const rc = current.converter ? await current.converter.call(this, { body: rec, record, opts: save }) : rec
|
|
316
|
+
if (rc) {
|
|
317
|
+
if (recs.length > 0) {
|
|
318
|
+
const id = recs[0].id
|
|
319
|
+
await recordUpdate(current.coll, id, rc)
|
|
320
|
+
} else {
|
|
321
|
+
await recordCreate(current.coll, rc)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return method
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await this.app.startPlugin('dobo')
|
|
239
329
|
await this.fetchBulk(url, bulk, opts)
|
|
240
330
|
}
|
|
241
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Fetch data from a URL in bulk, processing it in steps and applying a handler function to each item.
|
|
334
|
+
*
|
|
335
|
+
* @async
|
|
336
|
+
* @method
|
|
337
|
+
* @param {string} url - The URL to fetch data from.
|
|
338
|
+
* @param {object} bulk - Bulk operation options.
|
|
339
|
+
* @param {object} opts - Fetch options.
|
|
340
|
+
*/
|
|
242
341
|
fetchBulk = async (url, bulk = {}, opts = {}) => {
|
|
243
342
|
const { isFunction } = this.bajo.lib._
|
|
244
343
|
opts.params = opts.params ?? {}
|
|
@@ -252,7 +351,7 @@ async function factory (pkgName) {
|
|
|
252
351
|
this.print.info('batch%s%d', spin.getElapsed(), step)
|
|
253
352
|
const newOpts = await bulk.paramsIncFn.call(this, { url, bulk, opts })
|
|
254
353
|
if (newOpts) opts = newOpts
|
|
255
|
-
const length = await
|
|
354
|
+
const length = await this._fetching({ url, bulk, opts, spin })
|
|
256
355
|
if (length === 0 || (bulk.maxStep > 0 && step >= bulk.maxStep)) {
|
|
257
356
|
this.print.info('allDone')
|
|
258
357
|
break
|
|
@@ -261,10 +360,33 @@ async function factory (pkgName) {
|
|
|
261
360
|
}
|
|
262
361
|
} else {
|
|
263
362
|
const spin = this.print.spinner({ showCounter: true }).start('fetchingStarts')
|
|
264
|
-
await
|
|
363
|
+
await this._fetching({ url, bulk, opts, spin })
|
|
265
364
|
}
|
|
266
365
|
}
|
|
267
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Download a file/resource from a URL.
|
|
369
|
+
*
|
|
370
|
+
* @async
|
|
371
|
+
* @method
|
|
372
|
+
* @param {string} url - The URL to download the file from.
|
|
373
|
+
* @param {object} [opts={}] - Fetch options. See {@link https://undici.nodejs.org//api/Client|Undici Client options} for available options.
|
|
374
|
+
* @param {object} [opts.method='GET'] - HTTP method to use for the request. Defaults to ```'GET'```.
|
|
375
|
+
* @param {object} [opts.auth] - Basic authentication credentials.
|
|
376
|
+
* @param {string} [opts.auth.username] - Username for basic authentication.
|
|
377
|
+
* @param {string} [opts.auth.password] - Password for basic authentication.
|
|
378
|
+
* @param {object} [opts.query] - Query parameters to append to the URL.
|
|
379
|
+
* @param {object} [opts.params] - Alias for ```opts.query```. Query parameters to append to the URL.
|
|
380
|
+
* @param {object} [opts.headers] - HTTP headers to include in the request.
|
|
381
|
+
* @param {object} [opts.body] - Request body to send with the request.
|
|
382
|
+
* @param {boolean} [extra.formData=false] - If true, sends the request body as multipart/form-data. Defaults to ```false```.
|
|
383
|
+
* @param {object} [extra={}] - Extra options for downloading.
|
|
384
|
+
* @param {boolean} [extra.cacheBuster=true] - If true, appends a cache-busting query parameter to the URL. Defaults to ```true```.
|
|
385
|
+
* @param {string} [extra.cacheBusterKey='_'] - The query parameter key to use for cache-busting. Defaults to ```'_'```.
|
|
386
|
+
* @param {object} [extra.agent] - Custom agent options for the fetch request. If provided, overrides the default agent configuration.
|
|
387
|
+
* @param {boolean} [extra.rawResponse=false] - If true, returns the raw response object instead of parsing it as JSON. Defaults to ```false```.
|
|
388
|
+
* @returns {Promise<Object>} - The response object.
|
|
389
|
+
*/
|
|
268
390
|
fetchUrl = async (url, opts = {}, extra = {}) => {
|
|
269
391
|
const { importPkg } = this.app.bajo
|
|
270
392
|
const { fetch, Agent } = await importPkg('bajoExtra:undici')
|
|
@@ -286,6 +408,7 @@ async function factory (pkgName) {
|
|
|
286
408
|
for (const q in query) {
|
|
287
409
|
if (!isSet(query[q])) delete query[q]
|
|
288
410
|
}
|
|
411
|
+
opts.query = opts.query ?? {}
|
|
289
412
|
if (!isEmpty(query)) opts.query = query
|
|
290
413
|
delete opts.params
|
|
291
414
|
if (!has(extra, 'cacheBuster')) extra.cacheBuster = true
|
|
@@ -320,10 +443,29 @@ async function factory (pkgName) {
|
|
|
320
443
|
return await resp.json()
|
|
321
444
|
}
|
|
322
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Gunzip a file, optionally deleting the original file after extraction.
|
|
448
|
+
*
|
|
449
|
+
* @async
|
|
450
|
+
* @method
|
|
451
|
+
* @param {string} file - The path to the file to be gunzipped.
|
|
452
|
+
* @param {boolean} [deleteOld=false] - If true, deletes the original file after extraction. Defaults to false.
|
|
453
|
+
* @returns {Promise<void>} - Resolves when the gunzip operation is complete.
|
|
454
|
+
*/
|
|
323
455
|
gunzip = async (file, deleteOld) => {
|
|
324
456
|
await this.gzip(file, deleteOld, true)
|
|
325
457
|
}
|
|
326
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Gzip or gunzip a file, optionally deleting the original file after the operation.
|
|
461
|
+
*
|
|
462
|
+
* @async
|
|
463
|
+
* @method
|
|
464
|
+
* @param {string} file - The path to the file to be gzipped or gunzipped.
|
|
465
|
+
* @param {boolean} [deleteOld=false] - If true, deletes the original file after the operation. Defaults to ```false```.
|
|
466
|
+
* @param {boolean} [expand=false] - If true, gunzips the file. If false, gzips the file. Defaults to ```false```.
|
|
467
|
+
* @returns {Promise<void>} - Resolves when the gzip or gunzip operation is complete.
|
|
468
|
+
*/
|
|
327
469
|
gzip = async (file, deleteOld, expand) => {
|
|
328
470
|
const { fs } = this.app.lib
|
|
329
471
|
return new Promise((resolve, reject) => {
|
|
@@ -341,30 +483,90 @@ async function factory (pkgName) {
|
|
|
341
483
|
})
|
|
342
484
|
}
|
|
343
485
|
|
|
344
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Hash a given text or object using the specified algorithm and options.
|
|
488
|
+
*
|
|
489
|
+
* @async
|
|
490
|
+
* @method
|
|
491
|
+
* @param {string|object} input - The text or object to be hashed.
|
|
492
|
+
* @param {string} [type='md5'] - The hashing algorithm to use. Defaults to 'md5'. Set to 'bcrypt' for bcrypt hashing or 'short' for a short hash.
|
|
493
|
+
* @param {object} [options={}] - Additional options for the hashing algorithm.
|
|
494
|
+
* @param {string} [options.digest='hex'] - The output encoding for the hash. Defaults to 'hex'.
|
|
495
|
+
* @param {number} [options.salt=10] - The salt rounds for bcrypt hashing. Defaults to 10.
|
|
496
|
+
* @returns {Promise<string>} - The resulting hash.
|
|
497
|
+
*/
|
|
498
|
+
hash = async (input, type = 'md5', options = {}) => {
|
|
345
499
|
const { importPkg } = this.app.bajo
|
|
346
500
|
const bcrypt = await importPkg('bajoExtra:bcrypt')
|
|
347
501
|
options.digest = options.digest ?? 'hex'
|
|
348
|
-
options.salt = options.
|
|
349
|
-
if (typeof
|
|
350
|
-
if (type === 'bcrypt') return await bcrypt.hash(
|
|
502
|
+
options.salt = options.salt ?? 10
|
|
503
|
+
if (typeof input !== 'string') input = JSON.stringify(input)
|
|
504
|
+
if (type === 'bcrypt') return await bcrypt.hash(input, options.salt)
|
|
351
505
|
if (type === 'short') {
|
|
352
506
|
type = 'shake256'
|
|
353
507
|
options.outputLength = 6
|
|
354
508
|
}
|
|
355
|
-
return crypto.createHash(type, options).update(
|
|
509
|
+
return crypto.createHash(type, options).update(input).digest(options.digest)
|
|
356
510
|
}
|
|
357
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Check if a given text is a bcrypt hash.
|
|
514
|
+
*
|
|
515
|
+
* @method
|
|
516
|
+
* @param {string} text - The text to be checked.
|
|
517
|
+
* @returns {boolean} - True if the text is a bcrypt hash, false otherwise.
|
|
518
|
+
*/
|
|
358
519
|
isBcrypt = (text) => {
|
|
359
520
|
// return /^\$2[ayb]\$.{56}$/.test(text)
|
|
360
521
|
return /^\$2[aby]?\$\d{1,2}\$[./A-Za-z0-9]{53}$/.test(text)
|
|
361
522
|
}
|
|
362
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Check if a given text is an MD5 hash.
|
|
526
|
+
*
|
|
527
|
+
* @method
|
|
528
|
+
* @param {string} text - The text to be checked.
|
|
529
|
+
* @returns {boolean} - True if the text is an MD5 hash, false otherwise.
|
|
530
|
+
*/
|
|
363
531
|
isMd5 = (text) => {
|
|
364
532
|
return /^[a-f0-9]{32}$/i.test(text)
|
|
365
533
|
}
|
|
366
534
|
|
|
367
|
-
|
|
535
|
+
/**
|
|
536
|
+
* Check if a given text is a SHA-256 hash.
|
|
537
|
+
*
|
|
538
|
+
* @method
|
|
539
|
+
* @param {string} text - The text to be checked.
|
|
540
|
+
* @returns {boolean} - True if the text is a SHA-256 hash, false otherwise.
|
|
541
|
+
*/
|
|
542
|
+
isSha256 = (text) => {
|
|
543
|
+
return /^[a-f0-9]{64}$/i.test(text)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Check if a given text is an HTML link.
|
|
548
|
+
*
|
|
549
|
+
* @method
|
|
550
|
+
* @param {string} text - The text to be checked.
|
|
551
|
+
* @returns {boolean} - True if the text is an HTML link, false otherwise.
|
|
552
|
+
*/
|
|
553
|
+
isHtmlLink = (text) => {
|
|
554
|
+
return /<a\s+[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi.test(text)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Encrypt a given text using the specified encryption type and options.
|
|
559
|
+
*
|
|
560
|
+
* @async
|
|
561
|
+
* @method
|
|
562
|
+
* @param {string} text - The text to be encrypted.
|
|
563
|
+
* @param {Object} [options={}] - The encryption options.
|
|
564
|
+
* @param {string} [options.type='short'] - The encryption type. Defaults to 'short'.
|
|
565
|
+
* @param {string} [options.subType='qr'] - The encryption sub-type. Defaults to 'qr'.
|
|
566
|
+
* @returns {Promise<string>} - The encrypted text.
|
|
567
|
+
*/
|
|
568
|
+
encrypt = async (text, options = {}) => {
|
|
569
|
+
const { type = 'short', subType = 'qr' } = options
|
|
368
570
|
const { importPkg } = this.app.bajo
|
|
369
571
|
const { ShortCrypt } = await importPkg('bajoExtra:short-crypt')
|
|
370
572
|
const short = (item) => {
|
|
@@ -378,7 +580,19 @@ async function factory (pkgName) {
|
|
|
378
580
|
throw this.error('invalid%s%s', this.t('encryption type'), type)
|
|
379
581
|
}
|
|
380
582
|
|
|
381
|
-
|
|
583
|
+
/**
|
|
584
|
+
* Decrypt a given cipher using the specified decryption type and options.
|
|
585
|
+
*
|
|
586
|
+
* @async
|
|
587
|
+
* @method
|
|
588
|
+
* @param {string} cipher - The cipher to be decrypted.
|
|
589
|
+
* @param {Object} [options={}] - The decryption options.
|
|
590
|
+
* @param {string} [options.type='short'] - The decryption type. Defaults to 'short'.
|
|
591
|
+
* @param {string} [options.subType='qr'] - The decryption sub-type. Defaults to 'qr'.
|
|
592
|
+
* @returns {Promise<string>} - The decrypted text.
|
|
593
|
+
*/
|
|
594
|
+
decrypt = async (cipher, options = {}) => {
|
|
595
|
+
const { type = 'short', subType = 'qr' } = options
|
|
382
596
|
const { importPkg } = this.app.bajo
|
|
383
597
|
const { ShortCrypt } = await importPkg('bajoExtra:short-crypt')
|
|
384
598
|
const short = (item) => {
|
|
@@ -392,11 +606,94 @@ async function factory (pkgName) {
|
|
|
392
606
|
throw this.error('invalid%s%s', this.t('decryption type'), type)
|
|
393
607
|
}
|
|
394
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Generate a random number or letter within a specified range.
|
|
611
|
+
*
|
|
612
|
+
* @method
|
|
613
|
+
* @param {number} min - The minimum value of the range.
|
|
614
|
+
* @param {number} max - The maximum value of the range.
|
|
615
|
+
* @param {boolean} [alpha=false] - Whether to return a letter instead of a number.
|
|
616
|
+
* @returns {number|string} - The generated random number or letter.
|
|
617
|
+
*/
|
|
395
618
|
randomRange = (min, max, alpha) => {
|
|
396
619
|
const num = Math.floor(Math.random() * (max - min + 1) + min)
|
|
397
620
|
if (!alpha) return num
|
|
398
621
|
return String.fromCharCode(96 + num)
|
|
399
622
|
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Get the dimensions for a given thumbnail size name.
|
|
626
|
+
*
|
|
627
|
+
* @method
|
|
628
|
+
* @param {string} name - The name of the thumbnail size (e.g., 's', 'm', 'l', or '<width>x<height>').
|
|
629
|
+
* @returns {number[]} - The width and height of the thumbnail.
|
|
630
|
+
*/
|
|
631
|
+
thumbnailSizes = name => {
|
|
632
|
+
switch (name) {
|
|
633
|
+
case 's': return [36, 36]
|
|
634
|
+
case 'm': return [100, 100]
|
|
635
|
+
case 'l': return [250, 250]
|
|
636
|
+
default: {
|
|
637
|
+
const [w, h] = name.split('x').map(s => parseInt(s))
|
|
638
|
+
if (!w || !h) return [0, 0]
|
|
639
|
+
return [w, h]
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Create a thumbnail for a given image file.
|
|
646
|
+
*
|
|
647
|
+
* @async
|
|
648
|
+
* @method
|
|
649
|
+
* @param {string} file - The path to the image file.
|
|
650
|
+
* @param {Object} [options={}] - The options for creating the thumbnail.
|
|
651
|
+
* @param {string} [options.dir] - The directory to save the thumbnail. Defaults to the same directory as the file.
|
|
652
|
+
* @param {boolean} [options.silent=true] - Whether to silently ignore errors. Defaults to true.
|
|
653
|
+
* @param {string|string[]} [options.size] - The size(s) of the thumbnail. Defaults to the configured sizes.
|
|
654
|
+
* @param {string|string[]} [options.format] - The format(s) of the thumbnail. Defaults to the configured output formats.
|
|
655
|
+
* @param {Object} [options.opts={}] - Additional options for the sharp library.
|
|
656
|
+
* @returns {Promise<void>}
|
|
657
|
+
*/
|
|
658
|
+
createThumbnail = async (file, options = {}) => {
|
|
659
|
+
const { fs } = this.app.lib
|
|
660
|
+
const { isString } = this.app.lib._
|
|
661
|
+
let {
|
|
662
|
+
dir = path.dirname(file),
|
|
663
|
+
silent = true,
|
|
664
|
+
size = this.config.thumbnail.sizes,
|
|
665
|
+
format = this.config.thumbnail.outputFormats,
|
|
666
|
+
opts = {}
|
|
667
|
+
} = options
|
|
668
|
+
if (isString(size)) size = [size]
|
|
669
|
+
if (isString(format)) format = [format]
|
|
670
|
+
const ext = path.extname(file)
|
|
671
|
+
|
|
672
|
+
if (!this.config.thumbnail.inputFormats.includes(ext)) {
|
|
673
|
+
if (silent) return
|
|
674
|
+
throw this.error('tnUnsupportedFormat%s%s', file, this.config.thumbnail.inputFormats)
|
|
675
|
+
}
|
|
676
|
+
const base = path.basename(file, ext)
|
|
677
|
+
fs.ensureDirSync(dir)
|
|
678
|
+
for (const s of size) {
|
|
679
|
+
const [w, h] = this.thumbnailSizes(s)
|
|
680
|
+
if (w === 0 || h === 0) {
|
|
681
|
+
if (silent) continue
|
|
682
|
+
throw this.error('tnInvalidSize%s%s', file, [...size, '<width>x<height>'])
|
|
683
|
+
}
|
|
684
|
+
for (const to of format) {
|
|
685
|
+
const dest = `${dir}/${base}-${s}${to}`
|
|
686
|
+
try {
|
|
687
|
+
await sharp(file)
|
|
688
|
+
.resize(w, h, opts)
|
|
689
|
+
.toFile(dest)
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (silent) continue
|
|
692
|
+
throw err
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
400
697
|
}
|
|
401
698
|
|
|
402
699
|
return BajoExtra
|