从0-1实现文件下载CLI工具(2)

简介: 示例代码5Proxy部分资源访问不顺畅的时候,通常会走服务代理(🪜)以谷歌的logo资源链接https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png要让前面的方法downloadByUrl顺利执行,就需要其走代理服务

从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,
})

下面是使用代理成功请求的示例

image.png

示例代码6

合法文件名生成

文件下载到本地肯定需要有个名字,如果用随机的或者用户手动输入那肯定体验较差

最常见的就是通过urlpathname生成

比如上面的谷歌图片资源,咱们使用URL构造出一个示例,查看url的构成

new URL(sourceUrl)

image.png

文件名就可以取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)
}

下面是运行示例

image.png

到此完成了nameext的分离

文件名分离后简单进行一下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

测试案例运行结果如下

image.png

示例代码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')处捕获错误

下面是示例代码及运行结果

image.png

示例代码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)

image.png

从0-1实现文件下载CLI工具(3)https://developer.aliyun.com/article/1395072?spm=a2c6h.13148508.setting.28.55964f0ez7IHhI

相关文章
|
18天前
|
前端开发 Java 测试技术
性能工具之 JMeter 上传与下载脚本编写
【4月更文挑战第3天】性能测试工作中,文件上传也是经常见的性能压测场景之一,那么 JMeter 文件上传下载脚本怎么做?
39 2
性能工具之 JMeter 上传与下载脚本编写
|
18天前
|
JavaScript 前端开发 数据安全/隐私保护
NodeJS 下构建 命令行工具(CLI) 与 交互式命令界面 的实践
NodeJS 下构建 命令行工具(CLI) 与 交互式命令界面 的实践
264 1
|
18天前
|
存储
从0-1实现文件下载CLI工具(3)
参数转换传递 下面是defaultCommand的逻辑,只需要将相关参数处理后透传给定义的download方法即可,option 不支持 number 所以需要对数字字符串做一些转换
|
18天前
|
存储
从0-1实现文件下载CLI工具(1)
前言 在日常学习/生活中,下载资源时,大部分情况是通过别人分享的资源站点,找到下载入口然后触发下载。 当资源通过url传播的时候,一般也是直接打开,通过浏览器触发下载。 资深的冲浪选手,一般会用一些客户端工具(还记得Win上的各种下载器),Mac上笔者有时候会使用 NeatDownloadManager,无 🪜 时也能拥有不错的下载速度
|
18天前
uniapp下载文件安装
uniapp下载文件安装
|
8月前
|
数据安全/隐私保护 iOS开发
如何使用 altool 命令行工具上传 IPA 包:
如何使用 altool 命令行工具上传 IPA 包:
29 0
|
缓存 前端开发
前端下载并生成文件
前端下载并生成文件
|
Java Android开发 数据安全/隐私保护
ApkScan-PKID 查壳工具下载与使用
一、 关于壳的介绍 1、壳的功能:壳最本质的功能就是实现加载器,壳是指在一个程序的外面再包裹上另外一段代码,保护里面的代码不被非法修改或反编译的程序。它们一般都是先于程序运行,拿到控制权,然后完成它们保护软件的任务,深入点就是在apk外面再套一层壳,在运行的时候这层壳会把真正的apk的dalvik指令集释放出来,为apk加壳是目前主流的防护方案,真正的源码是隐藏在壳之下的,要想拿到源码研究其逻辑就
583 0
ApkScan-PKID 查壳工具下载与使用
|
JavaScript jenkins 持续交付
jenkins下载插件下载不了,解决办法
jenkins下载插件下载不了,解决办法
674 0
jenkins下载插件下载不了,解决办法
|
缓存 安全 JavaScript
如何实现上传文件到 nodejs 和文件下载
最近拿 next.js 做个全栈项目,需要文件上传和下载,这里记录下实现方式,也写一下使用原生 node 代码如何实现。