# OSS Uploader

将本地目录上传到对象存储（当前支持华为云 OBS）的 npm 包，提供 CLI 与库两种使用方式。采用中间件 + Provider 抽象，便于后续切换或接入其他云厂商（如阿里云 OSS、AWS S3）。

---

## 功能特性

- **目录上传**：按本地目录结构上传到指定 Bucket，远端 key 规则为 `basePrefix / env / pathPrefix / 相对路径`。
- **同名跳过**：上传前按前缀调用 OBS `listObjects` 拉取已有 key，内存 Set 判断是否存在，**仅 1 次（或少量）列表请求**，不再逐文件 head，大幅提速。
- **强制覆盖**：环境变量 `FILE_RE_WHITE_LIST`（JSON 数组）中匹配到的 key 始终上传、不跳过。
- **过滤规则**：支持 `--include` / `--exclude` 通配符（如 `**/*.js`、`**/*.map`），使用 [micromatch](https://github.com/micromatch/micromatch)。
- **上传进度**：控制台显示进度条（当前/总数、百分比），基于 [cli-progress](https://www.npmjs.com/package/cli-progress)。
- **CDN 地址**：可选配置 `OBS_CDN_BASE_URL`，上传完成后控制台打印每个文件的访问 URL。
- **可切换 Provider**：通过环境变量 `OSS_PROVIDER` 与单一配置文件 `ProviderConfigResolverImpl` 即可切换云厂商，无需改业务代码。

---

## 环境要求

- **Node.js**：>= 18
- **包管理**：npm / pnpm / yarn 均可

---

## 安装

```bash
# 从本地或私有 registry 安装（请将 @your-scope/oss-uploader 替换为实际包名与版本）
npm install @your-scope/oss-uploader

# 或在本仓库根目录
pnpm install
pnpm run build
```

---

## 配置

所有配置通过**环境变量**读取，不依赖 `.env` 文件（若使用 dotenv 可在进程内自行加载）。

### 华为 OBS（当前默认）

| 变量名 | 必填 | 说明 |
|--------|------|------|
| `OBS_ENDPOINT` | 是 | OBS 服务地址，如 `https://obs.cn-north-4.myhuaweicloud.com` |
| `OBS_ACCESS_KEY` | 是 | 访问密钥 AK |
| `OBS_SECRET_KEY` | 是 | 访问密钥 SK |
| `OBS_REGION` | 否 | 区域，如 `cn-north-4` |
| `OBS_BASE_PREFIX` | 否 | 所有对象 key 的基础前缀（仅在配置中，不通过 CLI 暴露），如 `company/project-a` |
| `OBS_CDN_BASE_URL` | 否 | CDN 加速根地址，用于上传完成后拼出访问 URL，如 `https://cdn.example.com` |
| `FILE_RE_WHITE_LIST` | 否 | JSON 数组字符串，key 包含其中任一项则强制上传不跳过，如 `["index.html","sw.js"]` |

### 多云与切换

| 变量名 | 必填 | 说明 |
|--------|------|------|
| `OSS_PROVIDER` | 否 | 当前使用的存储，默认 `huawei`。后续可扩展 `aliyun`、`aws` 等。 |

示例 `.env.example`（可复制为 `.env` 并按实际填写）：

```env
# 华为 OBS
OBS_ENDPOINT=https://obs.cn-north-4.myhuaweicloud.com
OBS_ACCESS_KEY=your-access-key-id
OBS_SECRET_KEY=your-secret-access-key
OBS_REGION=cn-north-4
OBS_BASE_PREFIX=company/project-a
OBS_CDN_BASE_URL=https://cdn.example.com

# 可选：强制覆盖的 key 片段（JSON 数组）
# FILE_RE_WHITE_LIST=["index.html","sw.js"]

# 可选：切换存储厂商
# OSS_PROVIDER=huawei
```

---

## 使用方式

### 一、命令行（CLI）

安装后可通过 `oss-uploader` 命令上传本地目录（若未全局安装，可使用 `npx oss-uploader` 或 `pnpm exec oss-uploader`）。

```bash
# 基本用法：上传 ./dist 到指定 bucket
oss-uploader ./dist -b my-bucket --path-prefix app/v1/

# 带 include/exclude
oss-uploader ./dist -b my-bucket --path-prefix app/v1/ \
  --include "**/*.js" --include "**/*.css" \
  --exclude "**/*.map"

# 指定环境标识（会参与 key 拼接）
npx oss-uploader ./dist -e dev --path-prefix web/app
```

**CLI 参数说明**

| 参数 | 简写 | 必填 | 说明 |
|------|------|------|------|
| `localDir` | - | 是 | 本地目录路径 |
| `--bucket` | `-b` | 否 | Bucket 名称，默认 `hr-uat` |
| `--env` | `-e` | 否 | 环境标识，参与 key 拼接，默认 `dev` |
| `--path-prefix` | - | 否 | 业务路径前缀，拼在 base/env 之后 |
| `--include` | - | 否 | 包含的文件模式，可多次 |
| `--exclude` | - | 否 | 排除的文件模式，可多次 |

上传过程中会显示进度条（当前/总数、百分比），结束后输出：成功/失败/跳过的 key 列表；若配置了 CDN 会打印访问地址。

---

### 二、作为库使用

包入口仅暴露一个上传方法 `upload` 及类型 `UploadOptions`、`UploadResult`，内部配置与 Reporter 固定（从环境变量读取，进度输出到控制台）。

```ts
import { upload, type UploadOptions, type UploadResult } from '@dd-code/oss-uploader';

const result: UploadResult = await upload({
  localDir: './dist',
  bucket: 'my-bucket',
  env: 'prod',
  pathPrefix: 'app/v1',
  include: ['**/*.js', '**/*.css'],
  exclude: ['**/*.map'],
});

console.log(`总数: ${result.total}, 成功: ${result.success.length}, 失败: ${result.failed.length}, 跳过: ${result.skipped.length}`);
```

需要自定义 Reporter（如对接企业微信、钉钉）时，可直接使用中间件层 `uploadDirectory` 并传入 `reporter`（见源码 `middleware/OssService.ts`）。

---

## 远端 Key 规则

单个文件的远端 key 由以下部分按顺序拼接（空段不参与）：

```
basePrefix / env / pathPrefix / 相对路径
```

- **basePrefix**：来自配置 `OBS_BASE_PREFIX`，仅在配置中设置，不通过 CLI 暴露，默认都会带上前缀。
- **env**：CLI 的 `--env` 或库入参的 `env`。
- **pathPrefix**：CLI 的 `--path-prefix` 或库入参的 `pathPrefix`。
- **相对路径**：相对 `localDir` 的路径，保持目录结构。

示例：`basePrefix=company/proj`、`env=prod`、`pathPrefix=web`、本地文件 `dist/js/app.js` → 远端 key：`company/proj/prod/web/js/app.js`。

---

## 架构与设计

### 分层结构

```
┌─────────────────────────────────────────────────────────┐
│  CLI / 业务代码                                           │
│  CLI 调用 uploadDirectory，库调用 upload(options)         │
└───────────────────────────┬─────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────┐
│  中间件层：OssService                                     │
│  解析配置、创建 StorageClient、组装 DirectoryUploadFlow   │
└───────────────────────────┬─────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────┐
│  流程层：UploadService / DirectoryUploadFlow              │
│  收集文件 → 过滤 → listObjectKeys 拉取已有 key →          │
│  逐文件 Set 判断 / head 回退 → putObject → Reporter       │
└───────────────────────────┬─────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────┐
│  Provider 层：StorageClient 实现（如 HuaweiObsClient）   │
│  listObjectKeys、headObject、putObject 对应 OBS API      │
└─────────────────────────────────────────────────────────┘
```

### 设计模式简述

- **Facade**：对外只暴露 `upload(options)`，内部固定配置与 Reporter。
- **Template Method**：`UploadService` 定义「收集 → 过滤 → 上传 → 上报」骨架，`DirectoryUploadFlow` 继承并执行。
- **Strategy / Adapter**：各云厂商实现 `StorageClient`（含可选 `listObjectKeys`），由 `StorageFactory` 按配置创建。

### 目录结构（源码）

```
src/
├── index.ts                 # 包入口，仅导出 upload / UploadOptions / UploadResult
├── cli.ts                   # CLI 入口（oss-uploader）
├── config/
│   ├── types.ts             # 配置与 Provider 类型
│   ├── EnvConfigResolverImpl.ts
│   ├── ProviderConfigResolverImpl.ts
│   └── loadObsCredentialsFromUrl.ts
├── core/
│   ├── StorageClient.ts     # 存储客户端接口（含可选 listObjectKeys）
│   ├── StorageFactory.ts
│   ├── UploadService.ts     # 上传流程基类（收集 → list/head 判断 → put）
│   ├── DirectoryUploadFlow.ts
│   ├── reporter.ts          # Reporter 与 ConsoleReporter（含进度条）
│   ├── filters.ts
│   └── urlHelper.ts
├── middleware/
│   └── OssService.ts        # 统一 API，解析配置并调用 Flow
└── providers/
    └── huawei/
        └── HuaweiObsClient.ts   # OBS 的 listObjectKeys / headObject / putObject
```

---

## 扩展与二次开发

### 切换云厂商（如阿里云 OSS）

1. 在 `src/config/types.ts` 中扩展 `ProviderType` 与对应配置类型（如 `AliyunOssConfig`）、在 `ProviderConfig` 中增加分支。
2. 在 `src/config/ProviderConfigResolverImpl.ts` 的 `resolve()` 中增加 `case 'aliyun'`，从环境变量读取阿里云配置，返回 `{ providerConfig, basePrefix, cdnBaseUrl }`。
3. 在 `src/core/StorageFactory.ts` 中增加 `case 'aliyun'`，创建阿里云 Provider 实例。
4. 在 `src/providers/` 下新增对应客户端，实现 `StorageClient` 的 `headObject`、`putObject`，可选实现 `listObjectKeys` 以启用「先 list 再判断」的加速逻辑。

业务侧只需设置 `OSS_PROVIDER=aliyun` 及对应环境变量，无需改 OssService / CLI。

### 自定义 Reporter

实现接口（含可选 `onProgress`）：

```ts
interface Reporter {
  onStart?(context: { env?, bucket, basePrefix, pathPrefix, localDir, cdnBaseUrl? }): void | Promise<void>;
  onProgress?(current: number, total: number): void | Promise<void>;
  onFileResult?(result: UploadFileResult): void | Promise<void>;
  onComplete?(summary: UploadSummary): void | Promise<void>;
}
```

需直接使用中间件层 `uploadDirectory` 并传入 `reporter`（包入口 `upload()` 不暴露 reporter 参数），详见 `middleware/OssService.ts`。

### 工具方法

- 包入口仅导出 `upload`、`UploadOptions`、`UploadResult`。`buildCdnAccessUrl` 等工具在内部使用，若需在业务中拼 CDN URL 可参考 `core/urlHelper.ts`。

---

## 构建与发布

```bash
# 安装依赖
pnpm install

# 构建（Rollup 输出 dist/index.cjs、dist/cli.cjs 等）
pnpm run build

# 监听模式
pnpm run dev
```

发布前请将 `package.json` 中的 `name` 改为实际 scope 与包名（当前为 `@dd-code/oss-uploader`），按需配置 `publishConfig` 与 registry。

---

