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

简介: 前言在日常学习/生活中,下载资源时,大部分情况是通过别人分享的资源站点,找到下载入口然后触发下载。当资源通过url传播的时候,一般也是直接打开,通过浏览器触发下载。资深的冲浪选手,一般会用一些客户端工具(还记得Win上的各种下载器),Mac上笔者有时候会使用 NeatDownloadManager,无 🪜 时也能拥有不错的下载速度

前言

在日常学习/生活中,下载资源时,大部分情况是通过别人分享的资源站点,找到下载入口然后触发下载。

当资源通过url传播的时候,一般也是直接打开,通过浏览器触发下载。

资深的冲浪选手,一般会用一些客户端工具(还记得Win上的各种下载器),Mac上笔者有时候会使用 NeatDownloadManager,无 🪜 时也能拥有不错的下载速度

Coder们用命令行下载文件的方式就很多了,比如最常使用的内置库 curl

下面是最常用的拉取资源的例子

# 链接是第三方服务缩短后的
# -L 参数表明自动对资源进行重定向
curl -L http://mtw.so/6647Rc -o 码上掘金logo.image
# 通过管道
curl -L http://mtw.so/6647Rc >码上掘金logo.image
# 原图链接 https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/759e2aa805c0461b840e0f0f09ed05fa~tplv-k3u1fbpfcp-zoom-1.image

当然 curl 也支持上传下载,以及多种传输协议,具体用法这里就不展开了,感兴趣的读者可以前往Quick Reference: Curl 备忘清单 进一步了解。

本文从 0-1 使用Node实现一个 url文件下载 工具,读者可以收获包含但不限于如下知识点,

Node实现下载文件如何通过Proxy(🪜)代理下载资源通用的Node本地持久化存储方法fs/path/http等模块的常见用法等。

对包含文件下载场景的CLI提供一个实践参考。

下面是简单的使用演示,对实现感兴趣的读者可以接着往下阅读

npx efst http://mtw.so/66eO7c

image.png

url资源下载

先是纯 url资源下载 的场景,本小节将详细展开相关小功能的实现。

Node原生实现

基于读写流操作,可以看到代码还是十分的简洁

import https from 'https'
import fs from 'fs'
import path from 'path'
function downloadByUrl(url: string, filename?: string) {
  const filepath = path.resolve(filename || randomName())
  https.get(url, (response) => {
    // 创建1个可写流
    const writeStream = fs.createWriteStream(filepath)
    response.pipe(writeStream).on('close', () => {
      console.log(`file save to ${filepath}`)
    })
  })
}
// sourceUrl 为前面的原图链接
downloadByUrl(sourceUrl,'test.image')

image.png

示例代码1

下载进度获取

大一点的文件肯定无法实现秒下载,需要获取一下进度,了解现在下载了多少

资源的总大小可以一般可以通过response headers中的content-length字段获取

const sumSize = +response.headers['content-length']

流的传输进度可以通过on data事件间接获取

在不通过response.setEncoding(BufferEncoding)修改的编码时,chunk默认是Buffer类型

let receive = 0
response.on('data', (chunk: Buffer) => {
  receive += chunk.length
  const percentage = receive / sumSize
})

到此进度percentage就可以获取到了

对上面的方法进行稍加改造,增加progressend两个方法(支持链式调用的丐版实现)

function downloadByUrl(url: string, filename?: string) {
  let receive = 0
  // 支持链式调用相关逻辑
  let progressFn: (cur: number, rec: number, sum: number) => void
  let endFn: (filepath: string) => void
  const thisArg = {
    progress: (fn: typeof progressFn) => {
      progressFn = fn
      return thisArg
    },
    end: (fn: typeof endFn) => {
      endFn = fn
      return thisArg
    }
  }
  https.get(url, (response) => {
    // 输出文件路径
    const filepath = path.resolve(filename || randomName())
    // 创建一个可写流
    const writeStream = fs.createWriteStream(filepath)
    const sumSize = +response.headers['content-length']! || 0
    response.on('data', (chunk: Buffer) => {
      receive += chunk.length
      progressFn && progressFn(chunk.length, receive, sumSize)
    })
    response.pipe(writeStream).on('close', () => {
      endFn && endFn(filepath)
    })
  })
  return thisArg
}
// 调用示例
downloadByUrl(sourceUrl, 'test.image')
  .progress((current, receive, sum) => {
    console.log(receive, ((receive / sum) * 100).toFixed(2), '%')
  })
  .end((filepath) => {
    console.log('file save:', filepath)
  })

image.png

示例代码2

重定向处理

部分资源在对外直接暴露时,可能是一个短链,此时就需要做重定向处理

重定向的状态码常见301302,当然还有其它的3开头的这里不赘述

除了状态码,重定向的目标url由response.headers.location表示

这里稍微改造一下之前的代码,添加一个重定向逻辑即可

