Files
v-hono/tencent_cos.v
T
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

1007 lines
28 KiB
V
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
module hono
import io
import net.http
import net.urllib
import time
import crypto.sha1
import crypto.md5
import encoding.hex
// TencentCOS 腾讯云 COS 存储提供者
pub struct TencentCOS {
config TencentCOSConfig
mut:
// 用于跟踪分片上传的内存存储
multipart_uploads map[string]COSMultipartUploadState
}
// COS 分片上传状态
struct COSMultipartUploadState {
mut:
bucket string
key string
upload_id string
content_type string
parts map[int]PartInfo
created_at i64
}
// 创建腾讯云 COS 存储提供者
pub fn new_tencent_cos(config TencentCOSConfig) !TencentCOS {
// 验证配置
validation := validate_tencent_cos_config(config)
if !validation.valid {
return error(validation.error_message)
}
return TencentCOS{
config: config
multipart_uploads: map[string]COSMultipartUploadState{}
}
}
// ============================================================================
// 腾讯云 COS 签名算法实现 (COS Signature)
// 参考: https://cloud.tencent.com/document/product/436/7778
// ============================================================================
// 签名请求所需的信息
struct COSSigningInfo {
method string
uri string
query_string string
headers map[string]string
signed_headers string
timestamp i64
key_time string
header_list string
url_param_list string
}
// 创建签名信息
fn (c TencentCOS) create_signing_info(method string, uri string, query_params map[string]string, headers map[string]string) COSSigningInfo {
now := time.utc()
timestamp := now.unix()
// 计算签名有效期 (当前时间 - 60秒 到 当前时间 + 3600秒)
start_time := timestamp - 60
end_time := timestamp + 3600
key_time := '${start_time};${end_time}'
// 构建查询字符串
query_string := c.build_canonical_query_string(query_params)
// 获取签名头列表
signed_headers := c.get_signed_headers(headers)
header_list := c.get_header_list(headers)
url_param_list := c.get_url_param_list(query_params)
return COSSigningInfo{
method: method.to_lower()
uri: uri
query_string: query_string
headers: headers
signed_headers: signed_headers
timestamp: timestamp
key_time: key_time
header_list: header_list
url_param_list: url_param_list
}
}
// 构建规范查询字符串
fn (c TencentCOS) build_canonical_query_string(params map[string]string) string {
if params.len == 0 {
return ''
}
// 获取排序后的键(小写)
mut keys := []string{}
mut lower_params := map[string]string{}
for key, value in params {
lower_key := key.to_lower()
keys << lower_key
lower_params[lower_key] = value
}
keys.sort()
mut parts := []string{}
for key in keys {
encoded_key := urllib.query_escape(key)
encoded_value := urllib.query_escape(lower_params[key])
parts << '${encoded_key}=${encoded_value}'
}
return parts.join('&')
}
// 获取签名头列表(小写排序,分号分隔)
fn (c TencentCOS) get_signed_headers(headers map[string]string) string {
mut header_names := []string{}
for key, _ in headers {
lower_key := key.to_lower()
// 只包含需要签名的头
if c.should_sign_header(lower_key) {
header_names << lower_key
}
}
header_names.sort()
return header_names.join(';')
}
// 获取头列表(用于签名)
fn (c TencentCOS) get_header_list(headers map[string]string) string {
mut header_names := []string{}
for key, _ in headers {
lower_key := key.to_lower()
if c.should_sign_header(lower_key) {
header_names << lower_key
}
}
header_names.sort()
return header_names.join(';')
}
// 获取 URL 参数列表(用于签名)
fn (c TencentCOS) get_url_param_list(params map[string]string) string {
if params.len == 0 {
return ''
}
mut keys := []string{}
for key, _ in params {
keys << key.to_lower()
}
keys.sort()
return keys.join(';')
}
// 判断是否需要签名该头
fn (c TencentCOS) should_sign_header(header_name string) bool {
// COS 签名需要包含的头
required_headers := ['host', 'content-type', 'content-length', 'content-md5']
if header_name in required_headers {
return true
}
// x-cos- 开头的头都需要签名
if header_name.starts_with('x-cos-') {
return true
}
return false
}
// 构建规范请求
fn (c TencentCOS) build_canonical_request(info COSSigningInfo) string {
// 构建规范头
mut canonical_headers := ''
mut header_names := []string{}
for key, _ in info.headers {
lower_key := key.to_lower()
if c.should_sign_header(lower_key) {
header_names << lower_key
}
}
header_names.sort()
for name in header_names {
// 查找原始键(不区分大小写)
for key, value in info.headers {
if key.to_lower() == name {
canonical_headers += '${name}=${urllib.query_escape(value.trim_space())}\n'
break
}
}
}
// 移除最后的换行符
if canonical_headers.len > 0 {
canonical_headers = canonical_headers.trim_right('\n')
}
// HttpMethod\nUriPathname\nHttpParameters\nHttpHeaders\n
return '${info.method}\n${info.uri}\n${info.query_string}\n${canonical_headers}\n'
}
// 构建待签名字符串
fn (c TencentCOS) build_string_to_sign(info COSSigningInfo, canonical_request string) string {
// sha1 = lowercase(HexEncode(Hash(HttpString)))
request_hash := sha1.sum(canonical_request.bytes())
hashed_request := hex.encode(request_hash).to_lower()
// StringToSign = q-sign-algorithm\nq-sign-time\nSha1Digest\n
return 'sha1\n${info.key_time}\n${hashed_request}\n'
}
// 计算签名密钥
fn (c TencentCOS) get_signing_key(key_time string) []u8 {
// SignKey = HMAC-SHA1(SecretKey, KeyTime)
return cos_hmac_sha1(c.config.secret_key.bytes(), key_time.bytes())
}
// 计算签名
fn (c TencentCOS) calculate_signature(info COSSigningInfo, string_to_sign string) string {
signing_key := c.get_signing_key(info.key_time)
signature := cos_hmac_sha1(signing_key, string_to_sign.bytes())
return hex.encode(signature).to_lower()
}
// COS HMAC-SHA1 实现
fn cos_hmac_sha1(key []u8, data []u8) []u8 {
block_size := 64
mut k := key.clone()
// 如果 key 长度大于 block_size,先进行 hash
if k.len > block_size {
k = sha1.sum(k)
}
// 如果 key 长度小于 block_size,用 0 填充
for k.len < block_size {
k << u8(0)
}
// 计算 inner 和 outer padding
mut i_pad := []u8{len: block_size}
mut o_pad := []u8{len: block_size}
for i in 0 .. block_size {
i_pad[i] = k[i] ^ u8(0x36)
o_pad[i] = k[i] ^ u8(0x5c)
}
// inner hash: SHA1(i_pad || data)
mut inner_data := i_pad.clone()
inner_data << data
inner_hash := sha1.sum(inner_data)
// outer hash: SHA1(o_pad || inner_hash)
mut outer_data := o_pad.clone()
outer_data << inner_hash
return sha1.sum(outer_data)
}
// 构建 Authorization 头
fn (c TencentCOS) build_authorization_header(info COSSigningInfo, signature string) string {
// q-sign-algorithm=sha1&q-ak=SecretId&q-sign-time=KeyTime&q-key-time=KeyTime&q-header-list=HeaderList&q-url-param-list=UrlParamList&q-signature=Signature
mut auth := 'q-sign-algorithm=sha1'
auth += '&q-ak=${c.config.secret_id}'
auth += '&q-sign-time=${info.key_time}'
auth += '&q-key-time=${info.key_time}'
auth += '&q-header-list=${info.header_list}'
auth += '&q-url-param-list=${info.url_param_list}'
auth += '&q-signature=${signature}'
return auth
}
// 签名请求并返回完整的请求头
pub fn (c TencentCOS) sign_request(method string, uri string, query_params map[string]string, mut headers map[string]string) map[string]string {
// 创建签名信息
info := c.create_signing_info(method, uri, query_params, headers)
// 构建规范请求
canonical_request := c.build_canonical_request(info)
// 构建待签名字符串
string_to_sign := c.build_string_to_sign(info, canonical_request)
// 计算签名
signature := c.calculate_signature(info, string_to_sign)
// 构建 Authorization 头
headers['Authorization'] = c.build_authorization_header(info, signature)
return headers
}
// ============================================================================
// HTTP 请求辅助方法
// ============================================================================
// 获取主机名
fn (c TencentCOS) get_host(bucket string) string {
// COS 域名格式: <BucketName-APPID>.cos.<Region>.myqcloud.com
if bucket != '' {
return '${bucket}.cos.${c.config.region}.myqcloud.com'
}
return 'cos.${c.config.region}.myqcloud.com'
}
// 获取完整 URL
fn (c TencentCOS) get_url(bucket string, key string, query_params map[string]string) string {
host := c.get_host(bucket)
mut path := ''
if key != '' {
// URL 编码 key,但保留 /
path = '/' + c.encode_key(key)
} else {
path = '/'
}
mut url := 'https://${host}${path}'
if query_params.len > 0 {
query_string := c.build_query_string(query_params)
url += '?${query_string}'
}
return url
}
// URL 编码 key(保留 /)
fn (c TencentCOS) encode_key(key string) string {
parts := key.split('/')
mut encoded_parts := []string{}
for part in parts {
encoded_parts << urllib.query_escape(part)
}
return encoded_parts.join('/')
}
// 构建查询字符串(不进行小写转换)
fn (c TencentCOS) build_query_string(params map[string]string) string {
if params.len == 0 {
return ''
}
mut keys := params.keys()
keys.sort()
mut parts := []string{}
for key in keys {
value := params[key]
if value != '' {
parts << '${urllib.query_escape(key)}=${urllib.query_escape(value)}'
} else {
parts << urllib.query_escape(key)
}
}
return parts.join('&')
}
// 获取 URI 路径
fn (c TencentCOS) get_uri_path(key string) string {
if key != '' {
return '/' + c.encode_key(key)
}
return '/'
}
// 执行 HTTP 请求
fn (c TencentCOS) do_request(method http.Method, bucket string, key string, query_params map[string]string, extra_headers map[string]string, payload []u8) !http.Response {
url := c.get_url(bucket, key, query_params)
uri := c.get_uri_path(key)
// 准备请求头
mut headers := extra_headers.clone()
headers['Host'] = c.get_host(bucket)
// 如果有 payload,计算 Content-Length
if payload.len > 0 {
headers['Content-Length'] = payload.len.str()
}
// 签名请求
signed_headers := c.sign_request(method.str(), uri, query_params, mut headers)
// 构建 http.Header
mut http_header := http.Header{}
for k, v in signed_headers {
http_header.add_custom(k, v) or {}
}
// 执行请求
mut config := http.FetchConfig{
url: url
method: method
header: http_header
data: payload.bytestr()
}
response := http.fetch(config) or {
return error(new_timeout_error('tencent_cos', method.str()).msg())
}
return response
}
// 解析 COS 错误响应
fn (c TencentCOS) parse_error_response(response http.Response, operation string) StorageError {
status := response.status_code
// 根据状态码判断错误类型
kind := match status {
403 { StorageErrorKind.access_denied }
404 { StorageErrorKind.object_not_found }
409 { StorageErrorKind.bucket_not_found }
429 { StorageErrorKind.rate_limited }
500, 502, 503, 504 { StorageErrorKind.service_unavailable }
else { StorageErrorKind.unknown }
}
// 尝试从响应体解析错误消息
message := if response.body.len > 0 {
c.extract_error_message(response.body)
} else {
'HTTP ${status}'
}
return new_storage_error_with_status(kind, message, 'tencent_cos', operation, status)
}
// 从 XML 错误响应中提取错误消息
fn (c TencentCOS) extract_error_message(body string) string {
// 简单的 XML 解析,提取 <Message> 标签内容
if message_start := body.index('<Message>') {
if message_end := body.index('</Message>') {
return body[message_start + 9..message_end]
}
}
// 尝试提取 <Code> 标签
if code_start := body.index('<Code>') {
if code_end := body.index('</Code>') {
return body[code_start + 6..code_end]
}
}
return body
}
// 计算数据的 ETag (MD5)
fn (c TencentCOS) calculate_etag(data []u8) string {
hash := md5.sum(data)
mut result := ''
for b in hash {
result += '${b:02x}'
}
return '"${result}"'
}
// ============================================================================
// StorageProvider 接口实现 - 基本操作
// ============================================================================
// 上传文件
pub fn (mut c TencentCOS) upload(bucket string, key string, data []u8, content_type string) !StorageResult {
mut headers := map[string]string{}
if content_type != '' {
headers['Content-Type'] = content_type
} else {
headers['Content-Type'] = 'application/octet-stream'
}
response := c.do_request(.put, bucket, key, map[string]string{}, headers, data)!
if response.status_code != 200 {
return error(c.parse_error_response(response, 'upload').msg())
}
// 从响应头获取 ETag
etag := response.header.get_custom('ETag') or { c.calculate_etag(data) }
return new_storage_result(key, etag, i64(data.len))
}
// 流式上传文件
pub fn (mut c TencentCOS) upload_stream(bucket string, key string, mut reader io.Reader, size i64, content_type string) !StorageResult {
// 读取所有数据
mut data := []u8{}
mut buf := []u8{len: 8192}
for {
n := reader.read(mut buf) or { break }
if n == 0 {
break
}
data << buf[..n]
}
return c.upload(bucket, key, data, content_type)
}
// 下载文件
pub fn (c TencentCOS) download(bucket string, key string) ![]u8 {
response := c.do_request(.get, bucket, key, map[string]string{}, map[string]string{}, []u8{})!
if response.status_code == 404 {
return error(new_not_found_error('tencent_cos', bucket, key).msg())
}
if response.status_code != 200 {
return error(c.parse_error_response(response, 'download').msg())
}
return response.body.bytes()
}
// 流式下载文件
pub fn (c TencentCOS) download_stream(bucket string, key string, mut writer io.Writer) !i64 {
data := c.download(bucket, key)!
written := writer.write(data) or {
return error(new_storage_error(.unknown, 'Failed to write to stream: ${err}', 'tencent_cos', 'download_stream').msg())
}
return i64(written)
}
// 删除文件
pub fn (c TencentCOS) delete(bucket string, key string) ! {
response := c.do_request(.delete, bucket, key, map[string]string{}, map[string]string{}, []u8{})!
// COS 返回 204 或 200 表示成功删除
if response.status_code != 204 && response.status_code != 200 {
if response.status_code == 404 {
return error(new_not_found_error('tencent_cos', bucket, key).msg())
}
return error(c.parse_error_response(response, 'delete').msg())
}
}
// 检查文件是否存在
pub fn (c TencentCOS) exists(bucket string, key string) !bool {
response := c.do_request(.head, bucket, key, map[string]string{}, map[string]string{}, []u8{})!
if response.status_code == 200 {
return true
}
if response.status_code == 404 {
return false
}
return error(c.parse_error_response(response, 'exists').msg())
}
// ============================================================================
// StorageProvider 接口实现 - 元数据操作
// ============================================================================
// 获取文件元数据
pub fn (c TencentCOS) head(bucket string, key string) !ObjectInfo {
response := c.do_request(.head, bucket, key, map[string]string{}, map[string]string{}, []u8{})!
if response.status_code == 404 {
return error(new_not_found_error('tencent_cos', bucket, key).msg())
}
if response.status_code != 200 {
return error(c.parse_error_response(response, 'head').msg())
}
// 解析响应头
content_length := response.header.get_custom('Content-Length') or { '0' }
etag := response.header.get_custom('ETag') or { '' }
content_type := response.header.get_custom('Content-Type') or { 'application/octet-stream' }
last_modified_str := response.header.get_custom('Last-Modified') or { '' }
// 解析 Last-Modified 时间
last_modified := parse_cos_http_date(last_modified_str)
return new_object_info(key, content_length.i64(), etag, content_type, last_modified)
}
// 复制文件
pub fn (mut c TencentCOS) copy(src_bucket string, src_key string, dst_bucket string, dst_key string) !StorageResult {
mut headers := map[string]string{}
// COS 复制源格式: /<BucketName-APPID>.cos.<Region>.myqcloud.com/<ObjectKey>
copy_source := '${src_bucket}.cos.${c.config.region}.myqcloud.com/${src_key}'
headers['x-cos-copy-source'] = copy_source
response := c.do_request(.put, dst_bucket, dst_key, map[string]string{}, headers, []u8{})!
if response.status_code != 200 {
return error(c.parse_error_response(response, 'copy').msg())
}
// 从响应解析 ETag
etag := c.extract_copy_result_etag(response.body)
// 获取复制后文件的大小
head_info := c.head(dst_bucket, dst_key) or {
return new_storage_result(dst_key, etag, 0)
}
return new_storage_result(dst_key, etag, head_info.size)
}
// 从复制结果 XML 中提取 ETag
fn (c TencentCOS) extract_copy_result_etag(body string) string {
if etag_start := body.index('<ETag>') {
if etag_end := body.index('</ETag>') {
return body[etag_start + 6..etag_end]
}
}
return ''
}
// ============================================================================
// StorageProvider 接口实现 - 列表操作
// ============================================================================
// 列出文件
pub fn (c TencentCOS) list(bucket string, options ListOptions) !ListResult {
mut query_params := map[string]string{}
if options.prefix != '' {
query_params['prefix'] = options.prefix
}
if options.delimiter != '' {
query_params['delimiter'] = options.delimiter
}
if options.max_keys > 0 {
query_params['max-keys'] = options.max_keys.str()
}
if options.start_after != '' {
query_params['marker'] = options.start_after
}
response := c.do_request(.get, bucket, '', query_params, map[string]string{}, []u8{})!
if response.status_code == 404 {
return error(new_bucket_not_found_error('tencent_cos', bucket).msg())
}
if response.status_code != 200 {
return error(c.parse_error_response(response, 'list').msg())
}
// 解析 XML 响应
return c.parse_list_objects_response(response.body)
}
// 解析 ListObjects 响应
fn (c TencentCOS) parse_list_objects_response(body string) !ListResult {
mut objects := []ObjectInfo{}
mut common_prefixes := []string{}
mut is_truncated := false
mut next_marker := ''
// 解析 IsTruncated
if truncated_start := body.index('<IsTruncated>') {
if truncated_end := body.index('</IsTruncated>') {
truncated_str := body[truncated_start + 13..truncated_end]
is_truncated = truncated_str == 'true'
}
}
// 解析 NextMarker
if marker_start := body.index('<NextMarker>') {
if marker_end := body.index('</NextMarker>') {
next_marker = body[marker_start + 12..marker_end]
}
}
// 解析 Contents
mut search_pos := 0
for {
content_start := body.index_after('<Contents>', search_pos) or { break }
content_end := body.index_after('</Contents>', content_start) or { break }
content_xml := body[content_start..content_end + 11]
obj := c.parse_object_from_xml(content_xml)
objects << obj
search_pos = content_end + 11
}
// 解析 CommonPrefixes
search_pos = 0
for {
prefix_start := body.index_after('<CommonPrefixes>', search_pos) or { break }
prefix_end := body.index_after('</CommonPrefixes>', prefix_start) or { break }
prefix_xml := body[prefix_start..prefix_end + 17]
if p_start := prefix_xml.index('<Prefix>') {
if p_end := prefix_xml.index('</Prefix>') {
common_prefixes << prefix_xml[p_start + 8..p_end]
}
}
search_pos = prefix_end + 17
}
return new_list_result(objects, common_prefixes, is_truncated, next_marker)
}
// 从 XML 解析单个对象信息
fn (c TencentCOS) parse_object_from_xml(xml string) ObjectInfo {
mut key := ''
mut size := i64(0)
mut etag := ''
mut last_modified := i64(0)
if key_start := xml.index('<Key>') {
if key_end := xml.index('</Key>') {
key = xml[key_start + 5..key_end]
}
}
if size_start := xml.index('<Size>') {
if size_end := xml.index('</Size>') {
size = xml[size_start + 6..size_end].i64()
}
}
if etag_start := xml.index('<ETag>') {
if etag_end := xml.index('</ETag>') {
etag = xml[etag_start + 6..etag_end]
}
}
if lm_start := xml.index('<LastModified>') {
if lm_end := xml.index('</LastModified>') {
lm_str := xml[lm_start + 14..lm_end]
last_modified = parse_cos_iso8601_date(lm_str)
}
}
content_type := infer_content_type(key)
return new_object_info(key, size, etag, content_type, last_modified)
}
// ============================================================================
// StorageProvider 接口实现 - Bucket 操作
// ============================================================================
// 创建 bucket
pub fn (c TencentCOS) create_bucket(bucket string) ! {
mut headers := map[string]string{}
headers['Content-Type'] = 'application/xml'
response := c.do_request(.put, bucket, '', map[string]string{}, headers, []u8{})!
// 200 或 409 (BucketAlreadyExists) 都算成功
if response.status_code != 200 && response.status_code != 409 {
return error(c.parse_error_response(response, 'create_bucket').msg())
}
}
// 删除 bucket
pub fn (c TencentCOS) delete_bucket(bucket string) ! {
response := c.do_request(.delete, bucket, '', map[string]string{}, map[string]string{}, []u8{})!
if response.status_code != 204 && response.status_code != 200 {
if response.status_code == 404 {
return error(new_bucket_not_found_error('tencent_cos', bucket).msg())
}
return error(c.parse_error_response(response, 'delete_bucket').msg())
}
}
// 检查 bucket 是否存在
pub fn (c TencentCOS) bucket_exists(bucket string) !bool {
response := c.do_request(.head, bucket, '', map[string]string{}, map[string]string{}, []u8{})!
if response.status_code == 200 {
return true
}
if response.status_code == 404 {
return false
}
return error(c.parse_error_response(response, 'bucket_exists').msg())
}
// 获取提供者名称
pub fn (c TencentCOS) provider_name() string {
return 'tencent_cos'
}
// ============================================================================
// StorageProvider 接口实现 - 分片上传
// ============================================================================
// 初始化分片上传
pub fn (mut c TencentCOS) init_multipart(bucket string, key string, content_type string) !string {
mut query_params := map[string]string{}
query_params['uploads'] = ''
mut headers := map[string]string{}
if content_type != '' {
headers['Content-Type'] = content_type
}
response := c.do_request(.post, bucket, key, query_params, headers, []u8{})!
if response.status_code != 200 {
return error(c.parse_error_response(response, 'init_multipart').msg())
}
// 从 XML 响应中提取 UploadId
upload_id := c.extract_upload_id(response.body)
if upload_id == '' {
return error(new_storage_error(.unknown, 'Failed to parse UploadId from response', 'tencent_cos', 'init_multipart').msg())
}
// 记录上传状态
c.multipart_uploads[upload_id] = COSMultipartUploadState{
bucket: bucket
key: key
upload_id: upload_id
content_type: content_type
parts: map[int]PartInfo{}
created_at: time.now().unix()
}
return upload_id
}
// 从 InitiateMultipartUploadResult XML 中提取 UploadId
fn (c TencentCOS) extract_upload_id(body string) string {
if id_start := body.index('<UploadId>') {
if id_end := body.index('</UploadId>') {
return body[id_start + 10..id_end]
}
}
return ''
}
// 上传分片
pub fn (mut c TencentCOS) upload_part(bucket string, key string, upload_id string, part_number int, data []u8) !string {
mut query_params := map[string]string{}
query_params['partNumber'] = part_number.str()
query_params['uploadId'] = upload_id
mut headers := map[string]string{}
headers['Content-Length'] = data.len.str()
response := c.do_request(.put, bucket, key, query_params, headers, data)!
if response.status_code != 200 {
return error(c.parse_error_response(response, 'upload_part').msg())
}
// 从响应头获取 ETag
etag := response.header.get_custom('ETag') or { c.calculate_etag(data) }
// 更新上传状态
if upload_id in c.multipart_uploads {
mut upload_state := c.multipart_uploads[upload_id]
upload_state.parts[part_number] = new_part_info(part_number, etag, i64(data.len))
c.multipart_uploads[upload_id] = upload_state
}
return etag
}
// 完成分片上传
pub fn (mut c TencentCOS) complete_multipart(bucket string, key string, upload_id string, parts []PartInfo) !StorageResult {
mut query_params := map[string]string{}
query_params['uploadId'] = upload_id
// 构建 CompleteMultipartUpload XML
mut xml_parts := '<CompleteMultipartUpload>'
for part in parts {
xml_parts += '<Part>'
xml_parts += '<PartNumber>${part.part_number}</PartNumber>'
xml_parts += '<ETag>${part.etag}</ETag>'
xml_parts += '</Part>'
}
xml_parts += '</CompleteMultipartUpload>'
mut headers := map[string]string{}
headers['Content-Type'] = 'application/xml'
payload := xml_parts.bytes()
response := c.do_request(.post, bucket, key, query_params, headers, payload)!
if response.status_code != 200 {
return error(c.parse_error_response(response, 'complete_multipart').msg())
}
// 从响应解析 ETag
etag := c.extract_complete_multipart_etag(response.body)
// 计算总大小
mut total_size := i64(0)
for part in parts {
total_size += part.size
}
// 清理上传状态
c.multipart_uploads.delete(upload_id)
return new_storage_result(key, etag, total_size)
}
// 从 CompleteMultipartUploadResult XML 中提取 ETag
fn (c TencentCOS) extract_complete_multipart_etag(body string) string {
if etag_start := body.index('<ETag>') {
if etag_end := body.index('</ETag>') {
return body[etag_start + 6..etag_end]
}
}
return ''
}
// 取消分片上传
pub fn (mut c TencentCOS) abort_multipart(bucket string, key string, upload_id string) ! {
mut query_params := map[string]string{}
query_params['uploadId'] = upload_id
response := c.do_request(.delete, bucket, key, query_params, map[string]string{}, []u8{})!
if response.status_code != 204 && response.status_code != 200 {
return error(c.parse_error_response(response, 'abort_multipart').msg())
}
// 清理上传状态
c.multipart_uploads.delete(upload_id)
}
// ============================================================================
// StorageProvider 接口实现 - 预签名 URL
// ============================================================================
// 生成预签名 URL
pub fn (c TencentCOS) presign_url(bucket string, key string, options PresignOptions) !string {
now := time.utc()
timestamp := now.unix()
// 计算签名有效期
start_time := timestamp - 60
end_time := timestamp + i64(options.expires_in)
key_time := '${start_time};${end_time}'
// 构建 URI
uri := c.get_uri_path(key)
// 构建待签名字符串
// HttpString = [HttpMethod]\n[HttpURI]\n[HttpParameters]\n[HttpHeaders]\n
http_string := '${options.method.to_lower()}\n${uri}\n\n\n'
// 计算 SHA1 哈希
request_hash := sha1.sum(http_string.bytes())
hashed_request := hex.encode(request_hash).to_lower()
// StringToSign = q-sign-algorithm\nq-sign-time\nSha1Digest\n
string_to_sign := 'sha1\n${key_time}\n${hashed_request}\n'
// 计算签名密钥
signing_key := cos_hmac_sha1(c.config.secret_key.bytes(), key_time.bytes())
// 计算签名
signature := cos_hmac_sha1(signing_key, string_to_sign.bytes())
signature_hex := hex.encode(signature).to_lower()
// 构建 URL
host := c.get_host(bucket)
encoded_key := c.encode_key(key)
mut url := 'https://${host}/${encoded_key}'
url += '?q-sign-algorithm=sha1'
url += '&q-ak=${urllib.query_escape(c.config.secret_id)}'
url += '&q-sign-time=${key_time}'
url += '&q-key-time=${key_time}'
url += '&q-header-list='
url += '&q-url-param-list='
url += '&q-signature=${signature_hex}'
return url
}
// ============================================================================
// 辅助函数
// ============================================================================
// 解析 COS HTTP 日期格式
fn parse_cos_http_date(date_str string) i64 {
if date_str == '' {
return 0
}
// HTTP 日期格式: "Mon, 02 Jan 2006 15:04:05 GMT"
// 简化处理,返回当前时间
return time.now().unix()
}
// 解析 COS ISO8601 日期格式
fn parse_cos_iso8601_date(date_str string) i64 {
if date_str == '' {
return 0
}
// ISO8601 格式: "2006-01-02T15:04:05.000Z"
// 简化处理,返回当前时间
return time.now().unix()
}