sfiledl 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs DELETED
@@ -1,794 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict'
3
-
4
- Object.defineProperty(exports, '__esModule', { value: true })
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')
11
-
12
- const isOk = (result) => result.success
13
- const isErr = (result) => !result.success
14
- const safeStringify = (value, space) => {
15
- try {
16
- return JSON.stringify(value, null, space)
17
- } catch {
18
- return '[Circular or unserializable]'
19
- }
20
- }
21
- const sanitizeFilename = (name, replacement = '_') => {
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))
30
- class AppError extends Error {
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
- }
55
- }
56
- class ValidationError extends AppError {
57
- constructor(message, context) {
58
- super(message, 'VALIDATION_ERROR', false, context)
59
- }
60
- }
61
- class NetworkError extends AppError {
62
- constructor(message, context) {
63
- super(message, 'NETWORK_ERROR', true, context)
64
- }
65
- }
66
- class FileError extends AppError {
67
- constructor(message, context) {
68
- super(message, 'FILE_ERROR', false, context)
69
- }
70
- }
71
- class BrowserError extends AppError {
72
- constructor(message, context) {
73
- super(message, 'BROWSER_ERROR', true, context)
74
- }
75
- }
76
- class Logger {
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
- }
144
- }
145
- class CliFlagsBuilder {
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
- }
171
- }
172
- class CliParser {
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
- }
297
- }
298
- class BrowserManager {
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
- }
531
- }
532
- class Downloader {
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
- }
684
- }
685
- class App {
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(`
733
- 🚀 Sfile Downloader - Strict TypeScript CLI
734
- Usage:
735
- bun run src/index.ts <url> [saveDir] [options]
736
- Arguments:
737
- url sfile.co URL to download (required)
738
- saveDir Directory to save files (default: current directory)
739
- Options:
740
- --help, -h Show this help message
741
- --debug Enable debug logging
742
- --headless=BOOL Run browser headless (default: true)
743
- --proxy=URL Proxy server URL
744
- --log-file=PATH Write logs to file
745
- --json Output logs as JSON
746
- --batch=FILE Download URLs from file (one per line)
747
- --concurrency=N Parallel downloads (1-20, default: 1)
748
- --retry=N Max retry attempts (1-10, default: 3)
749
- --timeout=MS Operation timeout in ms (default: 60000)
750
- Examples:
751
- bun run src/index.ts https://sfile.co/xyz ./downloads
752
- bun run src/index.ts --batch=urls.txt --concurrency=3
753
- bun run src/index.ts https://sfile.co/abc --debug --headless=false
754
- Exit Codes:
755
- 0 Success
756
- 1 Download/operation error
757
- 2 CLI/validation error
758
- `)
759
- }
760
- }
761
- const main = async () => {
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
- }
785
- if (require.main === module) {
786
- void main()
787
- }
788
-
789
- exports.App = App
790
- exports.BrowserManager = BrowserManager
791
- exports.CliParser = CliParser
792
- exports.Downloader = Downloader
793
- exports.Logger = Logger
794
- //# sourceMappingURL=index.cjs.map