npm package: @libeilong/node-file
Description: 针对 Node 环境封装的一组文件 / 媒体处理工具，统一抽象多种资源输入（URL、本地路径、Buffer、Stream、Base64 等），并提供：
Author: lblblong
Latest version: 0.1.5

# @libeilong/node-file

针对 Node 环境封装的一组文件 / 媒体处理工具，统一抽象多种资源输入（URL、本地路径、Buffer、Stream、Base64 等），并提供：

- `NormalFile`：统一的文件抽象，支持自动识别类型、扩展名、MIME、大小等
- `ImageFile` / `AudioFile` / `VideoFile`：在 `NormalFile` 基础上获取媒体元信息
- `DownloadManager`：带本地缓存与清理策略的下载工具
- `FFprobeHelper`：基于 `ffprobe` 的音视频信息解析
- `UploadAdapter`：文件上传适配抽象，方便对接各类对象存储服务

适合在脚本、CLI、服务端（如 NestJS）项目中统一处理文件和媒体资源。

## 安装

```bash
pnpm add @libeilong/node-file
# 或
npm install @libeilong/node-file
# 或
yarn add @libeilong/node-file
```

> 运行环境要求：Node.js `>= 10`

### peerDependencies

本包声明了：

- `@nestjs/common`（内部主要用于日志输出 `Logger`，即使不是 Nest 项目也可安全使用）

### 主要依赖

内部使用的关键依赖（已作为 `dependencies`）包括：

- `axios`：网络请求
- `ffmpeg-ffprobe-static`：提供 `ffprobe` 可执行文件
- `@napi-rs/image`：高性能图片信息读取
- `@libeilong/func`：部分通用工具函数（如宽高比计算）

使用时按正常方式安装本包即可，无需额外安装上述依赖。

---

## 核心概念

### ResourceInput & ResourceType

`ResourceInput` 表示可以被本库统一处理的资源输入类型：

- `string`
  - URL：`http://` / `https://` / `ftp://`
  - 本地文件路径
  - Base64 字符串（支持 `data:` URL 和纯 base64）
- `Buffer`
- `Readable`（Node 可读流）
- `File`（浏览器环境下的 `File` 对象）
- `ArrayBuffer`
- `Uint8Array`

内部会自动识别其对应的 `ResourceType`：

- `'url' | 'path' | 'buffer' | 'stream' | 'file' | 'arraybuffer' | 'uint8array' | 'base64'`

### NormalFile：统一文件抽象

`NormalFile` 是整个包的基础类，用于封装一个“文件资源”的生命周期：

- 自动识别资源类型与来源（URL / 本地路径 / Buffer / 流 / Base64 等）
- 尽可能推断：
  - `extension`：扩展名
  - `mimetype`：MIME 类型
  - `size`：大小（字节）
- 提供统一 API 获取：
  - `getBuffer()`：获取完整 `Buffer`（带缓存）
  - `getStream()`：获取可读流
  - `getPath()`：获取本地路径（URL 会下载到临时目录）
- 管理临时资源：
  - 自动维护 `.cache/temp` 下的临时文件
  - 通过 `clear()` 主动清理缓存的临时文件 / Buffer
- 预留 `oss` 字段，用于保存上传后得到的 `url` / `key`

一个典型示例：

```ts
import { NormalFile } from '@libeilong/node-file'

// 你可以传入 URL、本地路径、Buffer、Stream、Base64 等
const file = new NormalFile('https://example.com/image.jpg')

// 基本信息（部分字段可能是推断得到的）
console.log(file.extension) // 'jpg'
console.log(file.mimetype)  // 'image/jpeg'
console.log(file.size)      // 若可立即获取则有值，否则为 undefined，稍后会延迟计算

// 获取 Buffer（内部会根据资源类型自动处理）
const buffer = await file.getBuffer()

// 获取本地路径（如果是 URL，会先下载到临时目录）
const localPath = await file.getPath()

// 处理完成后，主动清理临时资源
file.clear()
```

在上传场景中，你可以对 `NormalFile` 做统一的前置处理（校验大小 / 类型、计算哈希、生成缩略图等），然后交给自定义的 `UploadAdapter` 完成上传。

