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:
+539
-123
@@ -4,18 +4,73 @@ import db.sqlite
|
||||
import time
|
||||
import crypto.rand
|
||||
|
||||
// 文件信息结构
|
||||
// 文件信息结构(扩展版本,支持多存储提供者)
|
||||
pub struct FileInfo {
|
||||
pub:
|
||||
id int
|
||||
file_uuid string
|
||||
file_hash string
|
||||
file_name string
|
||||
file_size int
|
||||
file_size i64
|
||||
file_type string
|
||||
created_at int
|
||||
updated_at int
|
||||
storage_type string // 存储类型 (local/s3/oss/cos)
|
||||
bucket string // 存储桶名称
|
||||
object_key string // 对象键(存储路径)
|
||||
created_at i64
|
||||
updated_at i64
|
||||
metadata string // JSON 格式的额外元数据
|
||||
}
|
||||
|
||||
// 分片上传状态
|
||||
pub struct MultipartUpload {
|
||||
pub:
|
||||
id int
|
||||
upload_id string
|
||||
file_uuid string
|
||||
file_name string
|
||||
file_size i64
|
||||
chunk_size int
|
||||
total_chunks int
|
||||
bucket string
|
||||
object_key string
|
||||
content_type string
|
||||
status string // 'uploading', 'completed', 'aborted'
|
||||
created_at i64
|
||||
updated_at i64
|
||||
}
|
||||
|
||||
// 已上传分片记录
|
||||
pub struct UploadedPart {
|
||||
pub:
|
||||
id int
|
||||
upload_id string
|
||||
part_number int
|
||||
etag string
|
||||
size i64
|
||||
uploaded_at i64
|
||||
}
|
||||
|
||||
// 文件列表查询选项
|
||||
pub struct FileListOptions {
|
||||
pub:
|
||||
bucket string
|
||||
prefix string
|
||||
storage_type string
|
||||
limit int = 100
|
||||
offset int
|
||||
order_by string = 'created_at'
|
||||
order_desc bool = true
|
||||
}
|
||||
|
||||
// 文件列表查询结果
|
||||
pub struct FileListResult {
|
||||
pub:
|
||||
files []FileInfo
|
||||
total_count int
|
||||
has_more bool
|
||||
}
|
||||
|
||||
|
||||
// 数据库管理器
|
||||
pub struct DatabaseManager {
|
||||
mut:
|
||||
@@ -25,10 +80,10 @@ mut:
|
||||
// 创建数据库管理器
|
||||
pub fn new_database_manager(db_path string) !DatabaseManager {
|
||||
mut db := sqlite.connect(db_path) or {
|
||||
return error('Failed to connect to database: $err')
|
||||
return error('Failed to connect to database: ${err}')
|
||||
}
|
||||
|
||||
// 创建文件信息表
|
||||
|
||||
// 创建文件信息表(扩展版本,支持多存储提供者)
|
||||
db.exec('CREATE TABLE IF NOT EXISTS file_info (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_uuid TEXT UNIQUE NOT NULL,
|
||||
@@ -36,18 +91,60 @@ pub fn new_database_manager(db_path string) !DatabaseManager {
|
||||
file_name TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
file_type TEXT NOT NULL,
|
||||
storage_type TEXT NOT NULL DEFAULT "local",
|
||||
bucket TEXT NOT NULL DEFAULT "default",
|
||||
object_key TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);') or {
|
||||
return error('Failed to create table: $err')
|
||||
}
|
||||
updated_at INTEGER NOT NULL,
|
||||
metadata TEXT
|
||||
);') or { return error('Failed to create file_info table: ${err}') }
|
||||
|
||||
// 创建索引
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_file_hash ON file_info(file_hash);') or {
|
||||
return error('Failed to create index: $err')
|
||||
return error('Failed to create idx_file_hash index: ${err}')
|
||||
}
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_file_uuid ON file_info(file_uuid);') or {
|
||||
return error('Failed to create index: $err')
|
||||
return error('Failed to create idx_file_uuid index: ${err}')
|
||||
}
|
||||
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_bucket_key ON file_info(bucket, object_key);') or {
|
||||
return error('Failed to create idx_bucket_key index: ${err}')
|
||||
}
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_storage_type ON file_info(storage_type);') or {
|
||||
return error('Failed to create idx_storage_type index: ${err}')
|
||||
}
|
||||
|
||||
// 创建分片上传状态表
|
||||
db.exec("CREATE TABLE IF NOT EXISTS multipart_uploads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
upload_id TEXT UNIQUE NOT NULL,
|
||||
file_uuid TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
chunk_size INTEGER NOT NULL,
|
||||
total_chunks INTEGER NOT NULL,
|
||||
bucket TEXT NOT NULL,
|
||||
object_key TEXT NOT NULL,
|
||||
content_type TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'uploading',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);") or { return error('Failed to create multipart_uploads table: ${err}') }
|
||||
|
||||
// 创建已上传分片记录表
|
||||
db.exec('CREATE TABLE IF NOT EXISTS uploaded_parts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
upload_id TEXT NOT NULL,
|
||||
part_number INTEGER NOT NULL,
|
||||
etag TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
uploaded_at INTEGER NOT NULL,
|
||||
UNIQUE(upload_id, part_number)
|
||||
);') or { return error('Failed to create uploaded_parts table: ${err}') }
|
||||
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_upload_id ON uploaded_parts(upload_id);') or {
|
||||
return error('Failed to create idx_upload_id index: ${err}')
|
||||
}
|
||||
|
||||
return DatabaseManager{
|
||||
db: db
|
||||
}
|
||||
@@ -57,156 +154,475 @@ pub fn new_database_manager(db_path string) !DatabaseManager {
|
||||
pub fn generate_file_uuid() string {
|
||||
// 生成16字节的随机数据
|
||||
random_bytes := rand.bytes(16) or { return '' }
|
||||
|
||||
|
||||
// 转换为UUID格式 (8-4-4-4-12)
|
||||
mut uuid := ''
|
||||
for i, byte in random_bytes {
|
||||
for i, b in random_bytes {
|
||||
if i == 4 || i == 6 || i == 8 || i == 10 {
|
||||
uuid += '-'
|
||||
}
|
||||
uuid += '${byte:02x}'
|
||||
uuid += '${b:02x}'
|
||||
}
|
||||
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
// 插入或更新文件信息
|
||||
pub fn (mut db DatabaseManager) insert_or_update_file(file_hash string, file_name string, file_size int, file_type string) !FileInfo {
|
||||
now := int(time.now().unix())
|
||||
|
||||
// 检查是否已存在相同的file_hash
|
||||
existing_file := db.get_file_by_hash(file_hash) or { FileInfo{} }
|
||||
|
||||
if existing_file.file_uuid != '' {
|
||||
// 如果存在相同的file_hash但文件名不同,创建新记录
|
||||
if existing_file.file_name != file_name {
|
||||
// 生成新的UUID
|
||||
file_uuid := generate_file_uuid()
|
||||
|
||||
// 插入新记录
|
||||
db.db.exec('INSERT INTO file_info (file_uuid, file_hash, file_name, file_size, file_type, created_at, updated_at) VALUES ("$file_uuid", "$file_hash", "$file_name", $file_size, "$file_type", $now, $now)') or {
|
||||
return error('Failed to insert file info: $err')
|
||||
}
|
||||
|
||||
return FileInfo{
|
||||
file_uuid: file_uuid
|
||||
file_hash: file_hash
|
||||
file_name: file_name
|
||||
file_size: file_size
|
||||
file_type: file_type
|
||||
created_at: now
|
||||
updated_at: now
|
||||
}
|
||||
} else {
|
||||
// 文件名相同,更新现有记录
|
||||
db.db.exec('UPDATE file_info SET file_size = $file_size, file_type = "$file_type", updated_at = $now WHERE file_hash = "$file_hash"') or {
|
||||
return error('Failed to update file info: $err')
|
||||
}
|
||||
|
||||
return FileInfo{
|
||||
file_uuid: existing_file.file_uuid
|
||||
file_hash: file_hash
|
||||
file_name: file_name
|
||||
file_size: file_size
|
||||
file_type: file_type
|
||||
created_at: existing_file.created_at
|
||||
updated_at: now
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不存在相同的file_hash,创建新记录
|
||||
file_uuid := generate_file_uuid()
|
||||
|
||||
db.db.exec('INSERT INTO file_info (file_uuid, file_hash, file_name, file_size, file_type, created_at, updated_at) VALUES ("$file_uuid", "$file_hash", "$file_name", $file_size, "$file_type", $now, $now)') or {
|
||||
return error('Failed to insert file info: $err')
|
||||
}
|
||||
|
||||
return FileInfo{
|
||||
file_uuid: file_uuid
|
||||
file_hash: file_hash
|
||||
file_name: file_name
|
||||
file_size: file_size
|
||||
file_type: file_type
|
||||
created_at: now
|
||||
updated_at: now
|
||||
}
|
||||
// 生成上传ID (用于数据库记录)
|
||||
pub fn generate_db_upload_id() string {
|
||||
random_bytes := rand.bytes(16) or { return '' }
|
||||
mut id := ''
|
||||
for b in random_bytes {
|
||||
id += '${b:02x}'
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
|
||||
// ============ 文件元数据 CRUD 操作 ============
|
||||
|
||||
// 插入文件信息
|
||||
pub fn (mut dm DatabaseManager) insert_file(file FileInfo) !FileInfo {
|
||||
now := time.now().unix()
|
||||
file_uuid := if file.file_uuid != '' { file.file_uuid } else { generate_file_uuid() }
|
||||
|
||||
dm.db.exec("INSERT INTO file_info (file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata) VALUES ('${file_uuid}', '${file.file_hash}', '${escape_sql(file.file_name)}', ${file.file_size}, '${file.file_type}', '${file.storage_type}', '${file.bucket}', '${escape_sql(file.object_key)}', ${now}, ${now}, '${escape_sql(file.metadata)}')") or {
|
||||
return error('Failed to insert file info: ${err}')
|
||||
}
|
||||
|
||||
return FileInfo{
|
||||
id: 0
|
||||
file_uuid: file_uuid
|
||||
file_hash: file.file_hash
|
||||
file_name: file.file_name
|
||||
file_size: file.file_size
|
||||
file_type: file.file_type
|
||||
storage_type: file.storage_type
|
||||
bucket: file.bucket
|
||||
object_key: file.object_key
|
||||
created_at: now
|
||||
updated_at: now
|
||||
metadata: file.metadata
|
||||
}
|
||||
}
|
||||
|
||||
// 根据文件hash获取文件信息
|
||||
pub fn (db DatabaseManager) get_file_by_hash(file_hash string) !FileInfo {
|
||||
rows := db.db.exec('SELECT file_uuid, file_hash, file_name, file_size, file_type, created_at, updated_at FROM file_info WHERE file_hash = "$file_hash"') or {
|
||||
return error('Failed to query file info: $err')
|
||||
// 根据UUID获取文件信息
|
||||
pub fn (dm DatabaseManager) get_file_by_uuid(file_uuid string) !FileInfo {
|
||||
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE file_uuid = '${file_uuid}'") or {
|
||||
return error('Failed to query file info: ${err}')
|
||||
}
|
||||
|
||||
|
||||
if rows.len > 0 {
|
||||
row := rows[0]
|
||||
return FileInfo{
|
||||
file_uuid: row.vals[0]
|
||||
file_hash: row.vals[1]
|
||||
file_name: row.vals[2]
|
||||
file_size: row.vals[3].int()
|
||||
file_type: row.vals[4]
|
||||
created_at: row.vals[5].int()
|
||||
updated_at: row.vals[6].int()
|
||||
id: row.vals[0].int()
|
||||
file_uuid: row.vals[1]
|
||||
file_hash: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
file_type: row.vals[5]
|
||||
storage_type: row.vals[6]
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
created_at: row.vals[9].i64()
|
||||
updated_at: row.vals[10].i64()
|
||||
metadata: row.vals[11]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return error('File not found')
|
||||
}
|
||||
|
||||
// 根据文件UUID获取文件信息
|
||||
pub fn (db DatabaseManager) get_file_by_uuid(file_uuid string) !FileInfo {
|
||||
rows := db.db.exec('SELECT file_uuid, file_hash, file_name, file_size, file_type, created_at, updated_at FROM file_info WHERE file_uuid = "$file_uuid"') or {
|
||||
return error('Failed to query file info: $err')
|
||||
// 根据hash获取文件信息
|
||||
pub fn (dm DatabaseManager) get_file_by_hash(file_hash string) !FileInfo {
|
||||
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE file_hash = '${file_hash}'") or {
|
||||
return error('Failed to query file info: ${err}')
|
||||
}
|
||||
|
||||
|
||||
if rows.len > 0 {
|
||||
row := rows[0]
|
||||
return FileInfo{
|
||||
file_uuid: row.vals[0]
|
||||
file_hash: row.vals[1]
|
||||
file_name: row.vals[2]
|
||||
file_size: row.vals[3].int()
|
||||
file_type: row.vals[4]
|
||||
created_at: row.vals[5].int()
|
||||
updated_at: row.vals[6].int()
|
||||
id: row.vals[0].int()
|
||||
file_uuid: row.vals[1]
|
||||
file_hash: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
file_type: row.vals[5]
|
||||
storage_type: row.vals[6]
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
created_at: row.vals[9].i64()
|
||||
updated_at: row.vals[10].i64()
|
||||
metadata: row.vals[11]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return error('File not found')
|
||||
}
|
||||
|
||||
// 获取所有文件信息
|
||||
pub fn (db DatabaseManager) get_all_files() ![]FileInfo {
|
||||
rows := db.db.exec('SELECT file_uuid, file_hash, file_name, file_size, file_type, created_at, updated_at FROM file_info ORDER BY created_at DESC') or {
|
||||
return error('Failed to query all files: $err')
|
||||
// 根据hash获取所有匹配的文件
|
||||
pub fn (dm DatabaseManager) get_files_by_hash(file_hash string) ![]FileInfo {
|
||||
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE file_hash = '${file_hash}'") or {
|
||||
return error('Failed to query files: ${err}')
|
||||
}
|
||||
|
||||
|
||||
mut files := []FileInfo{}
|
||||
for row in rows {
|
||||
files << FileInfo{
|
||||
file_uuid: row.vals[0]
|
||||
file_hash: row.vals[1]
|
||||
file_name: row.vals[2]
|
||||
file_size: row.vals[3].int()
|
||||
file_type: row.vals[4]
|
||||
created_at: row.vals[5].int()
|
||||
updated_at: row.vals[6].int()
|
||||
id: row.vals[0].int()
|
||||
file_uuid: row.vals[1]
|
||||
file_hash: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
file_type: row.vals[5]
|
||||
storage_type: row.vals[6]
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
created_at: row.vals[9].i64()
|
||||
updated_at: row.vals[10].i64()
|
||||
metadata: row.vals[11]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// 更新文件信息
|
||||
pub fn (mut dm DatabaseManager) update_file(file_uuid string, file_name string, file_type string, metadata string) !FileInfo {
|
||||
now := time.now().unix()
|
||||
|
||||
dm.db.exec("UPDATE file_info SET file_name = '${escape_sql(file_name)}', file_type = '${file_type}', metadata = '${escape_sql(metadata)}', updated_at = ${now} WHERE file_uuid = '${file_uuid}'") or {
|
||||
return error('Failed to update file info: ${err}')
|
||||
}
|
||||
|
||||
return dm.get_file_by_uuid(file_uuid)
|
||||
}
|
||||
|
||||
// 删除文件信息
|
||||
pub fn (mut db DatabaseManager) delete_file(file_uuid string) ! {
|
||||
db.db.exec('DELETE FROM file_info WHERE file_uuid = "$file_uuid"') or {
|
||||
return error('Failed to delete file info: $err')
|
||||
pub fn (mut dm DatabaseManager) delete_file(file_uuid string) ! {
|
||||
dm.db.exec("DELETE FROM file_info WHERE file_uuid = '${file_uuid}'") or {
|
||||
return error('Failed to delete file info: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
pub fn (dm DatabaseManager) file_exists(file_uuid string) bool {
|
||||
rows := dm.db.exec("SELECT 1 FROM file_info WHERE file_uuid = '${file_uuid}' LIMIT 1") or {
|
||||
return false
|
||||
}
|
||||
return rows.len > 0
|
||||
}
|
||||
|
||||
|
||||
// 分页列表查询
|
||||
pub fn (dm DatabaseManager) list_files(options FileListOptions) !FileListResult {
|
||||
mut where_clauses := []string{}
|
||||
|
||||
if options.bucket != '' {
|
||||
where_clauses << "bucket = '${options.bucket}'"
|
||||
}
|
||||
if options.prefix != '' {
|
||||
where_clauses << "object_key LIKE '${escape_sql(options.prefix)}%'"
|
||||
}
|
||||
if options.storage_type != '' {
|
||||
where_clauses << "storage_type = '${options.storage_type}'"
|
||||
}
|
||||
|
||||
where_sql := if where_clauses.len > 0 { 'WHERE ' + where_clauses.join(' AND ') } else { '' }
|
||||
order_dir := if options.order_desc { 'DESC' } else { 'ASC' }
|
||||
order_sql := 'ORDER BY ${options.order_by} ${order_dir}'
|
||||
|
||||
// 获取总数
|
||||
count_rows := dm.db.exec('SELECT COUNT(*) FROM file_info ${where_sql}') or {
|
||||
return error('Failed to count files: ${err}')
|
||||
}
|
||||
total_count := if count_rows.len > 0 { count_rows[0].vals[0].int() } else { 0 }
|
||||
|
||||
// 获取分页数据
|
||||
limit := if options.limit > 0 { options.limit } else { 100 }
|
||||
rows := dm.db.exec('SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info ${where_sql} ${order_sql} LIMIT ${limit} OFFSET ${options.offset}') or {
|
||||
return error('Failed to list files: ${err}')
|
||||
}
|
||||
|
||||
mut files := []FileInfo{}
|
||||
for row in rows {
|
||||
files << FileInfo{
|
||||
id: row.vals[0].int()
|
||||
file_uuid: row.vals[1]
|
||||
file_hash: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
file_type: row.vals[5]
|
||||
storage_type: row.vals[6]
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
created_at: row.vals[9].i64()
|
||||
updated_at: row.vals[10].i64()
|
||||
metadata: row.vals[11]
|
||||
}
|
||||
}
|
||||
|
||||
return FileListResult{
|
||||
files: files
|
||||
total_count: total_count
|
||||
has_more: options.offset + files.len < total_count
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有文件
|
||||
pub fn (dm DatabaseManager) get_all_files() ![]FileInfo {
|
||||
result := dm.list_files(FileListOptions{ limit: 10000 })!
|
||||
return result.files
|
||||
}
|
||||
|
||||
// ============ 分片上传状态管理 ============
|
||||
|
||||
// 创建分片上传记录
|
||||
pub fn (mut dm DatabaseManager) create_multipart_upload(upload MultipartUpload) !MultipartUpload {
|
||||
now := time.now().unix()
|
||||
upload_id := if upload.upload_id != '' { upload.upload_id } else { generate_db_upload_id() }
|
||||
file_uuid := if upload.file_uuid != '' { upload.file_uuid } else { generate_file_uuid() }
|
||||
|
||||
dm.db.exec("INSERT INTO multipart_uploads (upload_id, file_uuid, file_name, file_size, chunk_size, total_chunks, bucket, object_key, content_type, status, created_at, updated_at) VALUES ('${upload_id}', '${file_uuid}', '${escape_sql(upload.file_name)}', ${upload.file_size}, ${upload.chunk_size}, ${upload.total_chunks}, '${upload.bucket}', '${escape_sql(upload.object_key)}', '${upload.content_type}', 'uploading', ${now}, ${now})") or {
|
||||
return error('Failed to create multipart upload: ${err}')
|
||||
}
|
||||
|
||||
return MultipartUpload{
|
||||
id: 0
|
||||
upload_id: upload_id
|
||||
file_uuid: file_uuid
|
||||
file_name: upload.file_name
|
||||
file_size: upload.file_size
|
||||
chunk_size: upload.chunk_size
|
||||
total_chunks: upload.total_chunks
|
||||
bucket: upload.bucket
|
||||
object_key: upload.object_key
|
||||
content_type: upload.content_type
|
||||
status: 'uploading'
|
||||
created_at: now
|
||||
updated_at: now
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分片上传状态
|
||||
pub fn (dm DatabaseManager) get_multipart_upload(upload_id string) !MultipartUpload {
|
||||
rows := dm.db.exec("SELECT id, upload_id, file_uuid, file_name, file_size, chunk_size, total_chunks, bucket, object_key, content_type, status, created_at, updated_at FROM multipart_uploads WHERE upload_id = '${upload_id}'") or {
|
||||
return error('Failed to query multipart upload: ${err}')
|
||||
}
|
||||
|
||||
if rows.len > 0 {
|
||||
row := rows[0]
|
||||
return MultipartUpload{
|
||||
id: row.vals[0].int()
|
||||
upload_id: row.vals[1]
|
||||
file_uuid: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
chunk_size: row.vals[5].int()
|
||||
total_chunks: row.vals[6].int()
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
content_type: row.vals[9]
|
||||
status: row.vals[10]
|
||||
created_at: row.vals[11].i64()
|
||||
updated_at: row.vals[12].i64()
|
||||
}
|
||||
}
|
||||
|
||||
return error('Multipart upload not found')
|
||||
}
|
||||
|
||||
// 更新分片上传状态
|
||||
pub fn (mut dm DatabaseManager) update_multipart_status(upload_id string, status string) ! {
|
||||
now := time.now().unix()
|
||||
dm.db.exec("UPDATE multipart_uploads SET status = '${status}', updated_at = ${now} WHERE upload_id = '${upload_id}'") or {
|
||||
return error('Failed to update multipart status: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除分片上传记录
|
||||
pub fn (mut dm DatabaseManager) delete_multipart_upload(upload_id string) ! {
|
||||
// 先删除分片记录
|
||||
dm.db.exec("DELETE FROM uploaded_parts WHERE upload_id = '${upload_id}'") or {
|
||||
return error('Failed to delete uploaded parts: ${err}')
|
||||
}
|
||||
// 再删除上传记录
|
||||
dm.db.exec("DELETE FROM multipart_uploads WHERE upload_id = '${upload_id}'") or {
|
||||
return error('Failed to delete multipart upload: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// 列出所有进行中的分片上传
|
||||
pub fn (dm DatabaseManager) list_pending_multipart_uploads() ![]MultipartUpload {
|
||||
rows := dm.db.exec("SELECT id, upload_id, file_uuid, file_name, file_size, chunk_size, total_chunks, bucket, object_key, content_type, status, created_at, updated_at FROM multipart_uploads WHERE status = 'uploading' ORDER BY created_at DESC") or {
|
||||
return error('Failed to list pending uploads: ${err}')
|
||||
}
|
||||
|
||||
mut uploads := []MultipartUpload{}
|
||||
for row in rows {
|
||||
uploads << MultipartUpload{
|
||||
id: row.vals[0].int()
|
||||
upload_id: row.vals[1]
|
||||
file_uuid: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
chunk_size: row.vals[5].int()
|
||||
total_chunks: row.vals[6].int()
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
content_type: row.vals[9]
|
||||
status: row.vals[10]
|
||||
created_at: row.vals[11].i64()
|
||||
updated_at: row.vals[12].i64()
|
||||
}
|
||||
}
|
||||
|
||||
return uploads
|
||||
}
|
||||
|
||||
|
||||
// ============ 分片记录管理 ============
|
||||
|
||||
// 记录已上传的分片
|
||||
pub fn (mut dm DatabaseManager) record_uploaded_part(upload_id string, part_number int, etag string, size i64) !UploadedPart {
|
||||
now := time.now().unix()
|
||||
|
||||
// 使用 INSERT OR REPLACE 来处理重复上传的分片
|
||||
dm.db.exec("INSERT OR REPLACE INTO uploaded_parts (upload_id, part_number, etag, size, uploaded_at) VALUES ('${upload_id}', ${part_number}, '${etag}', ${size}, ${now})") or {
|
||||
return error('Failed to record uploaded part: ${err}')
|
||||
}
|
||||
|
||||
// 更新分片上传的更新时间
|
||||
dm.db.exec("UPDATE multipart_uploads SET updated_at = ${now} WHERE upload_id = '${upload_id}'") or {}
|
||||
|
||||
return UploadedPart{
|
||||
id: 0
|
||||
upload_id: upload_id
|
||||
part_number: part_number
|
||||
etag: etag
|
||||
size: size
|
||||
uploaded_at: now
|
||||
}
|
||||
}
|
||||
|
||||
// 获取已上传的分片列表
|
||||
pub fn (dm DatabaseManager) get_uploaded_parts(upload_id string) ![]UploadedPart {
|
||||
rows := dm.db.exec("SELECT id, upload_id, part_number, etag, size, uploaded_at FROM uploaded_parts WHERE upload_id = '${upload_id}' ORDER BY part_number ASC") or {
|
||||
return error('Failed to get uploaded parts: ${err}')
|
||||
}
|
||||
|
||||
mut parts := []UploadedPart{}
|
||||
for row in rows {
|
||||
parts << UploadedPart{
|
||||
id: row.vals[0].int()
|
||||
upload_id: row.vals[1]
|
||||
part_number: row.vals[2].int()
|
||||
etag: row.vals[3]
|
||||
size: row.vals[4].i64()
|
||||
uploaded_at: row.vals[5].i64()
|
||||
}
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
// 获取已上传分片数量
|
||||
pub fn (dm DatabaseManager) get_uploaded_parts_count(upload_id string) int {
|
||||
rows := dm.db.exec("SELECT COUNT(*) FROM uploaded_parts WHERE upload_id = '${upload_id}'") or {
|
||||
return 0
|
||||
}
|
||||
if rows.len > 0 {
|
||||
return rows[0].vals[0].int()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 检查分片是否已上传
|
||||
pub fn (dm DatabaseManager) is_part_uploaded(upload_id string, part_number int) bool {
|
||||
rows := dm.db.exec("SELECT 1 FROM uploaded_parts WHERE upload_id = '${upload_id}' AND part_number = ${part_number} LIMIT 1") or {
|
||||
return false
|
||||
}
|
||||
return rows.len > 0
|
||||
}
|
||||
|
||||
// 获取上传进度(已上传分片数 / 总分片数)
|
||||
pub fn (dm DatabaseManager) get_upload_progress(upload_id string) !(int, int) {
|
||||
upload := dm.get_multipart_upload(upload_id)!
|
||||
uploaded_count := dm.get_uploaded_parts_count(upload_id)
|
||||
return uploaded_count, upload.total_chunks
|
||||
}
|
||||
|
||||
// 删除分片记录
|
||||
pub fn (mut dm DatabaseManager) delete_uploaded_part(upload_id string, part_number int) ! {
|
||||
dm.db.exec("DELETE FROM uploaded_parts WHERE upload_id = '${upload_id}' AND part_number = ${part_number}") or {
|
||||
return error('Failed to delete uploaded part: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 辅助函数 ============
|
||||
|
||||
// SQL 字符串转义
|
||||
fn escape_sql(s string) string {
|
||||
return s.replace("'", "''")
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
pub fn (mut db DatabaseManager) close() {
|
||||
db.db.close() or { }
|
||||
}
|
||||
pub fn (mut dm DatabaseManager) close() {
|
||||
dm.db.close() or {}
|
||||
}
|
||||
|
||||
// ============ 向后兼容的接口 ============
|
||||
|
||||
// 插入或更新文件(兼容旧接口,支持扩展参数)
|
||||
pub fn (mut dm DatabaseManager) insert_or_update_file(file_hash string, file_name string, file_size i64, file_type string, storage_type string, bucket string, object_key string) !FileInfo {
|
||||
// 检查是否已存在相同的 file_hash 和 bucket/object_key
|
||||
existing := dm.get_file_by_hash(file_hash) or { FileInfo{} }
|
||||
|
||||
if existing.file_uuid != '' && existing.bucket == bucket && existing.object_key == object_key {
|
||||
// 更新现有记录
|
||||
return dm.update_file(existing.file_uuid, file_name, file_type, existing.metadata)
|
||||
}
|
||||
|
||||
// 创建新记录
|
||||
return dm.insert_file(FileInfo{
|
||||
file_hash: file_hash
|
||||
file_name: file_name
|
||||
file_size: file_size
|
||||
file_type: file_type
|
||||
storage_type: storage_type
|
||||
bucket: bucket
|
||||
object_key: object_key
|
||||
metadata: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 简化版插入或更新文件(完全向后兼容旧接口)
|
||||
pub fn (mut dm DatabaseManager) insert_or_update_file_simple(file_hash string, file_name string, file_size i64, file_type string) !FileInfo {
|
||||
return dm.insert_or_update_file(file_hash, file_name, file_size, file_type, 'local', 'default', file_name)
|
||||
}
|
||||
|
||||
// 根据 bucket 和 object_key 获取文件
|
||||
pub fn (dm DatabaseManager) get_file_by_key(bucket string, object_key string) !FileInfo {
|
||||
rows := dm.db.exec("SELECT id, file_uuid, file_hash, file_name, file_size, file_type, storage_type, bucket, object_key, created_at, updated_at, metadata FROM file_info WHERE bucket = '${bucket}' AND object_key = '${escape_sql(object_key)}'") or {
|
||||
return error('Failed to query file info: ${err}')
|
||||
}
|
||||
|
||||
if rows.len > 0 {
|
||||
row := rows[0]
|
||||
return FileInfo{
|
||||
id: row.vals[0].int()
|
||||
file_uuid: row.vals[1]
|
||||
file_hash: row.vals[2]
|
||||
file_name: row.vals[3]
|
||||
file_size: row.vals[4].i64()
|
||||
file_type: row.vals[5]
|
||||
storage_type: row.vals[6]
|
||||
bucket: row.vals[7]
|
||||
object_key: row.vals[8]
|
||||
created_at: row.vals[9].i64()
|
||||
updated_at: row.vals[10].i64()
|
||||
metadata: row.vals[11]
|
||||
}
|
||||
}
|
||||
|
||||
return error('File not found')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user