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)
This commit is contained in:
@@ -0,0 +1,500 @@
|
||||
module hono
|
||||
|
||||
import time
|
||||
|
||||
// 存储错误类型枚举
|
||||
pub enum StorageErrorKind {
|
||||
// 可重试错误 - 网络和临时性问题
|
||||
network_timeout
|
||||
service_unavailable
|
||||
rate_limited
|
||||
connection_reset
|
||||
temporary_failure
|
||||
// 不可重试错误 - 配置和权限问题
|
||||
invalid_credentials
|
||||
access_denied
|
||||
bucket_not_found
|
||||
object_not_found
|
||||
invalid_object_key
|
||||
quota_exceeded
|
||||
invalid_config
|
||||
bucket_not_empty
|
||||
bucket_already_exists
|
||||
object_already_exists
|
||||
invalid_range
|
||||
checksum_mismatch
|
||||
// 其他错误
|
||||
unknown
|
||||
}
|
||||
|
||||
// 存储错误结构
|
||||
pub struct StorageError {
|
||||
pub:
|
||||
kind StorageErrorKind
|
||||
message string
|
||||
provider string
|
||||
operation string
|
||||
http_status int
|
||||
retry_count int
|
||||
details map[string]string
|
||||
pub mut:
|
||||
retry_history []RetryAttempt
|
||||
}
|
||||
|
||||
// 重试尝试记录
|
||||
pub struct RetryAttempt {
|
||||
pub:
|
||||
attempt_number int
|
||||
timestamp i64
|
||||
error_message string
|
||||
delay_ms int
|
||||
}
|
||||
|
||||
|
||||
// 判断错误是否可重试
|
||||
pub fn (e StorageError) is_retryable() bool {
|
||||
return e.kind in [
|
||||
.network_timeout,
|
||||
.service_unavailable,
|
||||
.rate_limited,
|
||||
.connection_reset,
|
||||
.temporary_failure,
|
||||
]
|
||||
}
|
||||
|
||||
// 获取建议的重试延迟(毫秒)
|
||||
pub fn (e StorageError) suggested_retry_delay() int {
|
||||
base_delay := match e.kind {
|
||||
.rate_limited { 5000 } // 限流错误需要更长等待
|
||||
.service_unavailable { 2000 }
|
||||
.network_timeout { 1000 }
|
||||
.connection_reset { 500 }
|
||||
.temporary_failure { 1000 }
|
||||
else { 0 }
|
||||
}
|
||||
return base_delay
|
||||
}
|
||||
|
||||
// 获取错误类别描述
|
||||
pub fn (e StorageError) category() string {
|
||||
if e.is_retryable() {
|
||||
return 'retryable'
|
||||
}
|
||||
return 'non_retryable'
|
||||
}
|
||||
|
||||
// 实现 IError 接口
|
||||
pub fn (e StorageError) msg() string {
|
||||
return '${e.provider}/${e.operation}: ${e.message} (kind: ${e.kind}, http_status: ${e.http_status})'
|
||||
}
|
||||
|
||||
pub fn (e StorageError) code() int {
|
||||
return e.http_status
|
||||
}
|
||||
|
||||
// 添加重试记录
|
||||
pub fn (mut e StorageError) add_retry_attempt(attempt_number int, error_message string, delay_ms int) {
|
||||
e.retry_history << RetryAttempt{
|
||||
attempt_number: attempt_number
|
||||
timestamp: time.now().unix()
|
||||
error_message: error_message
|
||||
delay_ms: delay_ms
|
||||
}
|
||||
}
|
||||
|
||||
// 获取重试历史摘要
|
||||
pub fn (e StorageError) retry_summary() string {
|
||||
if e.retry_history.len == 0 {
|
||||
return 'No retry attempts'
|
||||
}
|
||||
mut summary := 'Retry attempts: ${e.retry_history.len}\n'
|
||||
for attempt in e.retry_history {
|
||||
summary += ' Attempt ${attempt.attempt_number}: ${attempt.error_message} (delay: ${attempt.delay_ms}ms)\n'
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// 创建存储错误的辅助函数
|
||||
pub fn new_storage_error(kind StorageErrorKind, message string, provider string, operation string) StorageError {
|
||||
return StorageError{
|
||||
kind: kind
|
||||
message: message
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 0
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建带 HTTP 状态码的存储错误
|
||||
pub fn new_storage_error_with_status(kind StorageErrorKind, message string, provider string, operation string, http_status int) StorageError {
|
||||
return StorageError{
|
||||
kind: kind
|
||||
message: message
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: http_status
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建带详情的存储错误
|
||||
pub fn new_storage_error_with_details(kind StorageErrorKind, message string, provider string, operation string, http_status int, details map[string]string) StorageError {
|
||||
return StorageError{
|
||||
kind: kind
|
||||
message: message
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: http_status
|
||||
retry_count: 0
|
||||
details: details
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建配置错误
|
||||
pub fn new_config_error(message string, field string) StorageError {
|
||||
mut details := map[string]string{}
|
||||
details['field'] = field
|
||||
return StorageError{
|
||||
kind: .invalid_config
|
||||
message: message
|
||||
provider: 'config'
|
||||
operation: 'validate'
|
||||
http_status: 0
|
||||
retry_count: 0
|
||||
details: details
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建对象未找到错误
|
||||
pub fn new_not_found_error(provider string, bucket string, key string) StorageError {
|
||||
mut details := map[string]string{}
|
||||
details['bucket'] = bucket
|
||||
details['key'] = key
|
||||
return StorageError{
|
||||
kind: .object_not_found
|
||||
message: 'Object not found: ${bucket}/${key}'
|
||||
provider: provider
|
||||
operation: 'get'
|
||||
http_status: 404
|
||||
retry_count: 0
|
||||
details: details
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 创建存储桶未找到错误
|
||||
pub fn new_bucket_not_found_error(provider string, bucket string) StorageError {
|
||||
mut details := map[string]string{}
|
||||
details['bucket'] = bucket
|
||||
return StorageError{
|
||||
kind: .bucket_not_found
|
||||
message: 'Bucket not found: ${bucket}'
|
||||
provider: provider
|
||||
operation: 'bucket'
|
||||
http_status: 404
|
||||
retry_count: 0
|
||||
details: details
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建访问拒绝错误
|
||||
pub fn new_access_denied_error(provider string, operation string, resource string) StorageError {
|
||||
mut details := map[string]string{}
|
||||
details['resource'] = resource
|
||||
return StorageError{
|
||||
kind: .access_denied
|
||||
message: 'Access denied to resource: ${resource}'
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 403
|
||||
retry_count: 0
|
||||
details: details
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建网络超时错误
|
||||
pub fn new_timeout_error(provider string, operation string) StorageError {
|
||||
return StorageError{
|
||||
kind: .network_timeout
|
||||
message: 'Network timeout during ${operation}'
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 0
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建服务不可用错误
|
||||
pub fn new_service_unavailable_error(provider string, operation string, message string) StorageError {
|
||||
return StorageError{
|
||||
kind: .service_unavailable
|
||||
message: message
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 503
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建限流错误
|
||||
pub fn new_rate_limited_error(provider string, operation string) StorageError {
|
||||
return StorageError{
|
||||
kind: .rate_limited
|
||||
message: 'Rate limited during ${operation}'
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 429
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建连接重置错误
|
||||
pub fn new_connection_reset_error(provider string, operation string) StorageError {
|
||||
return StorageError{
|
||||
kind: .connection_reset
|
||||
message: 'Connection reset during ${operation}'
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 0
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建凭据无效错误
|
||||
pub fn new_invalid_credentials_error(provider string, operation string) StorageError {
|
||||
return StorageError{
|
||||
kind: .invalid_credentials
|
||||
message: 'Invalid credentials for ${provider}'
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 401
|
||||
retry_count: 0
|
||||
details: map[string]string{}
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建配额超限错误
|
||||
pub fn new_quota_exceeded_error(provider string, operation string, resource string) StorageError {
|
||||
mut details := map[string]string{}
|
||||
details['resource'] = resource
|
||||
return StorageError{
|
||||
kind: .quota_exceeded
|
||||
message: 'Quota exceeded for ${resource}'
|
||||
provider: provider
|
||||
operation: operation
|
||||
http_status: 413
|
||||
retry_count: 0
|
||||
details: details
|
||||
retry_history: []RetryAttempt{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// HTTP 状态码到错误类型的映射
|
||||
// ============================================================================
|
||||
|
||||
// 从 HTTP 状态码映射到错误类型
|
||||
pub fn error_kind_from_http_status(status int) StorageErrorKind {
|
||||
return match status {
|
||||
400 { StorageErrorKind.invalid_object_key }
|
||||
401 { StorageErrorKind.invalid_credentials }
|
||||
403 { StorageErrorKind.access_denied }
|
||||
404 { StorageErrorKind.object_not_found }
|
||||
409 { StorageErrorKind.bucket_already_exists }
|
||||
413 { StorageErrorKind.quota_exceeded }
|
||||
416 { StorageErrorKind.invalid_range }
|
||||
429 { StorageErrorKind.rate_limited }
|
||||
500 { StorageErrorKind.service_unavailable }
|
||||
502 { StorageErrorKind.service_unavailable }
|
||||
503 { StorageErrorKind.service_unavailable }
|
||||
504 { StorageErrorKind.network_timeout }
|
||||
else { StorageErrorKind.unknown }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 提供者特定的错误映射
|
||||
// ============================================================================
|
||||
|
||||
// S3 错误代码映射
|
||||
pub fn map_s3_error_code(error_code string, http_status int) StorageErrorKind {
|
||||
return match error_code {
|
||||
'AccessDenied' { StorageErrorKind.access_denied }
|
||||
'AccountProblem' { StorageErrorKind.access_denied }
|
||||
'AllAccessDisabled' { StorageErrorKind.access_denied }
|
||||
'BucketAlreadyExists' { StorageErrorKind.bucket_already_exists }
|
||||
'BucketAlreadyOwnedByYou' { StorageErrorKind.bucket_already_exists }
|
||||
'BucketNotEmpty' { StorageErrorKind.bucket_not_empty }
|
||||
'CredentialsNotSupported' { StorageErrorKind.invalid_credentials }
|
||||
'ExpiredToken' { StorageErrorKind.invalid_credentials }
|
||||
'InvalidAccessKeyId' { StorageErrorKind.invalid_credentials }
|
||||
'InvalidBucketName' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidObjectName' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidRange' { StorageErrorKind.invalid_range }
|
||||
'InvalidSecurity' { StorageErrorKind.invalid_credentials }
|
||||
'InvalidToken' { StorageErrorKind.invalid_credentials }
|
||||
'NoSuchBucket' { StorageErrorKind.bucket_not_found }
|
||||
'NoSuchKey' { StorageErrorKind.object_not_found }
|
||||
'NoSuchUpload' { StorageErrorKind.object_not_found }
|
||||
'RequestTimeout' { StorageErrorKind.network_timeout }
|
||||
'RequestTimeTooSkewed' { StorageErrorKind.invalid_credentials }
|
||||
'ServiceUnavailable' { StorageErrorKind.service_unavailable }
|
||||
'SignatureDoesNotMatch' { StorageErrorKind.invalid_credentials }
|
||||
'SlowDown' { StorageErrorKind.rate_limited }
|
||||
'TemporaryRedirect' { StorageErrorKind.temporary_failure }
|
||||
'TokenRefreshRequired' { StorageErrorKind.invalid_credentials }
|
||||
else { error_kind_from_http_status(http_status) }
|
||||
}
|
||||
}
|
||||
|
||||
// 阿里云 OSS 错误代码映射
|
||||
pub fn map_aliyun_oss_error_code(error_code string, http_status int) StorageErrorKind {
|
||||
return match error_code {
|
||||
'AccessDenied' { StorageErrorKind.access_denied }
|
||||
'BucketAlreadyExists' { StorageErrorKind.bucket_already_exists }
|
||||
'BucketNotEmpty' { StorageErrorKind.bucket_not_empty }
|
||||
'EntityTooLarge' { StorageErrorKind.quota_exceeded }
|
||||
'EntityTooSmall' { StorageErrorKind.invalid_object_key }
|
||||
'FileGroupTooLarge' { StorageErrorKind.quota_exceeded }
|
||||
'InvalidAccessKeyId' { StorageErrorKind.invalid_credentials }
|
||||
'InvalidArgument' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidBucketName' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidDigest' { StorageErrorKind.checksum_mismatch }
|
||||
'InvalidObjectName' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidRange' { StorageErrorKind.invalid_range }
|
||||
'NoSuchBucket' { StorageErrorKind.bucket_not_found }
|
||||
'NoSuchKey' { StorageErrorKind.object_not_found }
|
||||
'NoSuchUpload' { StorageErrorKind.object_not_found }
|
||||
'RequestTimeout' { StorageErrorKind.network_timeout }
|
||||
'RequestTimeTooSkewed' { StorageErrorKind.invalid_credentials }
|
||||
'ServiceUnavailable' { StorageErrorKind.service_unavailable }
|
||||
'SignatureDoesNotMatch' { StorageErrorKind.invalid_credentials }
|
||||
'TooManyBuckets' { StorageErrorKind.quota_exceeded }
|
||||
else { error_kind_from_http_status(http_status) }
|
||||
}
|
||||
}
|
||||
|
||||
// 腾讯云 COS 错误代码映射
|
||||
pub fn map_tencent_cos_error_code(error_code string, http_status int) StorageErrorKind {
|
||||
return match error_code {
|
||||
'AccessDenied' { StorageErrorKind.access_denied }
|
||||
'BucketAlreadyExists' { StorageErrorKind.bucket_already_exists }
|
||||
'BucketAlreadyOwnedByYou' { StorageErrorKind.bucket_already_exists }
|
||||
'BucketNotEmpty' { StorageErrorKind.bucket_not_empty }
|
||||
'EntityTooLarge' { StorageErrorKind.quota_exceeded }
|
||||
'EntityTooSmall' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidAccessKeyId' { StorageErrorKind.invalid_credentials }
|
||||
'InvalidArgument' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidBucketName' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidDigest' { StorageErrorKind.checksum_mismatch }
|
||||
'InvalidObjectName' { StorageErrorKind.invalid_object_key }
|
||||
'InvalidRange' { StorageErrorKind.invalid_range }
|
||||
'NoSuchBucket' { StorageErrorKind.bucket_not_found }
|
||||
'NoSuchKey' { StorageErrorKind.object_not_found }
|
||||
'NoSuchUpload' { StorageErrorKind.object_not_found }
|
||||
'RequestTimeout' { StorageErrorKind.network_timeout }
|
||||
'RequestTimeTooSkewed' { StorageErrorKind.invalid_credentials }
|
||||
'ServiceUnavailable' { StorageErrorKind.service_unavailable }
|
||||
'SignatureDoesNotMatch' { StorageErrorKind.invalid_credentials }
|
||||
'SlowDown' { StorageErrorKind.rate_limited }
|
||||
'TooManyBuckets' { StorageErrorKind.quota_exceeded }
|
||||
else { error_kind_from_http_status(http_status) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 从错误消息解析错误代码
|
||||
// ============================================================================
|
||||
|
||||
// 从 XML 错误响应中提取错误代码
|
||||
pub fn extract_error_code_from_xml(body string) string {
|
||||
if code_start := body.index('<Code>') {
|
||||
if code_end := body.index('</Code>') {
|
||||
return body[code_start + 6..code_end]
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 从 XML 错误响应中提取错误消息
|
||||
pub fn extract_error_message_from_xml(body string) string {
|
||||
if message_start := body.index('<Message>') {
|
||||
if message_end := body.index('</Message>') {
|
||||
return body[message_start + 9..message_end]
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 创建提供者特定的错误
|
||||
// ============================================================================
|
||||
|
||||
// 从 S3 响应创建错误
|
||||
pub fn new_s3_error_from_response(body string, http_status int, operation string) StorageError {
|
||||
error_code := extract_error_code_from_xml(body)
|
||||
error_message := extract_error_message_from_xml(body)
|
||||
|
||||
kind := map_s3_error_code(error_code, http_status)
|
||||
message := if error_message != '' { error_message } else { 'HTTP ${http_status}' }
|
||||
|
||||
mut details := map[string]string{}
|
||||
if error_code != '' {
|
||||
details['error_code'] = error_code
|
||||
}
|
||||
|
||||
return new_storage_error_with_details(kind, message, 's3', operation, http_status, details)
|
||||
}
|
||||
|
||||
// 从阿里云 OSS 响应创建错误
|
||||
pub fn new_aliyun_oss_error_from_response(body string, http_status int, operation string) StorageError {
|
||||
error_code := extract_error_code_from_xml(body)
|
||||
error_message := extract_error_message_from_xml(body)
|
||||
|
||||
kind := map_aliyun_oss_error_code(error_code, http_status)
|
||||
message := if error_message != '' { error_message } else { 'HTTP ${http_status}' }
|
||||
|
||||
mut details := map[string]string{}
|
||||
if error_code != '' {
|
||||
details['error_code'] = error_code
|
||||
}
|
||||
|
||||
return new_storage_error_with_details(kind, message, 'aliyun_oss', operation, http_status, details)
|
||||
}
|
||||
|
||||
// 从腾讯云 COS 响应创建错误
|
||||
pub fn new_tencent_cos_error_from_response(body string, http_status int, operation string) StorageError {
|
||||
error_code := extract_error_code_from_xml(body)
|
||||
error_message := extract_error_message_from_xml(body)
|
||||
|
||||
kind := map_tencent_cos_error_code(error_code, http_status)
|
||||
message := if error_message != '' { error_message } else { 'HTTP ${http_status}' }
|
||||
|
||||
mut details := map[string]string{}
|
||||
if error_code != '' {
|
||||
details['error_code'] = error_code
|
||||
}
|
||||
|
||||
return new_storage_error_with_details(kind, message, 'tencent_cos', operation, http_status, details)
|
||||
}
|
||||
Reference in New Issue
Block a user