---

## 媒体子类

在 `NormalFile` 基础上，本包针对图片、音频、视频提供了更高层的封装。

所有子类都支持 `ResourceInput` 作为构造参数，也提供 `create` / 静态工具方法简化使用。

### ImageFile：图片信息读取

`ImageFile` 使用 `@napi-rs/image` 高性能读取图片元信息，适合在服务端大量处理图片时使用。

- 属性：
  - `metadata?: { width?: number; height?: number; format?: string; size: number; mime?: string }`
  - `width` / `height`：宽高（不存在时为 `0`）
  - `aspectRatioValue`：数值宽高比 `width / height`
  - `aspectRatio`：格式化宽高比（例如 `16:9`、`4:3`），由 `@libeilong/func` 的 `calculateAspectRatio` 计算

- 方法：
  - `getMetadata()`：读取并缓存图片元信息
  - `ImageFile.imageMetadata(resource)`：静态方法，一步拿到元信息 + 宽高比
  - `ImageFile.create(resource | ImageFile)`：如果已经是 `ImageFile` 实例则直接返回

使用示例：

```ts
import { ImageFile } from '@libeilong/node-file'

const img = ImageFile.create('https://example.com/banner.png')

const meta = await img.getMetadata()
console.log(meta.width, meta.height, meta.mime)
console.log('aspect ratio:', img.aspectRatio) // 比如 '16:9'

// 更简洁的静态工具用法
const full = await ImageFile.imageMetadata('https://example.com/banner.png')
// full 包含 width / height / format / size / mime / aspectRatio 等字段
```

### AudioFile：音频信息读取

`AudioFile` 在 `NormalFile` 之上，利用 `FFprobeHelper` 解析音频文件信息：

- `metadata?: MediaInfo`
  - `duration`：时长（秒）
  - `bitrate`：比特率
  - `format`、`size`、`audioCodec`、`mimeType` 等

- 方法：
  - `getMetadata()`：通过 ffprobe 获取音频信息，并在成功后调用 `clear()` 清理临时文件
  - `AudioFile.create(resource | AudioFile)`：工厂方法

```ts
import { AudioFile } from '@libeilong/node-file'

const audio = AudioFile.create('/path/to/song.mp3')

const meta = await audio.getMetadata()
console.log(meta.duration, meta.audioCodec, meta.bitrate)
```

### VideoFile：视频信息读取

`VideoFile` 与 `AudioFile` 类似，但会额外解析视频相关字段：

- `metadata?: MediaInfo`
  - `width` / `height`
  - `fps`：帧率
  - `videoCodec` / `audioCodec`
  - 其他同 `AudioFile`

- getter：
  - `width` / `height`
  - `aspectRatio`：格式化宽高比（内部使用 `calculateAspectRatio`）

- 静态方法：
  - `VideoFile.videoMetadata(resource)`：一步拿到媒体信息 + 宽高比
  - `VideoFile.create(resource | VideoFile)`

```ts
import { VideoFile } from '@libeilong/node-file'

const video = VideoFile.create('https://example.com/video.mp4')

const info = await video.getMetadata()
console.log(info.duration, info.width, info.height, info.fps)

const full = await VideoFile.videoMetadata('https://example.com/video.mp4')
console.log(full.aspectRatio) // 例如 '16:9'
```

> 注意：音视频相关功能依赖 `ffmpeg-ffprobe-static` 内置的 `ffprobe` 可执行文件，必须在可运行 ffprobe 的环境中使用（例如常规服务器、Docker 镜像等）。

---

## DownloadManager：带缓存的下载器

`DownloadManager` 用于按 URL 下载文件到本地，并在 `.cache/download` 目录下做缓存管理：

- 使用 URL 计算 md5 作为缓存 key，保留原始扩展名
- 已下载文件会复用，不会重复下载
- 默认最大缓存体积：`1GB`
- 超出上限后按照访问时间（LRU）清理最久未访问的文件

### 主要方法

- `DownloadManager.download(url): Promise<string>`
  - 下载文件并返回本地路径
  - 如已缓存则只更新访问时间并直接返回原路径