// 通过url 简单区分一下 资源是 https 还是 http 
const _http = url.startsWith('https') ? https : http
_http.get(
  url,
  {
    // 添加一个UA,避免404
    // 部分短链服务网站没有UA会响应404
    headers: {
      'User-Agent': 'node http module'
    }
  },
  (response) => {
    const { statusCode } = response
    // 判断状态码是否3开头
    if (Math.floor(statusCode! / 100) === 3) {
      // 且存在 location
      if (response.headers.location) {
        // 递归
        downloadByUrl(response.headers.location, filename)
          // 透传事件
          .progress(progressFn)
          .end(endFn)
        return
      }
      // 不存在抛出错误
      throw new Error(
        `url:${url} status ${statusCode} without location header`
      )
    }
  }
)

image.png

示例代码3

为了防止无限重定向,还需要加个次数限制,再简单改造一下上述代码,添加一个配置属性作为入参

interface Options {
  filename: string
  maxRedirects: number
}
function downloadByUrl(url: string, option?: Partial<Options>) {
  const ops: Options = { filename: randomName(), maxRedirects: 10, ...option }
  // 省略一些重复代码
  _http.get(
    url,
    (response) => {
      const { statusCode } = response
      if (Math.floor(statusCode! / 100) === 3 && ops.maxRedirects) {
        ops.maxRedirects -= 1
        // 递归调用
        if (response.headers.location) {
          downloadByUrl(response.headers.location, ops)
          return
        }
      }
    }
  )
  return thisArg
}

示例代码4

请求超时

部分资源由于网络原因可能出现超时,为了避免长时间无反馈等待,可以设置超时时间

http模块支持timeout属性设置

// 接着之前的例子修改部分代码即可
const request = _http.get(
  url,
  {
    // 设置超时时间,单位ms
    timeout: ops.timeout || 300000,
  },
  (response) => {
    // 省略response 逻辑
  }
)
request.on('timeout', () => {
  // 中断请求,输出错误
  request.destroy()
  console.error(`http request timeout url:${url}`)
})

下面是请求 google logo 失败示例

image.png

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


相关文章
|
弹性计算 人工智能 机器人
|
负载均衡 监控 Java
异步编程 - 14 异步、分布式、基于消息驱动的框架 Akka
异步编程 - 14 异步、分布式、基于消息驱动的框架 Akka
440 0
|
8月前
|
机器学习/深度学习 编解码 Java
YOLOv11改进策略【卷积层】| GnConv:一种通过门控卷积和递归设计来实现高效、可扩展、平移等变的高阶空间交互操作
YOLOv11改进策略【卷积层】| GnConv:一种通过门控卷积和递归设计来实现高效、可扩展、平移等变的高阶空间交互操作
243 0
YOLOv11改进策略【卷积层】| GnConv:一种通过门控卷积和递归设计来实现高效、可扩展、平移等变的高阶空间交互操作
|
存储 移动开发
【Sets】使用Google Guava工程中Sets工具包,实现集合的并集/交集/补集/差集
获取两个txt文档的内容~存储进集合中求集合的并集/交集/补集/差集 1 package com.sxd.readLines.aboutDB; 2 3 import java.io.BufferedReader; 4 import java.
2866 0
|
Shell Docker 容器
Docker - 解决同步容器与主机时间报错:Error response from daemon: Error processing tar file(exit status 1): invalid symlink "/usr/share/zoneinfo/UTC" -> "../usr/share/zoneinfo/Asia/Shanghai"
Docker - 解决同步容器与主机时间报错:Error response from daemon: Error processing tar file(exit status 1): invalid symlink "/usr/share/zoneinfo/UTC" -> "../usr/share/zoneinfo/Asia/Shanghai"
1485 0
Docker - 解决同步容器与主机时间报错:Error response from daemon: Error processing tar file(exit status 1): invalid symlink "/usr/share/zoneinfo/UTC" -> "../usr/share/zoneinfo/Asia/Shanghai"
|
存储 SQL 关系型数据库
MySQL 大表拆分
【9月更文挑战第13天】在 MySQL 中,为解决大数据量导致的性能问题,常采用表拆分策略,主要包括水平拆分和垂直拆分。水平拆分按规则将大表拆成多个小表,如范围划分(按时间或 ID)和哈希划分(按字段哈希值)。垂直拆分则按字段相关性拆分,减少表宽度。拆分需注意数据迁移、应用改造、索引优化及分布式事务处理等问题。实施前应充分评估和测试。
1013 8
|
Web App开发 自然语言处理 前端开发
可访问性测试(无障碍测试)
可访问性测试(无障碍测试)
477 0
可访问性测试(无障碍测试)
|
12月前
|
缓存 前端开发 JavaScript
阿里云CDN:怎么让网站变快
阿里云CDN:怎么让网站变快
|
小程序
【经验分享】使用swiper组件制作文字上下滚动播报效果
【经验分享】使用swiper组件制作文字上下滚动播报效果
544 7
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能