# object-storage Specification

## Purpose
TBD - created by archiving change add-object-storage. Update Purpose after archive.
## Requirements
### Requirement: Provider 接口

系统 SHALL 提供统一的对象存储 Provider 接口，支持 S3 兼容的对象存储服务。

接口定义：
```go
type Provider interface {
    Upload(ctx context.Context, key string, reader io.Reader, contentType string) error
    Download(ctx context.Context, key string, writer io.Writer) error
    DownloadToTemp(ctx context.Context, key string) (localPath string, cleanup func(), err error)
    Delete(ctx context.Context, key string) error
    Exists(ctx context.Context, key string) (bool, error)
    GetUploadURL(ctx context.Context, key string, contentType string, expires time.Duration) (string, error)
    GetDownloadURL(ctx context.Context, key string, expires time.Duration) (string, error)
}
```

#### Scenario: 创建 S3 Provider
- **WHEN** 系统启动时读取 storage 配置
- **THEN** 系统 SHALL 创建 S3Provider 实例并验证连接

#### Scenario: 配置缺失
- **WHEN** storage 配置未设置或不完整
- **THEN** 系统 SHALL 记录警告日志并跳过初始化（不影响启动）

---

### Requirement: 文件上传

系统 SHALL 支持通过 Provider 接口上传文件到对象存储。

#### Scenario: 上传成功
- **WHEN** 调用 `Upload(ctx, "imports/test.csv", reader, "text/csv")`
- **THEN** 文件 SHALL 被上传到配置的 Bucket 中指定路径
- **THEN** 方法 SHALL 返回 nil

#### Scenario: 上传失败
- **WHEN** 对象存储服务不可用
- **THEN** 方法 SHALL 返回包含错误详情的 error

---

### Requirement: 文件下载

系统 SHALL 支持从对象存储下载文件。

#### Scenario: 下载到 Writer
- **WHEN** 调用 `Download(ctx, "imports/test.csv", writer)`
- **THEN** 文件内容 SHALL 被写入到提供的 writer

#### Scenario: 下载到临时文件
- **WHEN** 调用 `DownloadToTemp(ctx, "imports/test.csv")`
- **THEN** 系统 SHALL 下载文件到临时目录
- **THEN** 方法 SHALL 返回本地文件路径和 cleanup 函数
- **THEN** 调用 cleanup() 后临时文件 SHALL 被删除

#### Scenario: 文件不存在
- **WHEN** 下载的文件在对象存储中不存在
- **THEN** 方法 SHALL 返回 "文件不存在" 错误

---

### Requirement: 文件删除

系统 SHALL 支持从对象存储删除文件。

#### Scenario: 删除成功
- **WHEN** 调用 `Delete(ctx, "imports/test.csv")`
- **THEN** 文件 SHALL 从对象存储中删除
- **THEN** 方法 SHALL 返回 nil

#### Scenario: 删除不存在的文件
- **WHEN** 删除的文件不存在
- **THEN** 方法 SHALL 返回 nil（幂等操作）

---

### Requirement: 文件存在性检查

系统 SHALL 支持检查文件是否存在于对象存储。

#### Scenario: 文件存在
- **WHEN** 调用 `Exists(ctx, "imports/test.csv")` 且文件存在
- **THEN** 方法 SHALL 返回 (true, nil)

#### Scenario: 文件不存在
- **WHEN** 调用 `Exists(ctx, "imports/test.csv")` 且文件不存在
- **THEN** 方法 SHALL 返回 (false, nil)

---

### Requirement: 预签名上传 URL

系统 SHALL 支持生成预签名上传 URL，允许前端直接上传文件到对象存储。

#### Scenario: 生成上传 URL
- **WHEN** 调用 `GetUploadURL(ctx, "imports/test.csv", "text/csv", 15*time.Minute)`
- **THEN** 方法 SHALL 返回有效的预签名 URL
- **THEN** URL SHALL 在指定时间（15分钟）后过期
- **THEN** 使用该 URL 的 PUT 请求 SHALL 能成功上传文件

#### Scenario: URL 过期后
- **WHEN** 使用过期的预签名 URL 上传
- **THEN** 对象存储 SHALL 返回 403 Forbidden

---

### Requirement: 预签名下载 URL

系统 SHALL 支持生成预签名下载 URL，允许用户直接从对象存储下载文件。

#### Scenario: 生成下载 URL
- **WHEN** 调用 `GetDownloadURL(ctx, "exports/report.xlsx", 24*time.Hour)`
- **THEN** 方法 SHALL 返回有效的预签名 URL
- **THEN** URL SHALL 在指定时间（24小时）后过期
- **THEN** 使用该 URL 的 GET 请求 SHALL 能下载文件

---

### Requirement: 获取上传 URL API

系统 SHALL 提供 API 接口供前端获取预签名上传 URL。

接口定义：
```
POST /api/admin/storage/upload-url
Authorization: Bearer <token>
Content-Type: application/json

Request:
{
  "file_name": "cards.csv",
  "content_type": "text/csv",
  "purpose": "iot_import"
}

Response:
{
  "code": 0,
  "message": "success",
  "data": {
    "upload_url": "http://obs-helf.cucloud.cn/cmp/imports/2025/01/24/abc123.csv?X-Amz-...",
    "file_key": "imports/2025/01/24/abc123.csv",
    "expires_in": 900
  }
}
```

#### Scenario: 获取上传 URL 成功
- **WHEN** 已认证用户调用 POST /api/admin/storage/upload-url
- **AND** 请求包含有效的 file_name、content_type、purpose
- **THEN** 系统 SHALL 返回预签名上传 URL 和 file_key
- **THEN** file_key 格式 SHALL 为 `{purpose}/{year}/{month}/{day}/{uuid}.{ext}`

#### Scenario: 参数缺失
- **WHEN** 请求缺少必填参数
- **THEN** 系统 SHALL 返回 400 错误

#### Scenario: 未认证
- **WHEN** 请求未携带有效 Token
- **THEN** 系统 SHALL 返回 401 错误

---

### Requirement: 文件路径规范

系统 SHALL 按照规范生成文件路径。

路径格式：`{purpose}/{year}/{month}/{day}/{uuid}.{ext}`

支持的 purpose 值：
- `iot_import` → `imports/`
- `export` → `exports/`
- `attachment` → `attachments/`

#### Scenario: 生成导入文件路径
- **WHEN** purpose 为 "iot_import"，file_name 为 "cards.csv"
- **THEN** 生成的 file_key SHALL 匹配 `imports/\d{4}/\d{2}/\d{2}/[a-f0-9-]+\.csv`

#### Scenario: 未知 purpose
- **WHEN** purpose 值不在支持列表中
- **THEN** 系统 SHALL 返回错误 "不支持的文件用途"

---

### Requirement: 配置结构

系统 SHALL 支持通过配置文件配置对象存储参数。

```yaml
storage:
  provider: "s3"
  s3:
    endpoint: "http://obs-helf.cucloud.cn"
    region: "cn-langfang-2"
    bucket: "cmp"
    access_key_id: "${OSS_ACCESS_KEY_ID}"
    secret_access_key: "${OSS_SECRET_ACCESS_KEY}"
    use_ssl: false
    path_style: true
  presign:
    upload_expires: "15m"
    download_expires: "24h"
  temp_dir: "/tmp/junhong-storage"
```

#### Scenario: 环境变量替换
- **WHEN** 配置值为 `${ENV_VAR}` 格式
- **THEN** 系统 SHALL 从环境变量读取实际值

#### Scenario: 临时目录不存在
- **WHEN** temp_dir 目录不存在
- **THEN** 系统 SHALL 自动创建该目录

