从0-1实现文件下载CLI工具(1):https://developer.aliyun.com/article/1394868
示例代码5
Proxy
部分资源访问不顺畅的时候,通常会走服务代理(🪜)
以谷歌的logo
资源链接https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png
要让前面的方法downloadByUrl
顺利执行,就需要其走代理服务
为http
模块添加代理也非常简单,原生提供了一个agent
参数,可用于设置代理
import http from 'http' const request = http.get(url,{ agent: Agent, })
这个Agent
的构造可以直接用社区已经封装好的http-proxy-agent
const HttpProxyAgent = require('http-proxy-agent') const proxy = new HttpProxyAgent('http://127.0.0.1:7890')
在调用时只需将这个proxy
实例传入即可
http.get(url, { agent: proxy })
原有的方法只需要添加一个proxy
入参即可,
const request = _http.get(url, { agent: ops.proxy ? new HttpProxyAgent(ops.proxy) : undefined, })
下面是使用代理成功请求的示例
示例代码6
合法文件名生成
文件下载到本地肯定需要有个名字,如果用随机的或者用户手动输入那肯定体验较差
最常见的就是通过url
的pathname
生成
比如上面的谷歌图片资源,咱们使用URL
构造出一个示例,查看url的构成
new URL(sourceUrl)
文件名就可以取pathname
最后一截,通过path.basename
即可获取
import path from 'path' const url = new URL('http://www.google.com/images/googlelogo_color_92x30dp.png') const filename = path.basename(url.pathname) // googlelogo_color_92x30dp.png
当然文件名也可能会重复,再非覆盖写入的前提下,通过会在文件名后添加"分隔符+数字",比如x.png
,x_1.png
,x 1.png
提取文件名与后缀可以用path.parse
直接获取
import path from 'path' // { ext: '.png', name: 'google' } path.parse('google.png') // { ext: '', name: 'hashname' } path.parse('hashname') // { ext: '.ts', name: 'index.d' } path.parse('index.d.ts') // { ext: '.', name: 'index' } path.parse('index.') // { ext: '', name: '.gitkeep' } path.parse('.gitkeep')
但是针对带有多个 . 的文件名不太友好,比如.d.ts
是期望被当做完整的ext
处理
所以咱们可以对其简单递归包装一下实现1个nameParse
,确保最后parse(input).name === input
即可
function nameParse(filename: string, suffix = '') { const { name, ext } = path.parse(filename) if (name === filename) { return { name, ext: ext + suffix } } return nameParse(name, ext + suffix) }
下面是运行示例
到此完成了name
和ext
的分离
文件名分离后简单进行一下name
的合法性替换,避免出现操作系统不支持的字符
正则来自于Google
function normalizeFilename(name: string) { return name.replace(/[\\/:*?"<>|]/g, '') }
再做文件名去重只需要给name
添加后缀数字即可
url
上的内容还可能存在encode
的情况,比如掘金.png
=> encode => %E6%8E%98%E9%87%91.png
因此咱们在处理从pathname
提取的filename
前先进行必要的decode
decodeURIComponent('%E6%8E%98%E9%87%91.png') // 掘金.png
有了前面的准备工作咱们就可以组装出一个从url
提取合法可用的文件名的方法嘞
function getValidFilenameByUrl(url: string) { const urlInstance = new URL(url) return decodeURIComponent(path.basename(urlInstance.pathname)) } getValidFilenameByUrl('http://a/b/c.png?width=100&height') // c.png
然后是获取不重复的文件路径
function getNoRepeatFilepath(filename: string, dir = process.cwd()) { const { name, ext } = nameParse(filename) let i = 0 let filepath = '' do { filepath = path.join(dir, `${name}${i ? ` ${i}` : ''}${ext}`) i += 1 } while (fs.existsSync(filepath)) return filepath }
最后集成到downloadByUrl
方法中,使输出的文件名可控
// ...code const filename = normalizeFilename( ops.filename || getValidFilenameByUrl(url) || randomName() ) const filepath = ops.override ? path.resolve(filename) : getNoRepeatFilepath(filename) const writeStream = fs.createWriteStream(filepath) // ...code
测试案例运行结果如下
示例代码7
异常错误情况处理
对于非法的url
,资源不存在通常会响应404
等没考虑到的异常场景
可以在上述的downloadByUrl
方法中拓展1个error
方法,用于错误处理
let request: http.ClientRequest let errorFn = (err, source) => { console.log('error url:', source) console.log('error msg:', err.message) console.log() } const responseCallback = (response: http.IncomingMessage) => { const { statusCode } = response // 404 if (statusCode === 404) { request.emit('error', new Error('404 source')) return } } // ...code try { request = _http.get(url, reqOptions, responseCallback) request.on('error', (err) => { request.destroy() errorFn && errorFn(err, url) }) request.on('timeout', () => { request.emit('error', new Error('request timeout')) }) } catch (error: any) { setTimeout(() => { errorFn && errorFn(error, url) }) }
除特殊情况外,统一用request.on('error')
处捕获错误
下面是示例代码及运行结果
示例代码8
封装CLI
Options定义
import { Command } from 'commander' const program = new Command() program .argument('<url>', 'set download source url') .option('-f,--filename <filename>', 'set download filename') .option('-L,--location <times>', 'set location times', '10') .option('-t,--timeout <timeout>', 'set the request timeout(ms)', '3000') .option('-p,--proxy <proxy server>', 'set proxy server') .option('-o,--override', 'override duplicate file', false) .action(defaultCommand)
从0-1实现文件下载CLI工具(3)https://developer.aliyun.com/article/1395072?spm=a2c6h.13148508.setting.28.55964f0ez7IHhI