feat: add chunk file upload
This commit is contained in:
+212
@@ -0,0 +1,212 @@
|
||||
module hono
|
||||
|
||||
import db.sqlite
|
||||
import time
|
||||
import crypto.rand
|
||||
|
||||
// 文件信息结构
|
||||
pub struct FileInfo {
|
||||
pub:
|
||||
file_uuid string
|
||||
file_hash string
|
||||
file_name string
|
||||
file_size int
|
||||
file_type string
|
||||
created_at int
|
||||
updated_at int
|
||||
}
|
||||
|
||||
// 数据库管理器
|
||||
pub struct DatabaseManager {
|
||||
mut:
|
||||
db sqlite.DB
|
||||
}
|
||||
|
||||
// 创建数据库管理器
|
||||
pub fn new_database_manager(db_path string) !DatabaseManager {
|
||||
mut db := sqlite.connect(db_path) or {
|
||||
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,
|
||||
file_hash TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
file_type TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);') or {
|
||||
return error('Failed to create 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')
|
||||
}
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_file_uuid ON file_info(file_uuid);') or {
|
||||
return error('Failed to create index: $err')
|
||||
}
|
||||
|
||||
return DatabaseManager{
|
||||
db: db
|
||||
}
|
||||
}
|
||||
|
||||
// 生成文件UUID
|
||||
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 {
|
||||
if i == 4 || i == 6 || i == 8 || i == 10 {
|
||||
uuid += '-'
|
||||
}
|
||||
uuid += '${byte: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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据文件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')
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// 删除文件信息
|
||||
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 db DatabaseManager) close() {
|
||||
db.db.close() or { }
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
module hono
|
||||
|
||||
// 简单的 JSON 解析器
|
||||
pub struct JsonParser {
|
||||
pub:
|
||||
data string
|
||||
}
|
||||
|
||||
// 创建 JSON 解析器
|
||||
pub fn new_json_parser(data string) JsonParser {
|
||||
return JsonParser{data: data}
|
||||
}
|
||||
|
||||
// 解析 JSON 对象
|
||||
pub fn (parser JsonParser) parse_object() !map[string]string {
|
||||
mut result := map[string]string{}
|
||||
|
||||
// 移除外层的大括号
|
||||
data := parser.data.trim_space()
|
||||
if !data.starts_with('{') || !data.ends_with('}') {
|
||||
return error('Invalid JSON object format')
|
||||
}
|
||||
|
||||
content := data[1..data.len - 1]
|
||||
|
||||
// 分割键值对
|
||||
pairs := split_json_pairs(content)
|
||||
|
||||
for pair in pairs {
|
||||
key_value := split_key_value(pair) or { continue }
|
||||
result[key_value.key] = key_value.value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 键值对结构
|
||||
struct KeyValue {
|
||||
pub:
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
// 分割 JSON 键值对
|
||||
fn split_json_pairs(content string) []string {
|
||||
mut pairs := []string{}
|
||||
mut current := ''
|
||||
mut brace_count := 0
|
||||
mut in_string := false
|
||||
mut escape_next := false
|
||||
|
||||
for i := 0; i < content.len; i++ {
|
||||
ch := content[i]
|
||||
|
||||
if escape_next {
|
||||
current += ch.str()
|
||||
escape_next = false
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == `\\` {
|
||||
escape_next = true
|
||||
current += ch.str()
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == `"` && !escape_next {
|
||||
in_string = !in_string
|
||||
current += ch.str()
|
||||
continue
|
||||
}
|
||||
|
||||
if !in_string {
|
||||
if ch == `{` {
|
||||
brace_count++
|
||||
} else if ch == `}` {
|
||||
brace_count--
|
||||
} else if ch == `,` && brace_count == 0 {
|
||||
pairs << current.trim_space()
|
||||
current = ''
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
current += ch.str()
|
||||
}
|
||||
|
||||
if current.trim_space() != '' {
|
||||
pairs << current.trim_space()
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
// 分割键值对
|
||||
fn split_key_value(pair string) !KeyValue {
|
||||
colon_index := pair.index(':') or {
|
||||
return error('No colon found')
|
||||
}
|
||||
mut key := pair[..colon_index].trim_space()
|
||||
mut value := pair[colon_index + 1..].trim_space()
|
||||
|
||||
// 移除键的引号
|
||||
if key.starts_with('"') && key.ends_with('"') {
|
||||
key = key[1..key.len - 1]
|
||||
}
|
||||
|
||||
// 移除值的引号
|
||||
if value.starts_with('"') && value.ends_with('"') {
|
||||
value = value[1..value.len - 1]
|
||||
}
|
||||
|
||||
return KeyValue{key: key, value: value}
|
||||
}
|
||||
|
||||
pub struct MergeRequest {
|
||||
pub:
|
||||
file_hash string
|
||||
filename string
|
||||
total_chunks int
|
||||
}
|
||||
|
||||
// 解析合并请求
|
||||
pub fn parse_merge_request(body string) !MergeRequest {
|
||||
if body.len == 0 {
|
||||
return error('Empty request body')
|
||||
}
|
||||
|
||||
parser := new_json_parser(body)
|
||||
obj := parser.parse_object() or {
|
||||
return error('Failed to parse JSON')
|
||||
}
|
||||
|
||||
file_hash := obj['file_hash'] or {
|
||||
return error('Missing file_hash')
|
||||
}
|
||||
filename := obj['filename'] or {
|
||||
return error('Missing filename')
|
||||
}
|
||||
total_chunks_str := obj['total_chunks'] or {
|
||||
return error('Missing total_chunks')
|
||||
}
|
||||
|
||||
if !total_chunks_str.is_int() {
|
||||
return error('Invalid total_chunks')
|
||||
}
|
||||
total_chunks := total_chunks_str.int()
|
||||
|
||||
return MergeRequest{
|
||||
file_hash: file_hash
|
||||
filename: filename
|
||||
total_chunks: total_chunks
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
module hono
|
||||
|
||||
import net.http
|
||||
|
||||
// Multipart 表单数据项
|
||||
pub struct MultipartItem {
|
||||
pub:
|
||||
name string
|
||||
filename string
|
||||
content string
|
||||
content_type string
|
||||
}
|
||||
|
||||
// Multipart 解析器
|
||||
pub struct MultipartParser {
|
||||
pub:
|
||||
boundary string
|
||||
data string
|
||||
}
|
||||
|
||||
// 创建 Multipart 解析器
|
||||
pub fn new_multipart_parser(content_type string, data string) !MultipartParser {
|
||||
// 从 Content-Type 中提取 boundary
|
||||
boundary := extract_boundary(content_type) or {
|
||||
return error('Failed to extract boundary')
|
||||
}
|
||||
|
||||
return MultipartParser{
|
||||
boundary: boundary
|
||||
data: data
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 multipart 数据
|
||||
pub fn (parser MultipartParser) parse() ![]MultipartItem {
|
||||
mut items := []MultipartItem{}
|
||||
// 更健壮的分割方式,兼容不同换行
|
||||
parts := parser.data.split('--${parser.boundary}')
|
||||
for _, part in parts {
|
||||
if part.trim_space() == '' || part.trim_space() == '--' || part.trim_space().starts_with('--') {
|
||||
continue
|
||||
}
|
||||
item := parser.parse_part(part) or {
|
||||
continue
|
||||
}
|
||||
items << item
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// 解析单个部分
|
||||
fn (parser MultipartParser) parse_part(part string) !MultipartItem {
|
||||
// 分离头部和内容
|
||||
header_content := part.split('\r\n\r\n')
|
||||
if header_content.len < 2 {
|
||||
return error('Invalid part format')
|
||||
}
|
||||
|
||||
header := header_content[0]
|
||||
content := header_content[1..].join('\r\n\r\n')
|
||||
|
||||
// 解析头部
|
||||
name := extract_header_value(header, 'name') or {
|
||||
return error('Missing name')
|
||||
}
|
||||
filename := extract_header_value(header, 'filename') or { '' }
|
||||
content_type := extract_header_value(header, 'Content-Type') or { 'text/plain' }
|
||||
|
||||
return MultipartItem{
|
||||
name: name
|
||||
filename: filename
|
||||
content: content
|
||||
content_type: content_type
|
||||
}
|
||||
}
|
||||
|
||||
// 从 Content-Type 中提取 boundary
|
||||
fn extract_boundary(content_type string) !string {
|
||||
if !content_type.starts_with('multipart/form-data') {
|
||||
return error('Not multipart/form-data')
|
||||
}
|
||||
|
||||
boundary_start := content_type.index('boundary=') or {
|
||||
return error('No boundary found')
|
||||
}
|
||||
mut boundary := content_type[boundary_start + 9..]
|
||||
|
||||
// 移除引号
|
||||
if boundary.starts_with('"') && boundary.ends_with('"') {
|
||||
boundary = boundary[1..boundary.len - 1]
|
||||
}
|
||||
|
||||
return boundary
|
||||
}
|
||||
|
||||
// 从头部中提取值
|
||||
fn extract_header_value(header string, key string) !string {
|
||||
// 先找 Content-Disposition 行
|
||||
for line in header.split('\r\n') {
|
||||
if line.starts_with('Content-Disposition:') {
|
||||
// 查找 key="value"
|
||||
key_eq := '${key}="'
|
||||
idx := line.index(key_eq) or { continue }
|
||||
start := idx + key_eq.len
|
||||
end := line.index_after('"', start) or { line.len }
|
||||
return line[start..end]
|
||||
}
|
||||
}
|
||||
return error('Key not found: $key')
|
||||
}
|
||||
|
||||
// 解析 multipart 表单数据(便捷函数)
|
||||
pub fn parse_multipart_form(req http.Request) !map[string]MultipartItem {
|
||||
content_type := req.header.get_custom('Content-Type') or {
|
||||
return error('No Content-Type header')
|
||||
}
|
||||
|
||||
parser := new_multipart_parser(content_type, req.data) or {
|
||||
return error('Failed to create parser: $err')
|
||||
}
|
||||
|
||||
items := parser.parse() or {
|
||||
return error('Failed to parse multipart data: $err')
|
||||
}
|
||||
|
||||
// 转换为 map
|
||||
mut result := map[string]MultipartItem{}
|
||||
for item in items {
|
||||
result[item.name] = item
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取文件数据
|
||||
pub fn (items map[string]MultipartItem) get_file(key string) !string {
|
||||
item := items[key] or {
|
||||
return error('File not found: $key')
|
||||
}
|
||||
return item.content
|
||||
}
|
||||
|
||||
// 获取表单字段值
|
||||
pub fn (items map[string]MultipartItem) get(key string) !string {
|
||||
item := items[key] or {
|
||||
return error('Field not found: $key')
|
||||
}
|
||||
return item.content
|
||||
}
|
||||
|
||||
// 检查是否为文件
|
||||
pub fn (items map[string]MultipartItem) is_file(key string) bool {
|
||||
item := items[key] or { return false }
|
||||
return item.filename != ''
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
pub fn (items map[string]MultipartItem) get_filename(key string) !string {
|
||||
item := items[key] or {
|
||||
return error('Item not found: $key')
|
||||
}
|
||||
return item.filename
|
||||
}
|
||||
|
||||
// 获取内容类型
|
||||
pub fn (items map[string]MultipartItem) get_content_type(key string) !string {
|
||||
item := items[key] or {
|
||||
return error('Item not found: $key')
|
||||
}
|
||||
return item.content_type
|
||||
}
|
||||
+4
-4
@@ -187,12 +187,12 @@ pub fn (mut c Context) file_with_options(file_path string, options FileOptions)
|
||||
// FileOptions struct for configuring file serving
|
||||
pub struct FileOptions {
|
||||
pub:
|
||||
status_code int = 0 // 自定义状态码,0表示使用默认200
|
||||
content_type string = '' // 自定义Content-Type
|
||||
status_code int // 自定义状态码,0表示使用默认200
|
||||
content_type string // 自定义Content-Type
|
||||
last_modified bool = true // 是否设置Last-Modified头
|
||||
etag bool = true // 是否设置ETag头
|
||||
max_age int = 0 // 缓存时间(秒)
|
||||
no_cache bool = false // 是否禁用缓存
|
||||
max_age int // 缓存时间(秒)
|
||||
no_cache bool // 是否禁用缓存
|
||||
headers map[string]string // 自定义响应头
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
module hono
|
||||
|
||||
import regex
|
||||
import time
|
||||
|
||||
// 路由节点类型
|
||||
enum RouteType {
|
||||
@@ -142,7 +141,7 @@ fn (mut router ContextHybridRouter) match_dynamic_route(method string, path stri
|
||||
}
|
||||
|
||||
for handler in router.dynamic_routes {
|
||||
match_result, replaced_path_reg, param_names := router.match_path_with_regex(path, handler.path)
|
||||
match_result, replaced_path_reg, _ := router.match_path_with_regex(path, handler.path)
|
||||
if match_result {
|
||||
mut param_map := map[string]string{}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ module hono
|
||||
|
||||
import os
|
||||
import net.http
|
||||
import net.urllib
|
||||
|
||||
// 静态文件服务配置
|
||||
pub struct StaticOptions {
|
||||
|
||||
+4
-4
@@ -52,7 +52,7 @@ pub fn (mut tr ContextTrieRouter) add_route(method string, path string, handler
|
||||
}
|
||||
|
||||
segments := path.split('/').filter(it != '')
|
||||
mut current := tr.method_trees[method]
|
||||
mut current := tr.method_trees[method] or { return }
|
||||
|
||||
for i, seg in segments {
|
||||
if seg.starts_with(':') {
|
||||
@@ -72,7 +72,7 @@ pub fn (mut tr ContextTrieRouter) add_route(method string, path string, handler
|
||||
if seg !in current.children {
|
||||
current.children[seg] = ContextTrieNode.new(seg, TrieNodeType.static, '')
|
||||
}
|
||||
current = current.children[seg]
|
||||
current = current.children[seg] or { return }
|
||||
}
|
||||
|
||||
// 在最后一个节点设置处理器
|
||||
@@ -94,13 +94,13 @@ pub fn (mut tr ContextTrieRouter) match_route(method string, path string) ?Conte
|
||||
}
|
||||
|
||||
segments := path.split('/').filter(it != '')
|
||||
mut current := tr.method_trees[method]
|
||||
mut current := tr.method_trees[method] or { return none }
|
||||
mut params := map[string]string{}
|
||||
|
||||
for seg in segments {
|
||||
// 先尝试静态匹配
|
||||
if seg in current.children {
|
||||
current = current.children[seg]
|
||||
current = current.children[seg] or { return none }
|
||||
} else if current.param_child != unsafe { nil } {
|
||||
// 参数匹配
|
||||
params[current.param_child.param_name] = seg
|
||||
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
module hono
|
||||
|
||||
import net.http
|
||||
import os
|
||||
import crypto.md5
|
||||
import json
|
||||
import time
|
||||
|
||||
// 分片上传配置
|
||||
pub struct ChunkUploadConfig {
|
||||
pub:
|
||||
chunk_size int = 1024 * 1024 // 1MB 默认分片大小
|
||||
max_file_size int = 1024 * 1024 * 1024 // 1GB 最大文件大小
|
||||
temp_dir string = './uploads/chunks' // 临时分片目录(分片保存在 temp_dir/filehash/chunksize/ 下)
|
||||
upload_dir string = './uploads/files' // 最终文件目录
|
||||
cleanup_delay int = 3600 // 1小时后清理临时文件
|
||||
clear_chunks_on_complete bool // 上传完成后是否清空分片,默认不清空
|
||||
db_path string = './uploads/files.db' // 数据库文件路径
|
||||
}
|
||||
|
||||
// 分片信息
|
||||
pub struct ChunkInfo {
|
||||
pub:
|
||||
file_hash string
|
||||
chunk_index int
|
||||
total_chunks int
|
||||
filename string
|
||||
file_size int
|
||||
chunk_size int
|
||||
upload_time int
|
||||
}
|
||||
|
||||
// 文件上传状态
|
||||
pub struct FileUploadStatus {
|
||||
pub:
|
||||
file_hash string
|
||||
filename string
|
||||
total_chunks int
|
||||
file_size int
|
||||
chunk_size int
|
||||
created_at int
|
||||
pub mut:
|
||||
uploaded_chunks []int
|
||||
status string
|
||||
updated_at int
|
||||
}
|
||||
|
||||
// 分片上传管理器
|
||||
pub struct ChunkUploadManager {
|
||||
pub mut:
|
||||
config ChunkUploadConfig
|
||||
uploads map[string]FileUploadStatus
|
||||
db DatabaseManager
|
||||
}
|
||||
|
||||
// 创建分片上传管理器
|
||||
pub fn new_chunk_upload_manager(config ChunkUploadConfig) ChunkUploadManager {
|
||||
// 确保目录存在
|
||||
os.mkdir_all(config.temp_dir) or { panic('Failed to create temp directory') }
|
||||
os.mkdir_all(config.upload_dir) or { panic('Failed to create upload directory') }
|
||||
|
||||
// 创建数据库管理器
|
||||
db := new_database_manager(config.db_path) or { panic('Failed to create database manager: $err') }
|
||||
|
||||
return ChunkUploadManager{
|
||||
config: config
|
||||
uploads: map[string]FileUploadStatus{}
|
||||
db: db
|
||||
}
|
||||
}
|
||||
|
||||
// 处理分片上传
|
||||
pub fn (mut manager ChunkUploadManager) handle_chunk_upload(mut ctx Context) http.Response {
|
||||
// 解析 multipart 表单数据
|
||||
form_data := hono.parse_multipart_form(ctx.req) or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Invalid form data", "message": "$err"}')
|
||||
}
|
||||
|
||||
// 获取必要参数
|
||||
file_hash := form_data.get('file_hash') or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing file_hash"}')
|
||||
}
|
||||
|
||||
chunk_index_str := form_data.get('chunk_index') or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing chunk_index"}')
|
||||
}
|
||||
|
||||
filename := form_data.get('filename') or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing filename"}')
|
||||
}
|
||||
|
||||
file_size_str := form_data.get('file_size') or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing file_size"}')
|
||||
}
|
||||
|
||||
// 解析数值
|
||||
chunk_index := chunk_index_str.int()
|
||||
file_size := file_size_str.int()
|
||||
|
||||
// 验证文件大小
|
||||
if file_size > manager.config.max_file_size {
|
||||
ctx.status(413)
|
||||
return ctx.json('{"error": "File too large", "max_size": "${manager.config.max_file_size}"}')
|
||||
}
|
||||
|
||||
// 获取前端传递的分片大小参数
|
||||
chunk_size_str := form_data.get('chunk_size') or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing chunk_size"}')
|
||||
}
|
||||
chunk_size := chunk_size_str.int()
|
||||
|
||||
// 验证分片大小(允许更大的分片大小,但不超过10MB)
|
||||
max_allowed_chunk_size := 10 * 1024 * 1024 // 10MB
|
||||
if chunk_size > max_allowed_chunk_size {
|
||||
ctx.status(413)
|
||||
return ctx.json('{"error": "Chunk size too large", "max_chunk_size": "$max_allowed_chunk_size"}')
|
||||
}
|
||||
|
||||
// 获取文件数据
|
||||
file_data := form_data.get_file('chunk') or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing chunk file"}')
|
||||
}
|
||||
|
||||
// 创建按文件hash和分片大小分组的目录
|
||||
chunk_dir := os.join_path(manager.config.temp_dir, file_hash.trim_space(), chunk_size.str())
|
||||
os.mkdir_all(chunk_dir) or {
|
||||
ctx.status(500)
|
||||
return ctx.json('{"error": "Failed to create chunk directory"}')
|
||||
}
|
||||
|
||||
// 保存分片文件到hash/chunksize子目录
|
||||
chunk_path := os.join_path(chunk_dir, 'chunk_${chunk_index}.part')
|
||||
println('[DEBUG] Saving chunk to: $chunk_path')
|
||||
println('[DEBUG] Chunk dir exists: ${os.exists(chunk_dir)}')
|
||||
println('[DEBUG] Chunk data size: ${file_data.len} bytes')
|
||||
|
||||
os.write_file(chunk_path, file_data) or {
|
||||
println('[DEBUG] Failed to save chunk: $err')
|
||||
ctx.status(500)
|
||||
return ctx.json('{"error": "Failed to save chunk"}')
|
||||
}
|
||||
|
||||
// 更新上传状态
|
||||
manager.update_upload_status(file_hash, filename, chunk_index, file_size, chunk_size)
|
||||
|
||||
println('[DEBUG] Updated upload status for file: $file_hash, chunk: $chunk_index')
|
||||
|
||||
// 判断是否所有分片都已上传,自动合并
|
||||
// 基于分片大小总和来判断是否合并
|
||||
mut all_chunk_uploaded := false
|
||||
merge_chunk_dir := os.join_path(manager.config.temp_dir, file_hash.trim_space(), chunk_size.str())
|
||||
|
||||
if os.exists(merge_chunk_dir) {
|
||||
mut total_chunk_size := u64(0)
|
||||
mut chunk_count := 0
|
||||
mut max_chunk_index := -1
|
||||
|
||||
// 遍历所有分片文件,计算总大小和找到最大分片索引
|
||||
for i := 0; ; i++ {
|
||||
merge_chunk_path := os.join_path(merge_chunk_dir, 'chunk_${i}.part')
|
||||
if !os.exists(merge_chunk_path) {
|
||||
break
|
||||
}
|
||||
chunk_info := os.stat(merge_chunk_path) or { continue }
|
||||
total_chunk_size += chunk_info.size
|
||||
chunk_count++
|
||||
max_chunk_index = i
|
||||
}
|
||||
|
||||
println('[DEBUG] [MergeCheck] total_chunk_size=$total_chunk_size, file_size=$file_size, chunk_count=$chunk_count, max_chunk_index=$max_chunk_index')
|
||||
|
||||
// 如果分片文件大小总和 >= file_size,认为可以合并
|
||||
if total_chunk_size >= u64(file_size) {
|
||||
all_chunk_uploaded = true
|
||||
println('[DEBUG] All chunks uploaded based on size comparison')
|
||||
}
|
||||
}
|
||||
|
||||
println('[DEBUG] All chunks uploaded: $all_chunk_uploaded')
|
||||
|
||||
if all_chunk_uploaded {
|
||||
// 重新计算分片数量用于合并
|
||||
mut actual_total_chunks := 0
|
||||
for i := 0; ; i++ {
|
||||
check_chunk_path := os.join_path(merge_chunk_dir, 'chunk_${i}.part')
|
||||
if !os.exists(check_chunk_path) {
|
||||
break
|
||||
}
|
||||
actual_total_chunks++
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
file_ext := get_file_extension(filename)
|
||||
final_filename := '${file_hash.trim_space()}${file_ext}'
|
||||
final_path := os.join_path(manager.config.upload_dir, final_filename)
|
||||
|
||||
// 检查最终文件是否已经存在,避免重复合并
|
||||
if os.exists(final_path) {
|
||||
println('[DEBUG] Final file already exists: $final_path, skipping merge')
|
||||
} else {
|
||||
println('[DEBUG] Merging chunks to: $final_path')
|
||||
println('[DEBUG] Upload dir exists: ${os.exists(manager.config.upload_dir)}')
|
||||
println('[DEBUG] Actual total chunks: $actual_total_chunks')
|
||||
|
||||
manager.merge_chunks(file_hash, actual_total_chunks, final_path, chunk_size) or {
|
||||
println('[DEBUG] Merge failed: $err')
|
||||
ctx.status(500)
|
||||
return ctx.json('{"error": "Failed to merge chunks", "message": "$err"}')
|
||||
}
|
||||
}
|
||||
|
||||
// 在数据库中记录文件信息
|
||||
file_info := manager.db.insert_or_update_file(file_hash, filename, file_size, file_ext) or {
|
||||
println('[DEBUG] Failed to save file info to database: $err')
|
||||
// 即使数据库保存失败,也不影响文件合并
|
||||
FileInfo{}
|
||||
}
|
||||
|
||||
manager.uploads[file_hash].status = 'completed'
|
||||
manager.uploads[file_hash].updated_at = int(time.now().unix())
|
||||
|
||||
if manager.config.clear_chunks_on_complete {
|
||||
manager.cleanup_chunks(file_hash, chunk_size)
|
||||
}
|
||||
|
||||
clean_file_path := final_path.replace('\n', '').replace('\r', '').replace('\\', '\\\\').trim_space()
|
||||
return ctx.json('{"success": true, "all_chunk_uploaded": true, "file_path": "$clean_file_path", "file_uuid": "${file_info.file_uuid}", "message": "File merged successfully"}')
|
||||
}
|
||||
// 未全部上传,正常返回
|
||||
return ctx.json('{"success": true, "chunk_index": $chunk_index, "all_chunk_uploaded": false, "message": "Chunk uploaded successfully"}')
|
||||
}
|
||||
|
||||
// 处理分片合并
|
||||
pub fn (mut manager ChunkUploadManager) handle_chunk_merge(mut ctx Context) http.Response {
|
||||
// 解析请求体
|
||||
merge_request := hono.parse_merge_request(ctx.body) or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Invalid request body", "message": "$err"}')
|
||||
}
|
||||
|
||||
file_hash := merge_request.file_hash
|
||||
filename := merge_request.filename
|
||||
total_chunks := merge_request.total_chunks
|
||||
|
||||
// 检查上传状态
|
||||
upload_status := manager.uploads[file_hash] or {
|
||||
ctx.status(404)
|
||||
return ctx.json('{"error": "Upload not found"}')
|
||||
}
|
||||
|
||||
// 验证所有分片是否上传完成
|
||||
if upload_status.uploaded_chunks.len != total_chunks {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Not all chunks uploaded", "uploaded": ${upload_status.uploaded_chunks.len}, "total": $total_chunks}')
|
||||
}
|
||||
|
||||
// 合并文件
|
||||
file_ext := get_file_extension(filename)
|
||||
final_filename := '${file_hash.trim_space()}${file_ext}'
|
||||
final_path := os.join_path(manager.config.upload_dir, final_filename)
|
||||
|
||||
// 从上传状态中获取分片大小
|
||||
chunk_size := upload_status.chunk_size
|
||||
manager.merge_chunks(file_hash, total_chunks, final_path, chunk_size) or {
|
||||
ctx.status(500)
|
||||
return ctx.json('{"error": "Failed to merge chunks", "message": "$err"}')
|
||||
}
|
||||
|
||||
// 在数据库中记录文件信息
|
||||
file_info := manager.db.insert_or_update_file(file_hash, filename, upload_status.file_size, file_ext) or {
|
||||
println('[DEBUG] Failed to save file info to database: $err')
|
||||
// 即使数据库保存失败,也不影响文件合并
|
||||
FileInfo{}
|
||||
}
|
||||
|
||||
// 更新状态为完成
|
||||
manager.uploads[file_hash].status = 'completed'
|
||||
manager.uploads[file_hash].updated_at = int(time.now().unix())
|
||||
|
||||
// 清理临时分片
|
||||
manager.cleanup_chunks(file_hash, chunk_size)
|
||||
|
||||
// 返回成功响应
|
||||
clean_file_path2 := final_path.replace('\n', '').replace('\r', '').replace('\\', '\\\\').trim_space()
|
||||
return ctx.json('{"success": true, "file_path": "$clean_file_path2", "file_uuid": "${file_info.file_uuid}", "message": "File merged successfully"}')
|
||||
}
|
||||
|
||||
// 获取上传状态
|
||||
pub fn (manager ChunkUploadManager) get_upload_status(mut ctx Context) http.Response {
|
||||
file_hash := ctx.query['file_hash'] or {
|
||||
ctx.status(400)
|
||||
return ctx.json('{"error": "Missing file_hash parameter"}')
|
||||
}
|
||||
|
||||
upload_status := manager.uploads[file_hash] or {
|
||||
ctx.status(404)
|
||||
return ctx.json('{"error": "Upload not found"}')
|
||||
}
|
||||
|
||||
return ctx.json(json.encode(upload_status))
|
||||
}
|
||||
|
||||
// 更新上传状态
|
||||
pub fn (mut manager ChunkUploadManager) update_upload_status(file_hash string, filename string, chunk_index int, file_size int, chunk_size int) {
|
||||
now := int(time.now().unix())
|
||||
|
||||
if file_hash !in manager.uploads {
|
||||
manager.uploads[file_hash] = FileUploadStatus{
|
||||
file_hash: file_hash
|
||||
filename: filename
|
||||
total_chunks: 0 // 不再使用固定的total_chunks,改为动态计算
|
||||
uploaded_chunks: []
|
||||
file_size: file_size
|
||||
chunk_size: chunk_size
|
||||
status: 'uploading'
|
||||
created_at: now
|
||||
updated_at: now
|
||||
}
|
||||
}
|
||||
|
||||
// 添加已上传的分片索引
|
||||
if chunk_index !in manager.uploads[file_hash].uploaded_chunks {
|
||||
manager.uploads[file_hash].uploaded_chunks << chunk_index
|
||||
}
|
||||
|
||||
manager.uploads[file_hash].updated_at = now
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
fn (mut manager ChunkUploadManager) merge_chunks(file_hash string, total_chunks int, final_path string, chunk_size int) ! {
|
||||
println('[DEBUG] Merge chunks called with:')
|
||||
println('[DEBUG] file_hash: $file_hash')
|
||||
println('[DEBUG] total_chunks: $total_chunks')
|
||||
println('[DEBUG] final_path: $final_path')
|
||||
println('[DEBUG] chunk_size: $chunk_size')
|
||||
println('[DEBUG] upload_dir: ${manager.config.upload_dir}')
|
||||
|
||||
// 确保上传目录存在
|
||||
os.mkdir_all(manager.config.upload_dir) or {
|
||||
return error('Failed to create upload directory: $err')
|
||||
}
|
||||
|
||||
// 检查目录权限
|
||||
if !os.is_writable(manager.config.upload_dir) {
|
||||
return error('Upload directory is not writable: ${manager.config.upload_dir}')
|
||||
}
|
||||
|
||||
println('[DEBUG] Creating final file: $final_path')
|
||||
println('[DEBUG] Final path length: ${final_path.len}')
|
||||
println('[DEBUG] Final path bytes: ${final_path.bytes()}')
|
||||
println('[DEBUG] Final path exists: ${os.exists(final_path)}')
|
||||
println('[DEBUG] Final path is file: ${os.is_file(final_path)}')
|
||||
println('[DEBUG] Final path is dir: ${os.is_dir(final_path)}')
|
||||
|
||||
// 如果文件已存在,直接返回成功
|
||||
if os.exists(final_path) {
|
||||
println('[DEBUG] Final file already exists, skipping creation')
|
||||
return
|
||||
}
|
||||
|
||||
// Try to create the file with explicit path cleaning
|
||||
clean_path := final_path.trim_space()
|
||||
println('[DEBUG] Clean path: "$clean_path"')
|
||||
println('[DEBUG] Clean path length: ${clean_path.len}')
|
||||
|
||||
// Try to get absolute path
|
||||
abs_path := os.abs_path(clean_path)
|
||||
println('[DEBUG] Absolute path: "$abs_path"')
|
||||
|
||||
mut final_file := os.create(abs_path) or {
|
||||
println('[DEBUG] File creation failed with error: $err')
|
||||
return error('Failed to create final file: $err')
|
||||
}
|
||||
defer { final_file.close() }
|
||||
|
||||
for i in 0 .. total_chunks {
|
||||
chunk_path := os.join_path(manager.config.temp_dir, file_hash.trim_space(), chunk_size.str(), 'chunk_${i}.part')
|
||||
println('[DEBUG] Reading chunk: $chunk_path')
|
||||
|
||||
if !os.exists(chunk_path) {
|
||||
return error('Chunk file not found: $chunk_path')
|
||||
}
|
||||
|
||||
chunk_data := os.read_file(chunk_path) or {
|
||||
return error('Failed to read chunk $i: $err')
|
||||
}
|
||||
|
||||
final_file.write(chunk_data.bytes()) or {
|
||||
return error('Failed to write chunk $i: $err')
|
||||
}
|
||||
|
||||
println('[DEBUG] Chunk $i merged successfully, size: ${chunk_data.len} bytes')
|
||||
}
|
||||
|
||||
println('[DEBUG] All chunks merged successfully to: $final_path')
|
||||
}
|
||||
|
||||
// 清理临时分片
|
||||
fn (mut manager ChunkUploadManager) cleanup_chunks(file_hash string, chunk_size int) {
|
||||
chunk_dir := os.join_path(manager.config.temp_dir, file_hash.trim_space(), chunk_size.str())
|
||||
if os.exists(chunk_dir) {
|
||||
// 删除整个分片目录
|
||||
os.rmdir_all(chunk_dir) or {
|
||||
println('[DEBUG] Failed to remove chunk directory: $err')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 公共清理方法
|
||||
pub fn (mut manager ChunkUploadManager) cleanup_chunks_public(file_hash string, chunk_size int) {
|
||||
manager.cleanup_chunks(file_hash, chunk_size)
|
||||
}
|
||||
|
||||
// 内部合并处理方法
|
||||
pub fn (mut manager ChunkUploadManager) handle_chunk_merge_internal(file_hash string, filename string, total_chunks int, final_path string, chunk_size int, file_size int, file_ext string) ! {
|
||||
// 执行合并
|
||||
manager.merge_chunks(file_hash, total_chunks, final_path, chunk_size) or {
|
||||
return error('Failed to merge chunks: $err')
|
||||
}
|
||||
|
||||
// 在数据库中记录文件信息
|
||||
manager.db.insert_or_update_file(file_hash, filename, file_size, file_ext) or {
|
||||
println('[DEBUG] Failed to save file info to database: $err')
|
||||
// 即使数据库保存失败,也不影响文件合并
|
||||
}
|
||||
}
|
||||
|
||||
// 生成文件哈希
|
||||
pub fn generate_file_hash(data string) string {
|
||||
return md5.sum(data.bytes()).hex()
|
||||
}
|
||||
|
||||
// 验证文件完整性
|
||||
pub fn verify_file_integrity(file_path string, expected_hash string) bool {
|
||||
file_data := os.read_file(file_path) or { return false }
|
||||
actual_hash := generate_file_hash(file_data)
|
||||
return actual_hash == expected_hash
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
fn get_file_extension(filename string) string {
|
||||
parts := filename.split('.')
|
||||
if parts.len > 1 {
|
||||
return '.${parts.last()}'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 清理文件上传状态(当文件被删除时调用)
|
||||
pub fn (mut manager ChunkUploadManager) cleanup_upload_status(file_hash string) {
|
||||
if file_hash in manager.uploads {
|
||||
manager.uploads.delete(file_hash)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并清理无效的上传状态
|
||||
pub fn (mut manager ChunkUploadManager) cleanup_invalid_status() {
|
||||
mut to_delete := []string{}
|
||||
|
||||
for file_hash, upload_status in manager.uploads {
|
||||
// 检查最终文件是否存在
|
||||
final_path := os.join_path(manager.config.upload_dir, upload_status.filename.trim_space())
|
||||
if !os.exists(final_path) {
|
||||
// 如果最终文件不存在,检查分片文件是否都存在
|
||||
mut all_chunks_exist := true
|
||||
chunk_size := upload_status.chunk_size
|
||||
chunk_dir := os.join_path(manager.config.temp_dir, file_hash.trim_space(), chunk_size.str())
|
||||
|
||||
// 动态检查分片文件
|
||||
for i := 0; ; i++ {
|
||||
chunk_path := os.join_path(chunk_dir, 'chunk_${i}.part')
|
||||
if !os.exists(chunk_path) {
|
||||
break
|
||||
}
|
||||
// 如果找到至少一个分片,说明上传正在进行中
|
||||
all_chunks_exist = false
|
||||
break
|
||||
}
|
||||
|
||||
// 如果没有找到任何分片文件,清理状态
|
||||
if all_chunks_exist {
|
||||
to_delete << file_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除无效状态
|
||||
for file_hash in to_delete {
|
||||
manager.uploads.delete(file_hash)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user