Files
v-hono/http_handlers.v
qinqx e7760ea80d feat: integrate multi-cloud storage (S3, OSS, COS, Local)
Add built-in storage support from v-hono-storage module:
- StorageProvider interface with 4 backends
- FileService with HTTP handlers
- Multipart upload support
- 86 property-based tests
- Retry mechanism with exponential backoff

Breaking: None (backward compatible)
2026-01-25 14:07:34 +08:00

779 lines
20 KiB
V

module hono
import net.http
import x.json2
import os
import time
// ============================================================================
// HTTP 请求/响应结构
// ============================================================================
// 文件上传请求(JSON 格式)
pub struct UploadRequest {
pub:
bucket string @[json: 'bucket']
filename string @[json: 'filename']
content_type string @[json: 'content_type']
metadata string @[json: 'metadata']
}
// 分片上传初始化请求
pub struct InitMultipartRequest {
pub:
bucket string @[json: 'bucket']
filename string @[json: 'filename']
file_size i64 @[json: 'file_size']
content_type string @[json: 'content_type']
chunk_size int @[json: 'chunk_size']
}
// 分片上传完成请求
pub struct CompleteMultipartRequest {
pub:
upload_id string @[json: 'upload_id']
}
// 分片上传取消请求
pub struct AbortMultipartRequest {
pub:
upload_id string @[json: 'upload_id']
}
// 预签名 URL 请求
pub struct PresignRequest {
pub:
file_uuid string @[json: 'file_uuid']
expires_in int @[json: 'expires_in']
method string @[json: 'method']
}
// 文件列表请求
pub struct ListFilesRequest {
pub:
bucket string @[json: 'bucket']
prefix string @[json: 'prefix']
storage_type string @[json: 'storage_type']
limit int @[json: 'limit']
offset int @[json: 'offset']
}
// 通用 API 响应
pub struct StorageApiResponse {
pub:
success bool @[json: 'success']
message string @[json: 'message']
data string @[json: 'data'] // JSON 编码的数据
}
// 存储错误响应
pub struct StorageErrorResponse {
pub:
success bool @[json: 'success']
error string @[json: 'error']
code int @[json: 'code']
}
// ============================================================================
// HTTP 处理器
// ============================================================================
// 处理文件上传(单文件)
// POST /upload
// Content-Type: multipart/form-data
// 表单字段: file (文件), bucket (可选), metadata (可选 JSON)
pub fn (mut fs FileService) handle_upload(mut ctx Context) http.Response {
// 解析 multipart 表单数据
content_type := ctx.req.header.get(.content_type) or { '' }
if !content_type.starts_with('multipart/form-data') {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Content-Type must be multipart/form-data'
code: 400
}))
}
// 使用 v-hono 的 multipart 解析器
parser := new_multipart_parser(content_type, ctx.body) or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to parse multipart data: ${err}'
code: 400
}))
}
items := parser.parse() or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to parse form data: ${err}'
code: 400
}))
}
// 提取表单字段
mut file_data := []u8{}
mut filename := ''
mut file_content_type := 'application/octet-stream'
mut bucket := ''
mut metadata := ''
for item in items {
match item.name {
'file' {
file_data = item.content.bytes()
filename = item.filename
if item.content_type != '' {
file_content_type = item.content_type
}
}
'bucket' {
bucket = item.content
}
'metadata' {
metadata = item.content
}
'content_type' {
file_content_type = item.content
}
else {}
}
}
if file_data.len == 0 || filename == '' {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing file data or filename'
code: 400
}))
}
// 上传文件
result := fs.upload_file(UploadParams{
bucket: bucket
filename: filename
content_type: file_content_type
metadata: metadata
}, file_data) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Upload failed: ${err}'
code: 500
}))
}
ctx.status(201)
return ctx.json(json2.encode[UploadResult](result))
}
// 处理文件下载
// GET /download/:file_uuid
pub fn (mut fs FileService) handle_download(mut ctx Context) http.Response {
file_uuid := ctx.params['file_uuid'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing file_uuid parameter'
code: 400
}))
}
// 获取文件信息
file_info := fs.get_file_info(file_uuid) or {
ctx.status(404)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'File not found: ${err}'
code: 404
}))
}
// 检查 Range 请求
range_header := ctx.req.header.get_custom('Range') or { '' }
if range_header != '' {
return fs.handle_range_download(mut ctx, file_info, range_header)
}
// 下载文件
data := fs.download_file(file_uuid) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Download failed: ${err}'
code: 500
}))
}
// 设置响应头
ctx.headers['Content-Type'] = file_info.file_type
ctx.headers['Content-Length'] = data.len.str()
ctx.headers['Content-Disposition'] = 'attachment; filename="${file_info.file_name}"'
ctx.headers['Accept-Ranges'] = 'bytes'
ctx.headers['ETag'] = '"${file_info.file_hash}"'
ctx.status(200)
mut headers := http.new_header()
for key, value in ctx.headers {
headers.add_custom(key, value) or { continue }
}
return http.Response{
status_code: 200
header: headers
body: data.bytestr()
}
}
// 处理 Range 请求下载
fn (mut fs FileService) handle_range_download(mut ctx Context, file_info FileInfo, range_header string) http.Response {
// 解析 Range 头
if !range_header.starts_with('bytes=') {
ctx.status(416)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid Range header'
code: 416
}))
}
range_spec := range_header[6..]
parts := range_spec.split('-')
if parts.len != 2 {
ctx.status(416)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid Range format'
code: 416
}))
}
file_size := file_info.file_size
mut start := i64(0)
mut end := file_size - 1
if parts[0] != '' {
start = parts[0].i64()
}
if parts[1] != '' {
end = parts[1].i64()
}
// 验证范围
if start < 0 || start >= file_size || end < start || end >= file_size {
ctx.status(416)
ctx.headers['Content-Range'] = 'bytes */${file_size}'
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Range not satisfiable'
code: 416
}))
}
// 下载完整文件
data := fs.download_file(file_info.file_uuid) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Download failed: ${err}'
code: 500
}))
}
// 提取范围数据
range_data := data[int(start)..int(end) + 1]
content_length := end - start + 1
// 设置响应头
ctx.headers['Content-Type'] = file_info.file_type
ctx.headers['Content-Length'] = content_length.str()
ctx.headers['Content-Range'] = 'bytes ${start}-${end}/${file_size}'
ctx.headers['Accept-Ranges'] = 'bytes'
ctx.headers['ETag'] = '"${file_info.file_hash}"'
ctx.status(206)
mut headers := http.new_header()
for key, value in ctx.headers {
headers.add_custom(key, value) or { continue }
}
return http.Response{
status_code: 206
header: headers
body: range_data.bytestr()
}
}
// 处理文件删除
// DELETE /files/:file_uuid
pub fn (mut fs FileService) handle_delete(mut ctx Context) http.Response {
file_uuid := ctx.params['file_uuid'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing file_uuid parameter'
code: 400
}))
}
// 删除文件
fs.delete_file(file_uuid) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Delete failed: ${err}'
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[StorageApiResponse](StorageApiResponse{
success: true
message: 'File deleted successfully'
data: ''
}))
}
// ============================================================================
// 分片上传处理器
// ============================================================================
// 初始化分片上传
// POST /multipart/init
pub fn (mut fs FileService) handle_init_multipart(mut ctx Context) http.Response {
// 解析请求体
req := json2.decode[InitMultipartRequest](ctx.body) or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid request body: ${err}'
code: 400
}))
}
if req.filename == '' || req.file_size <= 0 {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing required fields: filename and file_size'
code: 400
}))
}
// 生成对象键
object_key := generate_multipart_object_key(req.filename)
// 初始化分片上传
upload := fs.init_multipart_upload(InitMultipartParams{
bucket: req.bucket
object_key: object_key
file_name: req.filename
file_size: req.file_size
content_type: req.content_type
chunk_size: req.chunk_size
}) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to init multipart upload: ${err}'
code: 500
}))
}
ctx.status(201)
return ctx.json(json2.encode[MultipartUpload](upload))
}
// 上传分片
// POST /multipart/upload/:upload_id/:part_number
pub fn (mut fs FileService) handle_upload_part(mut ctx Context) http.Response {
upload_id := ctx.params['upload_id'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing upload_id parameter'
code: 400
}))
}
part_number_str := ctx.params['part_number'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing part_number parameter'
code: 400
}))
}
part_number := part_number_str.int()
if part_number < 1 {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid part_number: must be >= 1'
code: 400
}))
}
// 获取分片数据
data := ctx.body.bytes()
if data.len == 0 {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Empty part data'
code: 400
}))
}
// 上传分片
result := fs.upload_part(upload_id, part_number, data) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to upload part: ${err}'
code: 500
}))
}
if !result.success {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: result.error_msg
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[ChunkUploadResult](result))
}
// 完成分片上传
// POST /multipart/complete
pub fn (mut fs FileService) handle_complete_multipart(mut ctx Context) http.Response {
// 解析请求体
req := json2.decode[CompleteMultipartRequest](ctx.body) or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid request body: ${err}'
code: 400
}))
}
if req.upload_id == '' {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing upload_id'
code: 400
}))
}
// 完成上传
result := fs.complete_multipart_upload(req.upload_id) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to complete multipart upload: ${err}'
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[StorageResult](result))
}
// 取消分片上传
// POST /multipart/abort
pub fn (mut fs FileService) handle_abort_multipart(mut ctx Context) http.Response {
// 解析请求体
req := json2.decode[AbortMultipartRequest](ctx.body) or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid request body: ${err}'
code: 400
}))
}
if req.upload_id == '' {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing upload_id'
code: 400
}))
}
// 取消上传
fs.abort_multipart_upload(req.upload_id) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to abort multipart upload: ${err}'
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[StorageApiResponse](StorageApiResponse{
success: true
message: 'Multipart upload aborted'
data: ''
}))
}
// 获取上传进度
// GET /multipart/progress/:upload_id
pub fn (fs FileService) handle_upload_progress(mut ctx Context) http.Response {
upload_id := ctx.params['upload_id'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing upload_id parameter'
code: 400
}))
}
// 获取进度
progress := fs.get_upload_progress(upload_id) or {
ctx.status(404)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Upload not found: ${err}'
code: 404
}))
}
ctx.status(200)
return ctx.json(json2.encode[UploadProgress](progress))
}
// ============================================================================
// 文件列表和元数据处理器
// ============================================================================
// 获取文件列表
// GET /files?bucket=xxx&prefix=xxx&limit=100&offset=0
pub fn (fs FileService) handle_list_files(mut ctx Context) http.Response {
bucket := ctx.query['bucket'] or { '' }
prefix := ctx.query['prefix'] or { '' }
storage_type := ctx.query['storage_type'] or { '' }
limit_str := ctx.query['limit'] or { '100' }
offset_str := ctx.query['offset'] or { '0' }
limit := limit_str.int()
offset := offset_str.int()
// 查询文件列表
result := fs.list_files(FileListOptions{
bucket: bucket
prefix: prefix
storage_type: storage_type
limit: if limit > 0 { limit } else { 100 }
offset: if offset >= 0 { offset } else { 0 }
}) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to list files: ${err}'
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[FileListResult](result))
}
// 获取文件信息
// GET /files/:file_uuid/info
pub fn (fs FileService) handle_get_file_info(mut ctx Context) http.Response {
file_uuid := ctx.params['file_uuid'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing file_uuid parameter'
code: 400
}))
}
// 获取文件信息
file_info := fs.get_file_info(file_uuid) or {
ctx.status(404)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'File not found: ${err}'
code: 404
}))
}
ctx.status(200)
return ctx.json(json2.encode[FileInfo](file_info))
}
// 获取预签名 URL
// POST /presign
pub fn (mut fs FileService) handle_presign(mut ctx Context) http.Response {
// 解析请求体
req := json2.decode[PresignRequest](ctx.body) or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Invalid request body: ${err}'
code: 400
}))
}
if req.file_uuid == '' {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing file_uuid'
code: 400
}))
}
expires_in := if req.expires_in > 0 { req.expires_in } else { 3600 }
// 生成预签名 URL
result := fs.get_presigned_url(req.file_uuid, expires_in) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to generate presigned URL: ${err}'
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[PresignResult](result))
}
// 获取预签名 URL (GET 方式)
// GET /presign/:file_uuid?expires_in=3600
pub fn (mut fs FileService) handle_presign_get(mut ctx Context) http.Response {
file_uuid := ctx.params['file_uuid'] or {
ctx.status(400)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Missing file_uuid parameter'
code: 400
}))
}
expires_in_str := ctx.query['expires_in'] or { '3600' }
mut expires_in := expires_in_str.int()
if expires_in <= 0 {
expires_in = 3600
}
// 生成预签名 URL
result := fs.get_presigned_url(file_uuid, expires_in) or {
ctx.status(500)
return ctx.json(json2.encode[StorageErrorResponse](StorageErrorResponse{
success: false
error: 'Failed to generate presigned URL: ${err}'
code: 500
}))
}
ctx.status(200)
return ctx.json(json2.encode[PresignResult](result))
}
// ============================================================================
// 辅助函数
// ============================================================================
// 生成分片上传的对象键
fn generate_multipart_object_key(filename string) string {
now := time.now()
date_prefix := now.custom_format('YYYY/MM/DD')
uuid := generate_file_uuid()
ext := os.file_ext(filename)
return '${date_prefix}/${uuid}${ext}'
}
// ============================================================================
// 路由注册
// ============================================================================
// 注册所有文件服务路由
// prefix: 路由前缀,例如 "/api/storage"
pub fn (mut fs FileService) register_routes(mut app Hono, prefix string) {
// 文件上传
app.post('${prefix}/upload', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_upload(mut ctx)
})
// 文件下载
app.get('${prefix}/download/:file_uuid', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_download(mut ctx)
})
// 文件删除
app.delete('${prefix}/files/:file_uuid', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_delete(mut ctx)
})
// 文件列表
app.get('${prefix}/files', fn [fs] (mut ctx Context) http.Response {
return fs.handle_list_files(mut ctx)
})
// 文件信息
app.get('${prefix}/files/:file_uuid/info', fn [fs] (mut ctx Context) http.Response {
return fs.handle_get_file_info(mut ctx)
})
// 预签名 URL (POST)
app.post('${prefix}/presign', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_presign(mut ctx)
})
// 预签名 URL (GET)
app.get('${prefix}/presign/:file_uuid', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_presign_get(mut ctx)
})
// 分片上传 - 初始化
app.post('${prefix}/multipart/init', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_init_multipart(mut ctx)
})
// 分片上传 - 上传分片
app.post('${prefix}/multipart/upload/:upload_id/:part_number', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_upload_part(mut ctx)
})
// 分片上传 - 完成
app.post('${prefix}/multipart/complete', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_complete_multipart(mut ctx)
})
// 分片上传 - 取消
app.post('${prefix}/multipart/abort', fn [mut fs] (mut ctx Context) http.Response {
return fs.handle_abort_multipart(mut ctx)
})
// 分片上传 - 进度查询
app.get('${prefix}/multipart/progress/:upload_id', fn [fs] (mut ctx Context) http.Response {
return fs.handle_upload_progress(mut ctx)
})
}
// 注册简化的路由(不带前缀)
pub fn (mut fs FileService) register_default_routes(mut app Hono) {
fs.register_routes(mut app, '/storage')
}