sfiledl 1.0.0 → 1.0.2
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/build/index.cjs +736 -762
- package/build/index.cjs.map +19 -1
- package/build/index.d.ts +140 -110
- package/build/index.mjs +730 -756
- package/build/index.mjs.map +14 -1
- package/package.json +6 -6
package/build/index.cjs
CHANGED
|
@@ -1,760 +1,735 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict'
|
|
2
|
+
'use strict'
|
|
3
3
|
|
|
4
|
-
Object.defineProperty(exports, '__esModule', { value: true })
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
5
5
|
|
|
6
|
-
const playwright = require('playwright')
|
|
7
|
-
const fs = require('fs')
|
|
8
|
-
const path = require('path')
|
|
9
|
-
const os = require('os')
|
|
10
|
-
const process$1 = require('process')
|
|
6
|
+
const playwright = require('playwright')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const os = require('os')
|
|
10
|
+
const process$1 = require('process')
|
|
11
11
|
|
|
12
|
-
const isOk = (result) => result.success
|
|
13
|
-
const isErr = (result) => !result.success
|
|
12
|
+
const isOk = (result) => result.success
|
|
13
|
+
const isErr = (result) => !result.success
|
|
14
14
|
const safeStringify = (value, space) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value, null, space)
|
|
17
|
+
} catch {
|
|
18
|
+
return '[Circular or unserializable]'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
22
21
|
const sanitizeFilename = (name, replacement = '_') => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
22
|
+
const sanitized = name
|
|
23
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, replacement)
|
|
24
|
+
.replace(new RegExp(`${replacement}+`, 'g'), replacement)
|
|
25
|
+
.trim()
|
|
26
|
+
.slice(0, 255)
|
|
27
|
+
return sanitized || 'file.bin'
|
|
28
|
+
}
|
|
29
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
31
30
|
class AppError extends Error {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
31
|
+
code
|
|
32
|
+
retryable
|
|
33
|
+
timestamp
|
|
34
|
+
context
|
|
35
|
+
constructor(message, code, retryable = true, context) {
|
|
36
|
+
super(message)
|
|
37
|
+
this.code = code
|
|
38
|
+
this.retryable = retryable
|
|
39
|
+
this.name = this.constructor.name
|
|
40
|
+
this.timestamp = new Date().toISOString()
|
|
41
|
+
this.context = context !== undefined ? Object.freeze({ ...context }) : undefined
|
|
42
|
+
Error.captureStackTrace?.(this, this.constructor)
|
|
43
|
+
}
|
|
44
|
+
toJSON() {
|
|
45
|
+
return {
|
|
46
|
+
name: this.name,
|
|
47
|
+
message: this.message,
|
|
48
|
+
code: this.code,
|
|
49
|
+
retryable: this.retryable,
|
|
50
|
+
timestamp: this.timestamp,
|
|
51
|
+
context: this.context,
|
|
52
|
+
stack: process.env['NODE_ENV'] === 'development' ? this.stack : undefined,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
56
55
|
}
|
|
57
56
|
class ValidationError extends AppError {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
constructor(message, context) {
|
|
58
|
+
super(message, 'VALIDATION_ERROR', false, context)
|
|
59
|
+
}
|
|
61
60
|
}
|
|
62
61
|
class NetworkError extends AppError {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
constructor(message, context) {
|
|
63
|
+
super(message, 'NETWORK_ERROR', true, context)
|
|
64
|
+
}
|
|
66
65
|
}
|
|
67
66
|
class FileError extends AppError {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
constructor(message, context) {
|
|
68
|
+
super(message, 'FILE_ERROR', false, context)
|
|
69
|
+
}
|
|
71
70
|
}
|
|
72
71
|
class BrowserError extends AppError {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
constructor(message, context) {
|
|
73
|
+
super(message, 'BROWSER_ERROR', true, context)
|
|
74
|
+
}
|
|
76
75
|
}
|
|
77
76
|
class Logger {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (level === undefined)
|
|
146
|
-
return [...this.entries];
|
|
147
|
-
return this.entries.filter((e) => e.level === level);
|
|
148
|
-
}
|
|
77
|
+
static LEVELS = Object.freeze({
|
|
78
|
+
DEBUG: 0,
|
|
79
|
+
INFO: 1,
|
|
80
|
+
WARN: 2,
|
|
81
|
+
ERROR: 3,
|
|
82
|
+
})
|
|
83
|
+
minLevel
|
|
84
|
+
entries = []
|
|
85
|
+
maxEntries = 1000
|
|
86
|
+
logFile
|
|
87
|
+
constructor(minLevel = 'INFO') {
|
|
88
|
+
this.minLevel = minLevel
|
|
89
|
+
}
|
|
90
|
+
setLogLevel(level) {
|
|
91
|
+
this.minLevel = level
|
|
92
|
+
}
|
|
93
|
+
useLogFile(path) {
|
|
94
|
+
this.logFile = path
|
|
95
|
+
}
|
|
96
|
+
shouldLog(level) {
|
|
97
|
+
return Logger.LEVELS[level] >= Logger.LEVELS[this.minLevel]
|
|
98
|
+
}
|
|
99
|
+
format(entry) {
|
|
100
|
+
const ctx = entry.context !== undefined ? ` ${safeStringify(entry.context)}` : ''
|
|
101
|
+
return `[${entry.timestamp}] ${entry.level}: ${entry.message}${ctx}`
|
|
102
|
+
}
|
|
103
|
+
record(entry) {
|
|
104
|
+
this.entries.push(entry)
|
|
105
|
+
if (this.entries.length > this.maxEntries) {
|
|
106
|
+
this.entries.shift()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
output(entry) {
|
|
110
|
+
const formatted = this.format(entry)
|
|
111
|
+
const outputFn = entry.level === 'ERROR' ? console.error : console.log
|
|
112
|
+
outputFn(formatted)
|
|
113
|
+
if (this.logFile !== undefined) {
|
|
114
|
+
void fs.promises.appendFile(this.logFile, `${formatted}\n`).catch(() => {})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
log(level, message, context) {
|
|
118
|
+
if (!this.shouldLog(level)) return
|
|
119
|
+
const entry = {
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
level,
|
|
122
|
+
message,
|
|
123
|
+
...(context !== undefined ? { context: Object.freeze({ ...context }) } : {}),
|
|
124
|
+
}
|
|
125
|
+
this.record(entry)
|
|
126
|
+
this.output(entry)
|
|
127
|
+
}
|
|
128
|
+
debug(message, context) {
|
|
129
|
+
this.log('DEBUG', message, context)
|
|
130
|
+
}
|
|
131
|
+
info(message, context) {
|
|
132
|
+
this.log('INFO', message, context)
|
|
133
|
+
}
|
|
134
|
+
warn(message, context) {
|
|
135
|
+
this.log('WARN', message, context)
|
|
136
|
+
}
|
|
137
|
+
error(message, context) {
|
|
138
|
+
this.log('ERROR', message, context)
|
|
139
|
+
}
|
|
140
|
+
getEntries(level) {
|
|
141
|
+
if (level === undefined) return [...this.entries]
|
|
142
|
+
return this.entries.filter((e) => e.level === level)
|
|
143
|
+
}
|
|
149
144
|
}
|
|
150
145
|
class CliFlagsBuilder {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
146
|
+
help = false
|
|
147
|
+
debug = false
|
|
148
|
+
headless = true
|
|
149
|
+
proxy
|
|
150
|
+
logFile
|
|
151
|
+
json = false
|
|
152
|
+
batch
|
|
153
|
+
concurrency = 1
|
|
154
|
+
retry = 3
|
|
155
|
+
timeout = 60000
|
|
156
|
+
build() {
|
|
157
|
+
const flags = {
|
|
158
|
+
help: this.help,
|
|
159
|
+
debug: this.debug,
|
|
160
|
+
headless: this.headless,
|
|
161
|
+
json: this.json,
|
|
162
|
+
concurrency: this.concurrency,
|
|
163
|
+
retry: this.retry,
|
|
164
|
+
timeout: this.timeout,
|
|
165
|
+
...(this.proxy !== undefined && { proxy: this.proxy }),
|
|
166
|
+
...(this.logFile !== undefined && { logFile: this.logFile }),
|
|
167
|
+
...(this.batch !== undefined && { batch: this.batch }),
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze(flags)
|
|
170
|
+
}
|
|
176
171
|
}
|
|
177
172
|
class CliParser {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const num = Number(value);
|
|
303
|
-
if (Number.isFinite(num))
|
|
304
|
-
flags.timeout = num;
|
|
305
|
-
else
|
|
306
|
-
errors.push(`Invalid timeout: ${value}`);
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
default:
|
|
310
|
-
errors.push(`Unknown flag: --${key}`);
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
173
|
+
static parse(argv) {
|
|
174
|
+
const args = argv.slice(2)
|
|
175
|
+
const flags = new CliFlagsBuilder()
|
|
176
|
+
const positional = []
|
|
177
|
+
const errors = []
|
|
178
|
+
for (let i = 0; i < args.length; i++) {
|
|
179
|
+
const arg = args[i]
|
|
180
|
+
if (arg === undefined || arg === null) continue
|
|
181
|
+
if (arg === '--') {
|
|
182
|
+
positional.push(...args.slice(i + 1).filter((a) => a !== undefined && a !== null))
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
if (arg.startsWith('--')) {
|
|
186
|
+
const [key, ...valueParts] = arg.slice(2).split('=')
|
|
187
|
+
if (key === undefined || key === '') continue
|
|
188
|
+
let value
|
|
189
|
+
if (valueParts.length > 0) {
|
|
190
|
+
value = valueParts.join('=')
|
|
191
|
+
} else if (i + 1 < args.length) {
|
|
192
|
+
const nextArg = args[i + 1]
|
|
193
|
+
if (
|
|
194
|
+
nextArg !== undefined &&
|
|
195
|
+
typeof nextArg === 'string' &&
|
|
196
|
+
!nextArg.startsWith('-')
|
|
197
|
+
) {
|
|
198
|
+
value = nextArg
|
|
199
|
+
i++
|
|
200
|
+
} else {
|
|
201
|
+
value = 'true'
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
value = 'true'
|
|
205
|
+
}
|
|
206
|
+
CliParser.setFlag(flags, key, value ?? 'true', errors)
|
|
207
|
+
} else if (typeof arg === 'string' && arg.startsWith('-') && arg.length === 2) {
|
|
208
|
+
const key = arg[1]
|
|
209
|
+
if (key === undefined || key === '') continue
|
|
210
|
+
const next = args[i + 1]
|
|
211
|
+
const value =
|
|
212
|
+
next !== undefined &&
|
|
213
|
+
typeof next === 'string' &&
|
|
214
|
+
next !== '-' &&
|
|
215
|
+
!next.startsWith('-')
|
|
216
|
+
? args[++i]
|
|
217
|
+
: 'true'
|
|
218
|
+
CliParser.setFlag(flags, key, value ?? 'true', errors)
|
|
219
|
+
} else if (typeof arg === 'string' && arg.startsWith('-') && arg.length > 2) {
|
|
220
|
+
for (let j = 1; j < arg.length; j++) {
|
|
221
|
+
const char = arg[j]
|
|
222
|
+
if (char !== undefined && char !== '') {
|
|
223
|
+
CliParser.setFlag(flags, char, 'true', errors)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else if (typeof arg === 'string') {
|
|
227
|
+
positional.push(arg)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const url = positional[0]
|
|
231
|
+
const saveDir = positional[1]
|
|
232
|
+
if (flags.concurrency < 1 || flags.concurrency > 20) {
|
|
233
|
+
errors.push('--concurrency must be 1-20')
|
|
234
|
+
}
|
|
235
|
+
if (flags.retry < 1 || flags.retry > 10) {
|
|
236
|
+
errors.push('--retry must be 1-10')
|
|
237
|
+
}
|
|
238
|
+
if (flags.timeout < 1000) {
|
|
239
|
+
errors.push('--timeout must be >= 1000ms')
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
url,
|
|
243
|
+
saveDir,
|
|
244
|
+
flags: flags.build(),
|
|
245
|
+
errors: Object.freeze(errors),
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
static setFlag(flags, key, value, errors) {
|
|
249
|
+
const normalized = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
250
|
+
switch (normalized) {
|
|
251
|
+
case 'help':
|
|
252
|
+
case 'h':
|
|
253
|
+
flags.help = value === 'true'
|
|
254
|
+
break
|
|
255
|
+
case 'debug':
|
|
256
|
+
flags.debug = value === 'true'
|
|
257
|
+
break
|
|
258
|
+
case 'headless':
|
|
259
|
+
flags.headless = value !== 'false'
|
|
260
|
+
break
|
|
261
|
+
case 'proxy':
|
|
262
|
+
flags.proxy = value
|
|
263
|
+
break
|
|
264
|
+
case 'logfile':
|
|
265
|
+
case 'log-file':
|
|
266
|
+
flags.logFile = value
|
|
267
|
+
break
|
|
268
|
+
case 'json':
|
|
269
|
+
flags.json = value === 'true'
|
|
270
|
+
break
|
|
271
|
+
case 'batch':
|
|
272
|
+
flags.batch = value
|
|
273
|
+
break
|
|
274
|
+
case 'concurrency': {
|
|
275
|
+
const num = Number(value)
|
|
276
|
+
if (Number.isFinite(num)) flags.concurrency = num
|
|
277
|
+
else errors.push(`Invalid concurrency: ${value}`)
|
|
278
|
+
break
|
|
279
|
+
}
|
|
280
|
+
case 'retry': {
|
|
281
|
+
const num = Number(value)
|
|
282
|
+
if (Number.isFinite(num)) flags.retry = num
|
|
283
|
+
else errors.push(`Invalid retry: ${value}`)
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
case 'timeout': {
|
|
287
|
+
const num = Number(value)
|
|
288
|
+
if (Number.isFinite(num)) flags.timeout = num
|
|
289
|
+
else errors.push(`Invalid timeout: ${value}`)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
errors.push(`Unknown flag: --${key}`)
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
}
|
|
314
297
|
}
|
|
315
298
|
class BrowserManager {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
[Symbol.dispose]() {
|
|
549
|
-
void this[Symbol.asyncDispose]();
|
|
550
|
-
}
|
|
299
|
+
browser = null
|
|
300
|
+
context = null
|
|
301
|
+
page = null
|
|
302
|
+
logger
|
|
303
|
+
config
|
|
304
|
+
constructor(logger, config) {
|
|
305
|
+
this.logger = logger
|
|
306
|
+
const maybeViewport = config.viewport
|
|
307
|
+
const maybeProxy = config.proxy
|
|
308
|
+
const browserConfig = {
|
|
309
|
+
headless: config.headless ?? true,
|
|
310
|
+
userAgent:
|
|
311
|
+
config.userAgent ??
|
|
312
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
313
|
+
acceptDownloads: true,
|
|
314
|
+
...(maybeViewport !== undefined && { viewport: maybeViewport }),
|
|
315
|
+
...(maybeProxy !== undefined && { proxy: maybeProxy }),
|
|
316
|
+
}
|
|
317
|
+
this.config = Object.freeze(browserConfig)
|
|
318
|
+
}
|
|
319
|
+
async launch() {
|
|
320
|
+
try {
|
|
321
|
+
this.logger.debug('Launching browser', { headless: this.config.headless })
|
|
322
|
+
const launchOptions = {
|
|
323
|
+
headless: this.config.headless,
|
|
324
|
+
args: [
|
|
325
|
+
'--no-sandbox',
|
|
326
|
+
'--disable-setuid-sandbox',
|
|
327
|
+
'--disable-dev-shm-usage',
|
|
328
|
+
'--disable-gpu',
|
|
329
|
+
'--disable-blink-features=AutomationControlled',
|
|
330
|
+
],
|
|
331
|
+
}
|
|
332
|
+
if (this.config.proxy !== undefined) {
|
|
333
|
+
const proxyOpts = {
|
|
334
|
+
server: this.config.proxy.server,
|
|
335
|
+
}
|
|
336
|
+
if (this.config.proxy.username !== undefined) {
|
|
337
|
+
proxyOpts.username = this.config.proxy.username
|
|
338
|
+
}
|
|
339
|
+
if (this.config.proxy.password !== undefined) {
|
|
340
|
+
proxyOpts.password = this.config.proxy.password
|
|
341
|
+
}
|
|
342
|
+
launchOptions.proxy = proxyOpts
|
|
343
|
+
}
|
|
344
|
+
this.browser = await playwright.chromium.launch(launchOptions)
|
|
345
|
+
const contextOptions = {
|
|
346
|
+
userAgent: this.config.userAgent,
|
|
347
|
+
acceptDownloads: this.config.acceptDownloads,
|
|
348
|
+
locale: 'en-US',
|
|
349
|
+
timezoneId: 'UTC',
|
|
350
|
+
}
|
|
351
|
+
if (this.config.viewport !== undefined) {
|
|
352
|
+
contextOptions.viewport = this.config.viewport
|
|
353
|
+
}
|
|
354
|
+
this.context = await this.browser.newContext(contextOptions)
|
|
355
|
+
await this.context.addCookies([
|
|
356
|
+
{
|
|
357
|
+
name: 'safe_link_counter',
|
|
358
|
+
value: '1',
|
|
359
|
+
domain: '.sfile.co',
|
|
360
|
+
path: '/',
|
|
361
|
+
expires: Math.floor(Date.now() / 1000) + 3600,
|
|
362
|
+
},
|
|
363
|
+
])
|
|
364
|
+
this.page = await this.context.newPage()
|
|
365
|
+
this.setupListeners()
|
|
366
|
+
this.logger.info('Browser launched successfully')
|
|
367
|
+
return { success: true, value: undefined }
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
370
|
+
this.logger.error('Browser launch failed', { error: error.message })
|
|
371
|
+
await this.cleanup()
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
error: new BrowserError(`Launch failed: ${error.message}`, {
|
|
375
|
+
original: error.name,
|
|
376
|
+
}),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
setupListeners() {
|
|
381
|
+
if (this.page === null) return
|
|
382
|
+
this.page.on('console', (msg) => {
|
|
383
|
+
const type = msg.type()
|
|
384
|
+
if (type === 'error' || type === 'warning') {
|
|
385
|
+
const level = type === 'error' ? 'ERROR' : 'WARN'
|
|
386
|
+
this.logger.log(level, `[CONSOLE] ${msg.text()}`, { location: msg.location().url })
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
this.page.on('pageerror', (err) => {
|
|
390
|
+
this.logger.error('[PAGE ERROR]', { message: err.message })
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
async navigate(url, options) {
|
|
394
|
+
if (this.page === null) {
|
|
395
|
+
return { success: false, error: new NetworkError('Page not initialized') }
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
await this.page.goto(url, {
|
|
399
|
+
waitUntil: options?.waitUntil ?? 'networkidle',
|
|
400
|
+
timeout: options?.timeout ?? 60000,
|
|
401
|
+
})
|
|
402
|
+
return { success: true, value: undefined }
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
405
|
+
return {
|
|
406
|
+
success: false,
|
|
407
|
+
error: new NetworkError(`Navigation failed: ${error.message}`),
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async waitForDownloadButton(timeout = 30000) {
|
|
412
|
+
if (this.page === null) {
|
|
413
|
+
return { success: false, error: new Error('Page not initialized') }
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
const button = this.page.locator('#download')
|
|
417
|
+
await button.waitFor({ state: 'visible', timeout })
|
|
418
|
+
await this.page.waitForFunction(
|
|
419
|
+
() => {
|
|
420
|
+
const btn = document.querySelector('#download')
|
|
421
|
+
if (btn === null) return false
|
|
422
|
+
const href = btn.getAttribute('href')
|
|
423
|
+
const style = window.getComputedStyle(btn)
|
|
424
|
+
return href !== null && href !== '#' && style.pointerEvents !== 'none'
|
|
425
|
+
},
|
|
426
|
+
{ timeout },
|
|
427
|
+
)
|
|
428
|
+
return { success: true, value: undefined }
|
|
429
|
+
} catch (err) {
|
|
430
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
431
|
+
return { success: false, error }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async extractDownloadUrl() {
|
|
435
|
+
if (this.page === null) {
|
|
436
|
+
return { success: false, error: new Error('Page not initialized') }
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const href = await this.page.$eval('#download', (el) => {
|
|
440
|
+
const anchor = el
|
|
441
|
+
return anchor.href
|
|
442
|
+
})
|
|
443
|
+
if (href === null || href === '' || href === '#') {
|
|
444
|
+
return { success: false, error: new Error('Invalid download href') }
|
|
445
|
+
}
|
|
446
|
+
return { success: true, value: href }
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
449
|
+
return { success: false, error }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async waitForDownload(timeout = 60000) {
|
|
453
|
+
if (this.page === null) {
|
|
454
|
+
return { success: false, error: new Error('Page not initialized') }
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const download = await this.page.waitForEvent('download', { timeout })
|
|
458
|
+
return { success: true, value: download }
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
461
|
+
return { success: false, error }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async collectResponses(waitMs = 3000) {
|
|
465
|
+
if (this.page === null) return []
|
|
466
|
+
const responses = []
|
|
467
|
+
const handler = (res) => {
|
|
468
|
+
responses.push(res)
|
|
469
|
+
}
|
|
470
|
+
this.page.on('response', handler)
|
|
471
|
+
await sleep(waitMs)
|
|
472
|
+
this.page.off('response', handler)
|
|
473
|
+
return Object.freeze(responses)
|
|
474
|
+
}
|
|
475
|
+
findFileResponse(responses) {
|
|
476
|
+
return responses
|
|
477
|
+
.slice()
|
|
478
|
+
.reverse()
|
|
479
|
+
.find((r) => {
|
|
480
|
+
const headers = r.headers()
|
|
481
|
+
const disposition = headers['content-disposition']
|
|
482
|
+
return (
|
|
483
|
+
(disposition !== undefined && disposition.includes('attachment')) ||
|
|
484
|
+
r.url().includes('/downloadfile/')
|
|
485
|
+
)
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
async saveDebugArtifacts(errorMessage) {
|
|
489
|
+
if (this.page === null) {
|
|
490
|
+
return { success: false, error: new Error('Page not available') }
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const debugDir = path.join(os.tmpdir(), `sfile_debug_${Date.now()}`)
|
|
494
|
+
await fs.promises.mkdir(debugDir, { recursive: true })
|
|
495
|
+
await Promise.all([
|
|
496
|
+
this.page.screenshot({ path: path.join(debugDir, 'error.png'), fullPage: true }),
|
|
497
|
+
fs.promises.writeFile(path.join(debugDir, 'error.html'), await this.page.content()),
|
|
498
|
+
fs.promises.writeFile(path.join(debugDir, 'error.txt'), errorMessage),
|
|
499
|
+
])
|
|
500
|
+
return { success: true, value: debugDir }
|
|
501
|
+
} catch (err) {
|
|
502
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
503
|
+
return {
|
|
504
|
+
success: false,
|
|
505
|
+
error: new FileError(`Failed to save artifacts: ${error.message}`),
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async cleanup() {
|
|
510
|
+
if (this.page !== null) {
|
|
511
|
+
await this.page.close().catch(() => {})
|
|
512
|
+
this.page = null
|
|
513
|
+
}
|
|
514
|
+
if (this.context !== null) {
|
|
515
|
+
await this.context.close().catch(() => {})
|
|
516
|
+
this.context = null
|
|
517
|
+
}
|
|
518
|
+
if (this.browser !== null) {
|
|
519
|
+
await this.browser.close().catch(() => {})
|
|
520
|
+
this.browser = null
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async [Symbol.asyncDispose]() {
|
|
524
|
+
this.logger.debug('Closing browser resources...')
|
|
525
|
+
await this.cleanup()
|
|
526
|
+
this.logger.info('Browser resources closed')
|
|
527
|
+
}
|
|
528
|
+
[Symbol.dispose]() {
|
|
529
|
+
void this[Symbol.asyncDispose]()
|
|
530
|
+
}
|
|
551
531
|
}
|
|
552
532
|
class Downloader {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
error: new FileError(`Fallback download failed: ${error.message}`),
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
}
|
|
533
|
+
logger
|
|
534
|
+
config
|
|
535
|
+
constructor(logger, config) {
|
|
536
|
+
this.logger = logger
|
|
537
|
+
this.config = Object.freeze(config)
|
|
538
|
+
}
|
|
539
|
+
async download(url) {
|
|
540
|
+
if (!url.includes('sfile.co')) {
|
|
541
|
+
return { success: false, error: new ValidationError('Invalid sfile.co URL', { url }) }
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
await fs.promises.mkdir(this.config.saveDir, { recursive: true })
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
547
|
+
return {
|
|
548
|
+
success: false,
|
|
549
|
+
error: new FileError(`Failed to create save dir: ${error.message}`),
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
let lastError
|
|
553
|
+
for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
|
|
554
|
+
this.logger.info(`Download attempt ${attempt}/${this.config.retryAttempts}`, { url })
|
|
555
|
+
const result = await this.executeDownload(url)
|
|
556
|
+
if (isOk(result)) {
|
|
557
|
+
return result
|
|
558
|
+
}
|
|
559
|
+
lastError = result.error
|
|
560
|
+
if (!lastError.retryable || attempt === this.config.retryAttempts) {
|
|
561
|
+
break
|
|
562
|
+
}
|
|
563
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000)
|
|
564
|
+
this.logger.info(`Retrying in ${delay}ms...`)
|
|
565
|
+
await sleep(delay)
|
|
566
|
+
}
|
|
567
|
+
if (lastError === undefined) {
|
|
568
|
+
return { success: false, error: new Error('Unknown error') }
|
|
569
|
+
}
|
|
570
|
+
return { success: false, error: lastError }
|
|
571
|
+
}
|
|
572
|
+
async executeDownload(url) {
|
|
573
|
+
const browserMgr = new BrowserManager(this.logger, {
|
|
574
|
+
headless: true,
|
|
575
|
+
userAgent:
|
|
576
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
577
|
+
})
|
|
578
|
+
try {
|
|
579
|
+
const launchResult = await browserMgr.launch()
|
|
580
|
+
if (isErr(launchResult)) {
|
|
581
|
+
return { success: false, error: launchResult.error }
|
|
582
|
+
}
|
|
583
|
+
const navResult = await browserMgr.navigate(url)
|
|
584
|
+
if (isErr(navResult)) {
|
|
585
|
+
return { success: false, error: navResult.error }
|
|
586
|
+
}
|
|
587
|
+
const buttonResult = await browserMgr.waitForDownloadButton()
|
|
588
|
+
if (isErr(buttonResult)) {
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
error: new NetworkError(`Button wait failed: ${buttonResult.error.message}`),
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const urlResult = await browserMgr.extractDownloadUrl()
|
|
595
|
+
if (isErr(urlResult)) {
|
|
596
|
+
return {
|
|
597
|
+
success: false,
|
|
598
|
+
error: new NetworkError(`URL extract failed: ${urlResult.error.message}`),
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const intermediateUrl = urlResult.value
|
|
602
|
+
const autoUrl = intermediateUrl.includes('?')
|
|
603
|
+
? `${intermediateUrl}&auto=1`
|
|
604
|
+
: `${intermediateUrl}?auto=1`
|
|
605
|
+
this.logger.debug('Auto URL', { url: autoUrl })
|
|
606
|
+
const downloadPromise = browserMgr.waitForDownload(this.config.timeout)
|
|
607
|
+
const autoNavResult = await browserMgr.navigate(autoUrl, {
|
|
608
|
+
waitUntil: 'commit',
|
|
609
|
+
timeout: this.config.timeout,
|
|
610
|
+
})
|
|
611
|
+
if (isErr(autoNavResult)) {
|
|
612
|
+
return { success: false, error: autoNavResult.error }
|
|
613
|
+
}
|
|
614
|
+
const downloadResult = await downloadPromise
|
|
615
|
+
if (isOk(downloadResult)) {
|
|
616
|
+
return await this.handleDirectDownload(downloadResult.value, this.config.saveDir)
|
|
617
|
+
} else {
|
|
618
|
+
this.logger.warn('Download event failed, trying fallback')
|
|
619
|
+
const responses = await browserMgr.collectResponses(3000)
|
|
620
|
+
const fileResponse = browserMgr.findFileResponse(responses)
|
|
621
|
+
if (fileResponse === undefined) {
|
|
622
|
+
return {
|
|
623
|
+
success: false,
|
|
624
|
+
error: new NetworkError('No file response found in fallback'),
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return await this.handleFallbackDownload(fileResponse, this.config.saveDir)
|
|
628
|
+
}
|
|
629
|
+
} finally {
|
|
630
|
+
await browserMgr[Symbol.asyncDispose]()
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async handleDirectDownload(download, saveDir) {
|
|
634
|
+
try {
|
|
635
|
+
const suggested = download.suggestedFilename()
|
|
636
|
+
const filename = sanitizeFilename(suggested !== undefined ? suggested : 'file.bin')
|
|
637
|
+
const savePath = path.join(saveDir, filename)
|
|
638
|
+
this.logger.info('Saving via direct download', { filename })
|
|
639
|
+
await download.saveAs(savePath)
|
|
640
|
+
const stats = await fs.promises.stat(savePath)
|
|
641
|
+
return {
|
|
642
|
+
success: true,
|
|
643
|
+
value: {
|
|
644
|
+
filename,
|
|
645
|
+
savePath,
|
|
646
|
+
size: stats.size,
|
|
647
|
+
method: 'direct',
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
} catch (err) {
|
|
651
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
652
|
+
return {
|
|
653
|
+
success: false,
|
|
654
|
+
error: new FileError(`Direct download failed: ${error.message}`),
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async handleFallbackDownload(response, saveDir) {
|
|
659
|
+
try {
|
|
660
|
+
const buffer = await response.body()
|
|
661
|
+
const urlPath = response.url().split('?')[0] ?? 'unknown'
|
|
662
|
+
const base = path.basename(urlPath)
|
|
663
|
+
const filename = sanitizeFilename(base !== '' ? base : 'file.bin')
|
|
664
|
+
const savePath = path.join(saveDir, filename)
|
|
665
|
+
await fs.promises.writeFile(savePath, buffer)
|
|
666
|
+
this.logger.info('Saved via fallback', { filename, size: buffer.length })
|
|
667
|
+
return {
|
|
668
|
+
success: true,
|
|
669
|
+
value: {
|
|
670
|
+
filename,
|
|
671
|
+
savePath,
|
|
672
|
+
size: buffer.length,
|
|
673
|
+
method: 'fallback',
|
|
674
|
+
},
|
|
675
|
+
}
|
|
676
|
+
} catch (err) {
|
|
677
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
678
|
+
return {
|
|
679
|
+
success: false,
|
|
680
|
+
error: new FileError(`Fallback download failed: ${error.message}`),
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
708
684
|
}
|
|
709
685
|
class App {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
console.log(`
|
|
686
|
+
logger
|
|
687
|
+
constructor() {
|
|
688
|
+
this.logger = new Logger('INFO')
|
|
689
|
+
}
|
|
690
|
+
async run() {
|
|
691
|
+
const args = CliParser.parse(process.argv)
|
|
692
|
+
if (args.errors.length > 0) {
|
|
693
|
+
for (const err of args.errors) {
|
|
694
|
+
this.logger.error(`CLI error: ${err}`)
|
|
695
|
+
}
|
|
696
|
+
this.showHelp()
|
|
697
|
+
return 2
|
|
698
|
+
}
|
|
699
|
+
if (args.flags.help || args.url === undefined) {
|
|
700
|
+
this.showHelp()
|
|
701
|
+
return 0
|
|
702
|
+
}
|
|
703
|
+
this.logger.info('Starting Sfile Downloader', {
|
|
704
|
+
url: args.url,
|
|
705
|
+
saveDir: args.saveDir ?? process.cwd(),
|
|
706
|
+
headless: args.flags.headless,
|
|
707
|
+
})
|
|
708
|
+
const downloader = new Downloader(this.logger, {
|
|
709
|
+
saveDir: args.saveDir ?? process.cwd(),
|
|
710
|
+
timeout: args.flags.timeout,
|
|
711
|
+
retryAttempts: args.flags.retry,
|
|
712
|
+
})
|
|
713
|
+
const result = await downloader.download(args.url)
|
|
714
|
+
if (isOk(result)) {
|
|
715
|
+
this.logger.info('✅ Download complete', {
|
|
716
|
+
file: result.value.filename,
|
|
717
|
+
size: `${Math.round(result.value.size / 1024)} KB`,
|
|
718
|
+
method: result.value.method,
|
|
719
|
+
})
|
|
720
|
+
console.log(`Saved: ${result.value.savePath}`)
|
|
721
|
+
return 0
|
|
722
|
+
} else {
|
|
723
|
+
this.logger.error('❌ Download failed', {
|
|
724
|
+
error: result.error.message,
|
|
725
|
+
code: result.error.code,
|
|
726
|
+
retryable: result.error.retryable,
|
|
727
|
+
})
|
|
728
|
+
return 1
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
showHelp() {
|
|
732
|
+
console.log(`
|
|
758
733
|
🚀 Sfile Downloader - Strict TypeScript CLI
|
|
759
734
|
Usage:
|
|
760
735
|
bun run src/index.ts <url> [saveDir] [options]
|
|
@@ -780,41 +755,40 @@ Exit Codes:
|
|
|
780
755
|
0 Success
|
|
781
756
|
1 Download/operation error
|
|
782
757
|
2 CLI/validation error
|
|
783
|
-
`)
|
|
784
|
-
|
|
758
|
+
`)
|
|
759
|
+
}
|
|
785
760
|
}
|
|
786
761
|
const main = async () => {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
};
|
|
762
|
+
const app = new App()
|
|
763
|
+
const shutdown = async (signal) => {
|
|
764
|
+
console.error(`\nReceived ${signal}, shutting down...`)
|
|
765
|
+
process$1.exit(130)
|
|
766
|
+
}
|
|
767
|
+
process.once('SIGINT', () => {
|
|
768
|
+
void shutdown('SIGINT')
|
|
769
|
+
})
|
|
770
|
+
process.once('SIGTERM', () => {
|
|
771
|
+
void shutdown('SIGTERM')
|
|
772
|
+
})
|
|
773
|
+
try {
|
|
774
|
+
const exitCode = await app.run()
|
|
775
|
+
process$1.exit(exitCode)
|
|
776
|
+
} catch (err) {
|
|
777
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
778
|
+
console.error(`[FATAL] Unhandled error: ${error.message}`)
|
|
779
|
+
if (process.env['NODE_ENV'] === 'development' && error.stack !== undefined) {
|
|
780
|
+
console.error(error.stack)
|
|
781
|
+
}
|
|
782
|
+
process$1.exit(1)
|
|
783
|
+
}
|
|
784
|
+
}
|
|
811
785
|
if (require.main === module) {
|
|
812
|
-
|
|
786
|
+
void main()
|
|
813
787
|
}
|
|
814
788
|
|
|
815
|
-
exports.App = App
|
|
816
|
-
exports.BrowserManager = BrowserManager
|
|
817
|
-
exports.CliParser = CliParser
|
|
818
|
-
exports.Downloader = Downloader
|
|
819
|
-
exports.Logger = Logger
|
|
789
|
+
exports.App = App
|
|
790
|
+
exports.BrowserManager = BrowserManager
|
|
791
|
+
exports.CliParser = CliParser
|
|
792
|
+
exports.Downloader = Downloader
|
|
793
|
+
exports.Logger = Logger
|
|
820
794
|
//# sourceMappingURL=index.cjs.map
|