- `DownloadManager.getCacheStatus(): Promise<{ totalSize: number; fileCount: number; maxSize: number }>`
  - 获取当前缓存占用情况
- `DownloadManager.clearCache(): Promise<void>`
  - 手动清空所有缓存文件

### 使用示例

```ts
import { DownloadManager } from '@libeilong/node-file'

// 下载并缓存
const localPath = await DownloadManager.download('https://example.com/video.mp4')
console.log('file saved at:', localPath)

// 查看缓存状态
const status = await DownloadManager.getCacheStatus()
console.log(status.totalSize, status.fileCount)

// 手动清空缓存
await DownloadManager.clearCache()
```

在处理远程媒体文件时，你可以先通过 `DownloadManager` 获取本地路径，再交给 `VideoFile` / `AudioFile` 做后续解析；也可以直接把 URL 交给 `VideoFile`，内部会走统一的下载逻辑。

---

## FFprobeHelper：音视频信息工具

`FFprobeHelper` 是对 `ffprobe` 命令行工具的轻量封装，用于从音视频文件中提取结构化信息。

### MediaInfo 结构

`MediaInfo` 大致包含：

- `duration: number`：时长（秒）
- `width?: number` / `height?: number`
- `bitrate?: number`
- `fps?: number`
- `format?: string`
- `size?: number`：文件大小（字节）
- `audioCodec?: string`
- `videoCodec?: string`
- `mimeType?: string`

### 方法

- `FFprobeHelper.isAvailable(): Promise<boolean>`
  - 检查 ffprobe 是否可用（是否能访问内置可执行文件）
- `FFprobeHelper.getMediaInfo(filePath: string): Promise<MediaInfo>`
  - 调用 ffprobe，解析其 JSON 输出并转换为 `MediaInfo`

简单示例：

```ts
import { FFprobeHelper } from '@libeilong/node-file'

if (!(await FFprobeHelper.isAvailable())) {
  throw new Error('ffprobe not available in current environment')
}

const info = await FFprobeHelper.getMediaInfo('/path/to/video.mp4')
console.log(info.duration, info.fps, info.mimeType)
```

在大多数场景下，你可以直接使用 `AudioFile` / `VideoFile`，只有在需要更底层控制时再直接使用 `FFprobeHelper`。

---

## UploadAdapter：上传适配抽象

`UploadAdapter` 是一个抽象类，用来规范“如何把 `NormalFile` 上传到某个对象存储服务”：

```ts
import { NormalFile, UploadAdapter } from '@libeilong/node-file'

class MyOssAdapter extends UploadAdapter {
  async upload(file: NormalFile) {
    const buffer = await file.getBuffer()

    // 在这里调用具体的 SDK 上传，例如阿里云 OSS / 七牛 / S3 等
    const key = `uploads/${Date.now()}.${file.extension || 'bin'}`

    // await ossClient.put(key, buffer, { contentType: file.mimetype })

    const url = `https://cdn.example.com/${key}`

    // 你可以顺便把结果写回 file.oss，方便后续使用
    file.oss = { key, url }

    return { key, url }
  }
}

// 使用示例
const file = new NormalFile('https://example.com/avatar.png')
const adapter = new MyOssAdapter()

const result = await adapter.upload(file)
console.log(result.key, result.url)
```

借助 `UploadAdapter`，可以在业务层只关心“上传能力接口”，而不关心底层资源是 URL / 文件路径 / Buffer，全部交由 `NormalFile` 统一处理。

---

## 典型使用场景

- 在 **Node 脚本 / CLI** 中统一处理各种来源的文件：本地路径、URL、Base64、Buffer、Stream 等
- 在 **后端服务（如 NestJS）** 中实现：
  - 上传前文件校验（大小、类型、分辨率、时长等）
  - 媒体信息提取（图片宽高、视频时长和帧率、音频码率等）
  - 下载 + 缓存远程媒体资源
- 构建 **媒体处理流水线**：
  - `NormalFile`（统一输入） → `ImageFile` / `AudioFile` / `VideoFile`（信息解析） → 自定义 `UploadAdapter`（上传到 OSS / CDN）

---

## License

MIT