sfiledl 1.0.0 → 1.0.1

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