example: add chunk file upload example to example.v
This commit is contained in:
@@ -2,11 +2,16 @@ module main
|
|||||||
|
|
||||||
import net.http
|
import net.http
|
||||||
import hono
|
import hono
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// ===== 1. 应用初始化 =====
|
// ===== 1. 应用初始化 =====
|
||||||
mut app := hono.Hono.new()
|
mut app := hono.Hono.new()
|
||||||
|
|
||||||
|
// 创建分片上传管理器
|
||||||
|
mut upload_manager := hono.new_chunk_upload_manager(hono.ChunkUploadConfig{})
|
||||||
|
|
||||||
// ===== 2. 中间件配置 =====
|
// ===== 2. 中间件配置 =====
|
||||||
|
|
||||||
// 日志中间件
|
// 日志中间件
|
||||||
@@ -50,7 +55,231 @@ fn main() {
|
|||||||
}
|
}
|
||||||
app.use(hono.serve_static(options))
|
app.use(hono.serve_static(options))
|
||||||
|
|
||||||
// ===== 4. 基础路由 =====
|
// ===== 4. 分块上传功能 =====
|
||||||
|
|
||||||
|
// 分片上传接口
|
||||||
|
app.post('/upload/chunk', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
return upload_manager.handle_chunk_upload(mut c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询上传状态接口
|
||||||
|
app.get('/upload/status', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
return upload_manager.get_upload_status(mut c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询已上传分片接口
|
||||||
|
app.get('/upload/chunks', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
file_hash := c.query['file_hash'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing file_hash parameter"}')
|
||||||
|
}
|
||||||
|
upload_status := upload_manager.uploads[file_hash] or {
|
||||||
|
// 如果内存中没有记录,检查是否有最终文件(已完成上传)
|
||||||
|
final_path := './uploads/${file_hash.trim_space()}'
|
||||||
|
if os.exists(final_path) {
|
||||||
|
// 文件已存在,返回所有分片已上传
|
||||||
|
mut uploaded_chunks := []int{}
|
||||||
|
uploaded_chunks << 0 // 假设只有一个分片
|
||||||
|
return c.json('{"uploaded_chunks": ' + json.encode(uploaded_chunks) + ', "total_chunks": 1, "completed": true}')
|
||||||
|
}
|
||||||
|
c.status(404)
|
||||||
|
return c.json('{"error": "Upload not found"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否所有分片都已上传(基于分片大小总和)
|
||||||
|
chunk_dir := os.join_path(os.join_path('./uploads/chunks', file_hash.trim_space()), upload_status.chunk_size.str())
|
||||||
|
mut total_chunk_size := u64(0)
|
||||||
|
mut chunk_count := 0
|
||||||
|
|
||||||
|
if os.exists(chunk_dir) {
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
chunk_path := os.join_path(chunk_dir, 'chunk_${i}.part')
|
||||||
|
if !os.exists(chunk_path) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
chunk_info := os.stat(chunk_path) or { continue }
|
||||||
|
total_chunk_size += chunk_info.size
|
||||||
|
chunk_count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_completed := total_chunk_size >= u64(upload_status.file_size)
|
||||||
|
|
||||||
|
return c.json('{"uploaded_chunks": ' + json.encode(upload_status.uploaded_chunks) + ', "total_chunks": $chunk_count, "completed": $is_completed}')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查分片是否存在接口(秒传功能)
|
||||||
|
app.get('/upload/chunk_exists', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
file_hash := c.query['file_hash'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing file_hash parameter"}')
|
||||||
|
}
|
||||||
|
chunk_index := c.query['chunk_index'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing chunk_index parameter"}')
|
||||||
|
}
|
||||||
|
chunk_hash := c.query['chunk_hash'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing chunk_hash parameter"}')
|
||||||
|
}
|
||||||
|
file_size_str := c.query['file_size'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing file_size parameter"}')
|
||||||
|
}
|
||||||
|
file_size := file_size_str.int()
|
||||||
|
|
||||||
|
// 新增 trunk_size 参数
|
||||||
|
trunk_size_str := c.query['trunk_size'] or { '' }
|
||||||
|
mut chunk_size := upload_manager.config.chunk_size
|
||||||
|
if trunk_size_str != '' {
|
||||||
|
chunk_size = trunk_size_str.int()
|
||||||
|
} else if upload_status := upload_manager.uploads[file_hash] {
|
||||||
|
chunk_size = upload_status.chunk_size
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk_path := os.join_path(os.join_path(os.join_path('./uploads/chunks', file_hash.trim_space()), chunk_size.str()), 'chunk_${chunk_index}.part')
|
||||||
|
println('[DEBUG] Checking chunk: $chunk_path')
|
||||||
|
println('[DEBUG] File hash: $file_hash')
|
||||||
|
println('[DEBUG] Chunk index: $chunk_index')
|
||||||
|
println('[DEBUG] Expected hash: $chunk_hash')
|
||||||
|
|
||||||
|
// 清理无效的上传状态
|
||||||
|
upload_manager.cleanup_invalid_status()
|
||||||
|
|
||||||
|
mut exists := os.exists(chunk_path)
|
||||||
|
println('[DEBUG] File exists: $exists')
|
||||||
|
|
||||||
|
// 计算是否所有分片都已上传
|
||||||
|
mut all_chunk_uploaded := false
|
||||||
|
|
||||||
|
// 方案1:优先检查最终文件是否存在(新格式:filehash.filetype)
|
||||||
|
uploads_dir := './uploads/files'
|
||||||
|
if os.exists(uploads_dir) {
|
||||||
|
// 从上传状态中获取文件扩展名
|
||||||
|
mut file_ext := ''
|
||||||
|
if upload_status := upload_manager.uploads[file_hash] {
|
||||||
|
file_ext = get_file_extension(upload_status.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果内存中没有记录,尝试从数据库获取
|
||||||
|
if file_ext == '' {
|
||||||
|
if file_info := upload_manager.db.get_file_by_hash(file_hash) {
|
||||||
|
file_ext = file_info.file_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建最终文件名
|
||||||
|
final_filename := '${file_hash.trim_space()}${file_ext}'
|
||||||
|
final_path := os.join_path(uploads_dir, final_filename)
|
||||||
|
|
||||||
|
if os.exists(final_path) {
|
||||||
|
println('[DEBUG] Found final file: $final_path')
|
||||||
|
all_chunk_uploaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案2:如果最终文件不存在,检查分片文件
|
||||||
|
if !all_chunk_uploaded {
|
||||||
|
chunk_dir := os.join_path(os.join_path('./uploads/chunks', file_hash.trim_space()), chunk_size.str())
|
||||||
|
if os.exists(chunk_dir) {
|
||||||
|
// 计算所有分片文件的大小总和
|
||||||
|
mut total_chunk_size := u64(0)
|
||||||
|
mut chunk_count := 0
|
||||||
|
// 遍历所有分片文件
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
chunk_file_path := '${chunk_dir}/chunk_${i}.part'
|
||||||
|
if !os.exists(chunk_file_path) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 获取分片文件大小
|
||||||
|
chunk_info := os.stat(chunk_file_path) or { break }
|
||||||
|
total_chunk_size += chunk_info.size
|
||||||
|
chunk_count++
|
||||||
|
}
|
||||||
|
// 使用传入的文件大小
|
||||||
|
expected_file_size := u64(file_size)
|
||||||
|
println('[DEBUG] Total chunk size: $total_chunk_size, Expected file size: $expected_file_size, Chunk count: $chunk_count')
|
||||||
|
|
||||||
|
// 计算理论上需要的分片数量
|
||||||
|
expected_chunks := (expected_file_size + u64(chunk_size) - 1) / u64(chunk_size)
|
||||||
|
println('[DEBUG] Expected chunks: $expected_chunks')
|
||||||
|
|
||||||
|
// 只有当分片数量达到预期且总大小 >= 文件大小时,才认为上传完成
|
||||||
|
if chunk_count >= int(expected_chunks) && total_chunk_size >= expected_file_size {
|
||||||
|
all_chunk_uploaded = true
|
||||||
|
println('[DEBUG] All chunks uploaded: chunk_count=$chunk_count >= expected_chunks=$expected_chunks && total_size=$total_chunk_size >= file_size=$expected_file_size')
|
||||||
|
} else {
|
||||||
|
println('[DEBUG] Not all chunks uploaded: chunk_count=$chunk_count < expected_chunks=$expected_chunks || total_size=$total_chunk_size < file_size=$expected_file_size')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println('[DEBUG] All chunk uploaded: $all_chunk_uploaded')
|
||||||
|
|
||||||
|
// chunk_exists 接口不应该自动触发合并,只返回状态信息
|
||||||
|
// 合并操作应该在 /upload/chunk 接口中处理
|
||||||
|
|
||||||
|
// 只检查文件是否存在,不验证 hash(因为前后端 hash 算法可能不一致)
|
||||||
|
// 如果需要严格验证,可以在这里添加 hash 检查逻辑
|
||||||
|
|
||||||
|
return c.json('{"exists": $exists, "all_chunk_uploaded": $all_chunk_uploaded}')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取所有文件信息
|
||||||
|
app.get('/api/files', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
files := upload_manager.db.get_all_files() or {
|
||||||
|
c.status(500)
|
||||||
|
return c.json('{"error": "Failed to get files", "message": "$err"}')
|
||||||
|
}
|
||||||
|
return c.json(json.encode(files))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据文件UUID获取文件信息
|
||||||
|
app.get('/api/files/:uuid', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
file_uuid := c.params['uuid'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing file UUID"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
file_info := upload_manager.db.get_file_by_uuid(file_uuid) or {
|
||||||
|
c.status(404)
|
||||||
|
return c.json('{"error": "File not found"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(json.encode(file_info))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据文件hash获取文件信息
|
||||||
|
app.get('/api/files/hash/:hash', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
file_hash := c.params['hash'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing file hash"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
file_info := upload_manager.db.get_file_by_hash(file_hash) or {
|
||||||
|
c.status(404)
|
||||||
|
return c.json('{"error": "File not found"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(json.encode(file_info))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除文件信息
|
||||||
|
app.delete('/api/files/:uuid', fn [mut upload_manager] (mut c hono.Context) http.Response {
|
||||||
|
file_uuid := c.params['uuid'] or {
|
||||||
|
c.status(400)
|
||||||
|
return c.json('{"error": "Missing file UUID"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_manager.db.delete_file(file_uuid) or {
|
||||||
|
c.status(500)
|
||||||
|
return c.json('{"error": "Failed to delete file", "message": "$err"}')
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json('{"success": true, "message": "File deleted successfully"}')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== 5. 基础路由 =====
|
||||||
|
|
||||||
// 根路径 - 欢迎页面
|
// 根路径 - 欢迎页面
|
||||||
app.get('/', fn (mut c hono.Context) http.Response {
|
app.get('/', fn (mut c hono.Context) http.Response {
|
||||||
@@ -69,11 +298,12 @@ fn main() {
|
|||||||
.api-section { background: #f3e5f5; border-left: 4px solid #9C27B0; }
|
.api-section { background: #f3e5f5; border-left: 4px solid #9C27B0; }
|
||||||
.file-section { background: #e8f5e8; border-left: 4px solid #4CAF50; }
|
.file-section { background: #e8f5e8; border-left: 4px solid #4CAF50; }
|
||||||
.route-section { background: #fff3e0; border-left: 4px solid #FF9800; }
|
.route-section { background: #fff3e0; border-left: 4px solid #FF9800; }
|
||||||
|
.upload-section { background: #fff8e1; border-left: 4px solid #FFC107; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>🚀 V-Hono 完整示例</h1>
|
<h1>🚀 V-Hono 完整示例</h1>
|
||||||
<p>A lightweight web framework for V with static file serving</p>
|
<p>A lightweight web framework for V with static file serving and chunk upload</p>
|
||||||
|
|
||||||
<div class="section static-section">
|
<div class="section static-section">
|
||||||
<h2>📁 静态文件服务</h2>
|
<h2>📁 静态文件服务</h2>
|
||||||
@@ -142,6 +372,33 @@ fn main() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section upload-section">
|
||||||
|
<h2>📤 分块上传功能</h2>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>分片上传:</strong> <a href="/upload/chunk">POST /upload/chunk</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>上传状态:</strong> <a href="/upload/status">GET /upload/status</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>分片查询:</strong> <a href="/upload/chunks">GET /upload/chunks</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>分片检查:</strong> <a href="/upload/chunk_exists">GET /upload/chunk_exists</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>文件管理:</strong> <a href="/api/files">GET /api/files</a>
|
||||||
|
</div>
|
||||||
|
<p><strong>特性:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li class="feature">🚀 大文件分片上传</li>
|
||||||
|
<li class="feature">🔄 断点续传</li>
|
||||||
|
<li class="feature">⚡ 秒传功能</li>
|
||||||
|
<li class="feature">📊 上传进度监控</li>
|
||||||
|
<li class="feature">🛡️ 文件完整性验证</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>✨ 特性</h2>
|
<h2>✨ 特性</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -155,6 +412,7 @@ fn main() {
|
|||||||
<li class="feature">🔧 洋葱模型中间件</li>
|
<li class="feature">🔧 洋葱模型中间件</li>
|
||||||
<li class="feature">🔄 支持所有 HTTP 方法</li>
|
<li class="feature">🔄 支持所有 HTTP 方法</li>
|
||||||
<li class="feature">📊 路由性能统计</li>
|
<li class="feature">📊 路由性能统计</li>
|
||||||
|
<li class="feature">📤 分块上传支持</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,6 +440,11 @@ curl http://localhost:8080/hello
|
|||||||
curl http://localhost:8080/users/123
|
curl http://localhost:8080/users/123
|
||||||
curl http://localhost:8080/posts/456/comments/789
|
curl http://localhost:8080/posts/456/comments/789
|
||||||
|
|
||||||
|
# 分块上传测试
|
||||||
|
curl -X POST http://localhost:8080/upload/chunk -F "file_hash=test123" -F "chunk_index=0" -F "chunk=@test.txt"
|
||||||
|
curl http://localhost:8080/upload/status?file_hash=test123
|
||||||
|
curl http://localhost:8080/api/files
|
||||||
|
|
||||||
# POST 测试
|
# POST 测试
|
||||||
curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"
|
curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"
|
||||||
curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"
|
curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"
|
||||||
@@ -210,7 +473,7 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.json(response)
|
return c.json(response)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 5. API 路由 =====
|
// ===== 6. API 路由 =====
|
||||||
|
|
||||||
// 健康检查
|
// 健康检查
|
||||||
app.get('/api/health', fn (mut c hono.Context) http.Response {
|
app.get('/api/health', fn (mut c hono.Context) http.Response {
|
||||||
@@ -237,12 +500,13 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
"ETag support",
|
"ETag support",
|
||||||
"Last-Modified headers",
|
"Last-Modified headers",
|
||||||
"Configurable caching",
|
"Configurable caching",
|
||||||
"Custom headers"
|
"Custom headers",
|
||||||
|
"Chunk upload support"
|
||||||
]
|
]
|
||||||
}')
|
}')
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 6. 动态路由 =====
|
// ===== 7. 动态路由 =====
|
||||||
|
|
||||||
// 单参数路由
|
// 单参数路由
|
||||||
app.get('/users/:id', fn (mut c hono.Context) http.Response {
|
app.get('/users/:id', fn (mut c hono.Context) http.Response {
|
||||||
@@ -279,7 +543,7 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.json(response)
|
return c.json(response)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 7. HTTP 方法示例 =====
|
// ===== 8. HTTP 方法示例 =====
|
||||||
|
|
||||||
// POST 请求
|
// POST 请求
|
||||||
app.post('/api/users', fn (mut c hono.Context) http.Response {
|
app.post('/api/users', fn (mut c hono.Context) http.Response {
|
||||||
@@ -380,7 +644,7 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.text('')
|
return c.text('')
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 8. 文件服务功能 =====
|
// ===== 9. 文件服务功能 =====
|
||||||
|
|
||||||
// 基本文件服务示例
|
// 基本文件服务示例
|
||||||
app.get('/file/:filename', fn (mut c hono.Context) http.Response {
|
app.get('/file/:filename', fn (mut c hono.Context) http.Response {
|
||||||
@@ -449,7 +713,7 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.file_with_options(file_path, options)
|
return c.file_with_options(file_path, options)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 9. 错误处理示例 =====
|
// ===== 10. 错误处理示例 =====
|
||||||
|
|
||||||
// 安全文件服务
|
// 安全文件服务
|
||||||
app.get('/safe-file/:filename', fn (mut c hono.Context) http.Response {
|
app.get('/safe-file/:filename', fn (mut c hono.Context) http.Response {
|
||||||
@@ -471,7 +735,7 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
return c.json('{"error": "Not Found", "message": "The requested resource was not found"}')
|
return c.json('{"error": "Not Found", "message": "The requested resource was not found"}')
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== 10. 服务器启动 =====
|
// ===== 11. 服务器启动 =====
|
||||||
|
|
||||||
println('🚀 V-Hono 完整示例启动中...')
|
println('🚀 V-Hono 完整示例启动中...')
|
||||||
println('📍 服务器地址: http://localhost:8080')
|
println('📍 服务器地址: http://localhost:8080')
|
||||||
@@ -497,6 +761,13 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
println(' - GET /dynamic/:filename - 动态文件服务')
|
println(' - GET /dynamic/:filename - 动态文件服务')
|
||||||
println(' - GET /safe-file/:filename - 安全文件服务')
|
println(' - GET /safe-file/:filename - 安全文件服务')
|
||||||
println('')
|
println('')
|
||||||
|
println('📤 分块上传端点:')
|
||||||
|
println(' - POST /upload/chunk - 分片上传')
|
||||||
|
println(' - GET /upload/status - 上传状态')
|
||||||
|
println(' - GET /upload/chunks - 分片查询')
|
||||||
|
println(' - GET /upload/chunk_exists - 分片检查')
|
||||||
|
println(' - GET /api/files - 文件管理')
|
||||||
|
println('')
|
||||||
println('🎯 动态路由:')
|
println('🎯 动态路由:')
|
||||||
println(' - GET /users/123')
|
println(' - GET /users/123')
|
||||||
println(' - GET /posts/456/comments/789')
|
println(' - GET /posts/456/comments/789')
|
||||||
@@ -534,9 +805,28 @@ curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"na
|
|||||||
println(' curl http://localhost:8080/users/123')
|
println(' curl http://localhost:8080/users/123')
|
||||||
println(' curl http://localhost:8080/posts/456/comments/789')
|
println(' curl http://localhost:8080/posts/456/comments/789')
|
||||||
println('')
|
println('')
|
||||||
|
println(' # 分块上传测试')
|
||||||
|
println(' curl -X POST http://localhost:8080/upload/chunk -F "file_hash=test123" -F "chunk_index=0" -F "chunk=@test.txt"')
|
||||||
|
println(' curl http://localhost:8080/upload/status?file_hash=test123')
|
||||||
|
println(' curl http://localhost:8080/api/files')
|
||||||
|
println('')
|
||||||
println(' # POST 测试')
|
println(' # POST 测试')
|
||||||
println(' curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"')
|
println(' curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"')
|
||||||
println(' curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"')
|
println(' curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"')
|
||||||
|
|
||||||
app.listen(':8080')
|
app.listen(':8080')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件扩展名
|
||||||
|
fn get_file_extension(filename string) string {
|
||||||
|
println('[DEBUG] Getting extension for filename: "$filename"')
|
||||||
|
parts := filename.split('.')
|
||||||
|
println('[DEBUG] Split parts: $parts')
|
||||||
|
if parts.len > 1 {
|
||||||
|
ext := '.${parts.last()}'
|
||||||
|
println('[DEBUG] Extracted extension: "$ext"')
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
println('[DEBUG] No extension found, returning empty string')
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
@@ -244,6 +244,71 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分块上传相关样式 */
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area.dragover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #007bff, #28a745);
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunk-status {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -281,6 +346,10 @@
|
|||||||
<h3>🔄 HTTP 方法支持</h3>
|
<h3>🔄 HTTP 方法支持</h3>
|
||||||
<p>完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法</p>
|
<p>完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<h3>📤 分块上传</h3>
|
||||||
|
<p>支持大文件分片上传、断点续传、秒传功能,内置文件完整性验证</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -307,6 +376,7 @@
|
|||||||
<button class="tab-button" onclick="showTab('files')">文件服务</button>
|
<button class="tab-button" onclick="showTab('files')">文件服务</button>
|
||||||
<button class="tab-button" onclick="showTab('routes')">路由测试</button>
|
<button class="tab-button" onclick="showTab('routes')">路由测试</button>
|
||||||
<button class="tab-button" onclick="showTab('static')">静态文件</button>
|
<button class="tab-button" onclick="showTab('static')">静态文件</button>
|
||||||
|
<button class="tab-button" onclick="showTab('upload')">分块上传</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API 测试 -->
|
<!-- API 测试 -->
|
||||||
@@ -354,6 +424,40 @@
|
|||||||
<div id="static-result" class="result" style="display: none;"></div>
|
<div id="static-result" class="result" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 分块上传测试 -->
|
||||||
|
<div id="upload" class="tab-content">
|
||||||
|
<div class="test-area">
|
||||||
|
<h4>📤 分块上传功能</h4>
|
||||||
|
<p>支持大文件分片上传、断点续传、秒传功能</p>
|
||||||
|
|
||||||
|
<div class="upload-area" id="uploadArea">
|
||||||
|
<p>📁 拖拽文件到此处或点击选择文件</p>
|
||||||
|
<input type="file" id="fileInput" style="display: none;" multiple>
|
||||||
|
<button class="test-button" onclick="document.getElementById('fileInput').click()">选择文件</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-info" id="fileInfo">
|
||||||
|
<h4>文件信息</h4>
|
||||||
|
<p><strong>文件名:</strong> <span id="fileName"></span></p>
|
||||||
|
<p><strong>文件大小:</strong> <span id="fileSize"></span></p>
|
||||||
|
<p><strong>分片数量:</strong> <span id="chunkCount"></span></p>
|
||||||
|
<p><strong>分片大小:</strong> <span id="chunkSize"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-container" id="progressContainer">
|
||||||
|
<h4>上传进度</h4>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="chunk-status" id="chunkStatus"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status" id="uploadStatus"></div>
|
||||||
|
|
||||||
|
<button class="test-button" id="uploadBtn" onclick="startUpload()" style="display: none;">开始上传</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -413,6 +517,23 @@
|
|||||||
<div class="endpoint">
|
<div class="endpoint">
|
||||||
<strong>文件目录:</strong> <a href="/files/sample.json" target="_blank">/files/sample.json</a> (来自 ./uploads)
|
<strong>文件目录:</strong> <a href="/files/sample.json" target="_blank">/files/sample.json</a> (来自 ./uploads)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>📤 分块上传端点</h3>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>分片上传:</strong> <a href="/upload/chunk" target="_blank">POST /upload/chunk</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>上传状态:</strong> <a href="/upload/status" target="_blank">GET /upload/status</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>分片查询:</strong> <a href="/upload/chunks" target="_blank">GET /upload/chunks</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>分片检查:</strong> <a href="/upload/chunk_exists" target="_blank">GET /upload/chunk_exists</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>文件管理:</strong> <a href="/api/files" target="_blank">GET /api/files</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 性能统计 -->
|
<!-- 性能统计 -->
|
||||||
@@ -477,6 +598,7 @@ app.listen(':8080')
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let requestCount = 0;
|
let requestCount = 0;
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
@@ -769,6 +891,252 @@ app.listen(':8080')
|
|||||||
setToken();
|
setToken();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== 分块上传功能 =====
|
||||||
|
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
|
||||||
|
let selectedFile = null;
|
||||||
|
let fileHash = null;
|
||||||
|
let totalChunks = 0;
|
||||||
|
let uploadedChunks = 0;
|
||||||
|
|
||||||
|
// 文件选择处理
|
||||||
|
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
||||||
|
|
||||||
|
// 拖拽处理
|
||||||
|
const uploadArea = document.getElementById('uploadArea');
|
||||||
|
uploadArea.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadArea.style.borderColor = '#007bff';
|
||||||
|
uploadArea.style.backgroundColor = '#f8f9fa';
|
||||||
|
});
|
||||||
|
uploadArea.addEventListener('dragleave', () => {
|
||||||
|
uploadArea.style.borderColor = '#ccc';
|
||||||
|
uploadArea.style.backgroundColor = 'transparent';
|
||||||
|
});
|
||||||
|
uploadArea.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadArea.style.borderColor = '#ccc';
|
||||||
|
uploadArea.style.backgroundColor = 'transparent';
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
document.getElementById('fileInput').files = files;
|
||||||
|
handleFileSelect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleFileSelect() {
|
||||||
|
const file = document.getElementById('fileInput').files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// 重置上传状态
|
||||||
|
resetUploadState();
|
||||||
|
|
||||||
|
selectedFile = file;
|
||||||
|
totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||||
|
|
||||||
|
// 显示文件信息
|
||||||
|
document.getElementById('fileName').textContent = file.name;
|
||||||
|
document.getElementById('fileSize').textContent = formatFileSize(file.size);
|
||||||
|
document.getElementById('chunkCount').textContent = totalChunks;
|
||||||
|
document.getElementById('chunkSize').textContent = formatFileSize(CHUNK_SIZE);
|
||||||
|
|
||||||
|
document.getElementById('fileInfo').style.display = 'block';
|
||||||
|
document.getElementById('uploadBtn').style.display = 'inline-block';
|
||||||
|
|
||||||
|
// 计算文件hash
|
||||||
|
calculateFileHash(file).then(hash => {
|
||||||
|
fileHash = hash;
|
||||||
|
console.log('File hash:', hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置上传状态
|
||||||
|
function resetUploadState() {
|
||||||
|
// 重置进度条
|
||||||
|
document.getElementById('progressFill').style.width = '0%';
|
||||||
|
document.getElementById('chunkStatus').textContent = '';
|
||||||
|
|
||||||
|
// 隐藏进度容器
|
||||||
|
document.getElementById('progressContainer').style.display = 'none';
|
||||||
|
|
||||||
|
// 重置状态信息
|
||||||
|
document.getElementById('uploadStatus').style.display = 'none';
|
||||||
|
|
||||||
|
// 重置上传按钮
|
||||||
|
const uploadBtn = document.getElementById('uploadBtn');
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
uploadBtn.textContent = '开始上传';
|
||||||
|
|
||||||
|
// 重置变量
|
||||||
|
uploadedChunks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startUpload() {
|
||||||
|
if (!selectedFile || !fileHash) {
|
||||||
|
showUploadStatus('请先选择文件', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadBtn = document.getElementById('uploadBtn');
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
uploadBtn.textContent = '上传中...';
|
||||||
|
|
||||||
|
document.getElementById('progressContainer').style.display = 'block';
|
||||||
|
showUploadStatus('开始上传...', 'success');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 直接开始上传分片,每个分片都会检查是否存在
|
||||||
|
for (let i = 0; i < totalChunks; i++) {
|
||||||
|
const chunk = selectedFile.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
||||||
|
const chunkHash = await calculateChunkHash(chunk);
|
||||||
|
|
||||||
|
// 检查分片是否已存在(包括秒传检测)
|
||||||
|
const checkResult = await checkChunkExists(fileHash, i, chunkHash);
|
||||||
|
|
||||||
|
// 如果检测到所有分片都已上传完成,立即结束上传
|
||||||
|
if (checkResult.all_uploaded) {
|
||||||
|
console.log('检测到秒传,所有分片都已上传完成!');
|
||||||
|
updateProgress(totalChunks, totalChunks); // 更新进度到100%
|
||||||
|
showUploadStatus('秒传成功!文件已完整上传。', 'success');
|
||||||
|
uploadBtn.textContent = '上传完成';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果分片已存在,跳过上传
|
||||||
|
if (checkResult.exists) {
|
||||||
|
console.log(`分片 ${i} 已存在,跳过上传`);
|
||||||
|
updateProgress(i + 1, totalChunks);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传分片
|
||||||
|
await uploadChunk(selectedFile, chunk, {
|
||||||
|
fileHash, chunkIndex: i, totalChunks, chunkHash
|
||||||
|
});
|
||||||
|
|
||||||
|
updateProgress(i + 1, totalChunks);
|
||||||
|
console.log(`分片 ${i} 上传完成`);
|
||||||
|
}
|
||||||
|
|
||||||
|
showUploadStatus('上传完成!文件已自动合并。', 'success');
|
||||||
|
uploadBtn.textContent = '上传完成';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
showUploadStatus('上传失败: ' + error.message, 'error');
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
uploadBtn.textContent = '重新上传';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算文件hash
|
||||||
|
async function calculateFileHash(file) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const spark = new SparkMD5.ArrayBuffer();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
spark.append(e.target.result);
|
||||||
|
resolve(spark.end());
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分片hash
|
||||||
|
async function calculateChunkHash(chunk) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const spark = new SparkMD5.ArrayBuffer();
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
spark.append(e.target.result);
|
||||||
|
resolve(spark.end());
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(chunk);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查分片hash是否已存在
|
||||||
|
async function checkChunkExists(fileHash, chunkIndex, chunkHash) {
|
||||||
|
const url = new URL('/upload/chunk_exists', window.location.origin);
|
||||||
|
url.searchParams.set('file_hash', fileHash);
|
||||||
|
url.searchParams.set('chunk_index', chunkIndex);
|
||||||
|
url.searchParams.set('chunk_hash', chunkHash);
|
||||||
|
url.searchParams.set('file_size', selectedFile.size);
|
||||||
|
url.searchParams.set('trunk_size', CHUNK_SIZE);
|
||||||
|
|
||||||
|
if (currentToken) {
|
||||||
|
url.searchParams.set('token', currentToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url.toString());
|
||||||
|
if (!res.ok) return { exists: false, all_uploaded: false };
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// 如果所有分片都已上传完成,直接返回 true 表示秒传成功
|
||||||
|
if (data.all_chunk_uploaded) {
|
||||||
|
console.log('所有分片都已上传完成,秒传成功!');
|
||||||
|
return { exists: true, all_uploaded: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: data.exists, all_uploaded: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传分片
|
||||||
|
async function uploadChunk(file, chunk, { fileHash, chunkIndex, totalChunks, chunkHash }) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file_hash', fileHash);
|
||||||
|
form.append('chunk_index', chunkIndex);
|
||||||
|
form.append('total_chunks', totalChunks);
|
||||||
|
form.append('filename', file.name);
|
||||||
|
form.append('file_size', file.size);
|
||||||
|
form.append('chunk_size', CHUNK_SIZE);
|
||||||
|
form.append('chunk_hash', chunkHash);
|
||||||
|
form.append('chunk', chunk, file.name + '.part' + chunkIndex);
|
||||||
|
|
||||||
|
const url = new URL('/upload/chunk', window.location.origin);
|
||||||
|
if (currentToken) {
|
||||||
|
url.searchParams.set('token', currentToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || '上传失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.all_chunk_uploaded) {
|
||||||
|
console.log('所有分片已上传完成,文件已自动合并:', result.file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
function updateProgress(current, total) {
|
||||||
|
const percentage = (current / total) * 100;
|
||||||
|
document.getElementById('progressFill').style.width = percentage + '%';
|
||||||
|
document.getElementById('chunkStatus').textContent = current + '/' + total + ' 分片已上传 (' + percentage.toFixed(1) + '%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示上传状态
|
||||||
|
function showUploadStatus(message, type) {
|
||||||
|
const status = document.getElementById('uploadStatus');
|
||||||
|
status.textContent = message;
|
||||||
|
status.className = 'status ' + type;
|
||||||
|
status.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
module main
|
|
||||||
|
|
||||||
import hono
|
|
||||||
import os
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println('Testing upload without total_chunks parameter...')
|
|
||||||
|
|
||||||
// Create upload manager
|
|
||||||
mut upload_manager := hono.new_chunk_upload_manager(hono.ChunkUploadConfig{})
|
|
||||||
|
|
||||||
// Test file info
|
|
||||||
file_hash := 'test123456789'
|
|
||||||
filename := 'test_file.zip'
|
|
||||||
chunk_index := 0
|
|
||||||
chunk_size := 2097152
|
|
||||||
file_size := 93569
|
|
||||||
|
|
||||||
// Create test chunk directory and file
|
|
||||||
chunk_dir := os.join_path(os.join_path('./uploads/chunks', file_hash), chunk_size.str())
|
|
||||||
os.mkdir_all(chunk_dir) or { panic('Failed to create chunk directory') }
|
|
||||||
|
|
||||||
// Create a test chunk file
|
|
||||||
chunk_path := os.join_path(chunk_dir, 'chunk_${chunk_index}.part')
|
|
||||||
test_data := 'This is test chunk data for testing the merge functionality.'
|
|
||||||
os.write_file(chunk_path, test_data) or { panic('Failed to create test chunk') }
|
|
||||||
|
|
||||||
println('Created test chunk at: $chunk_path')
|
|
||||||
|
|
||||||
// Test the update_upload_status function
|
|
||||||
upload_manager.update_upload_status(file_hash, filename, chunk_index, file_size, chunk_size)
|
|
||||||
println('Updated upload status')
|
|
||||||
|
|
||||||
// Check if the upload status was created correctly
|
|
||||||
if file_hash in upload_manager.uploads {
|
|
||||||
status := upload_manager.uploads[file_hash]
|
|
||||||
println('Upload status:')
|
|
||||||
println(' File hash: ${status.file_hash}')
|
|
||||||
println(' Filename: ${status.filename}')
|
|
||||||
println(' Total chunks: ${status.total_chunks}')
|
|
||||||
println(' Uploaded chunks: ${status.uploaded_chunks}')
|
|
||||||
println(' File size: ${status.file_size}')
|
|
||||||
println(' Chunk size: ${status.chunk_size}')
|
|
||||||
println(' Status: ${status.status}')
|
|
||||||
} else {
|
|
||||||
println('Upload status not found!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the merge logic
|
|
||||||
file_ext := get_file_extension(filename)
|
|
||||||
final_filename := '${file_hash.trim_space()}${file_ext}'
|
|
||||||
final_path := os.join_path('./uploads/files', final_filename)
|
|
||||||
|
|
||||||
println('Testing merge logic...')
|
|
||||||
println('Final path: $final_path')
|
|
||||||
|
|
||||||
// Calculate actual total chunks
|
|
||||||
mut actual_total_chunks := 0
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
check_chunk_path := os.join_path(chunk_dir, 'chunk_${i}.part')
|
|
||||||
if !os.exists(check_chunk_path) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
actual_total_chunks++
|
|
||||||
}
|
|
||||||
|
|
||||||
println('Actual total chunks: $actual_total_chunks')
|
|
||||||
|
|
||||||
// Test merge
|
|
||||||
upload_manager.handle_chunk_merge_internal(file_hash, filename, actual_total_chunks, final_path, chunk_size, file_size, file_ext) or {
|
|
||||||
println('Merge failed: $err')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
println('Merge successful!')
|
|
||||||
println('Final file exists: ${os.exists(final_path)}')
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
os.rmdir_all(chunk_dir) or { println('Failed to clean up chunk directory') }
|
|
||||||
os.rm(final_path) or { println('Failed to clean up final file') }
|
|
||||||
|
|
||||||
println('Test completed successfully!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件扩展名
|
|
||||||
fn get_file_extension(filename string) string {
|
|
||||||
parts := filename.split('.')
|
|
||||||
if parts.len > 1 {
|
|
||||||
return '.${parts.last()}'
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
module main
|
|
||||||
|
|
||||||
import hono
|
|
||||||
import os
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println('Testing specific file fix...')
|
|
||||||
|
|
||||||
// Create upload manager
|
|
||||||
mut upload_manager := hono.new_chunk_upload_manager(hono.ChunkUploadConfig{})
|
|
||||||
|
|
||||||
// Use the exact same parameters from the failing case
|
|
||||||
file_hash := 'a45abf58302850765be03cf78bcf5129'
|
|
||||||
filename := 'test.zip' // This should give us .zip extension
|
|
||||||
total_chunks := 1
|
|
||||||
chunk_size := 2097152
|
|
||||||
file_size := 93569
|
|
||||||
|
|
||||||
// Create test chunk directory and file
|
|
||||||
chunk_dir := os.join_path(os.join_path('./uploads/chunks', file_hash), chunk_size.str())
|
|
||||||
os.mkdir_all(chunk_dir) or { panic('Failed to create chunk directory') }
|
|
||||||
|
|
||||||
// Create a test chunk file
|
|
||||||
chunk_path := os.join_path(chunk_dir, 'chunk_0.part')
|
|
||||||
test_data := 'This is test chunk data for testing the merge functionality.'
|
|
||||||
os.write_file(chunk_path, test_data) or { panic('Failed to create test chunk') }
|
|
||||||
|
|
||||||
println('Created test chunk at: $chunk_path')
|
|
||||||
|
|
||||||
// Test the file extension function
|
|
||||||
file_ext := get_file_extension(filename)
|
|
||||||
println('File extension: "$file_ext"')
|
|
||||||
|
|
||||||
// Test the merge functionality with the exact same path construction
|
|
||||||
final_filename := '${file_hash.trim_space()}${file_ext}'
|
|
||||||
final_path := os.join_path('./uploads/files', final_filename)
|
|
||||||
println('Attempting to merge to: $final_path')
|
|
||||||
|
|
||||||
upload_manager.handle_chunk_merge_internal(file_hash, filename, total_chunks, final_path, chunk_size, file_size, file_ext) or {
|
|
||||||
println('Merge failed: $err')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
println('Merge successful!')
|
|
||||||
println('Final file exists: ${os.exists(final_path)}')
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
os.rmdir_all(chunk_dir) or { println('Failed to clean up chunk directory') }
|
|
||||||
os.rm(final_path) or { println('Failed to clean up final file') }
|
|
||||||
|
|
||||||
println('Test completed successfully!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件扩展名
|
|
||||||
fn get_file_extension(filename string) string {
|
|
||||||
println('[DEBUG] Getting extension for filename: "$filename"')
|
|
||||||
parts := filename.split('.')
|
|
||||||
println('[DEBUG] Split parts: $parts')
|
|
||||||
if parts.len > 1 {
|
|
||||||
ext := '.${parts.last()}'
|
|
||||||
println('[DEBUG] Extracted extension: "$ext"')
|
|
||||||
return ext
|
|
||||||
}
|
|
||||||
println('[DEBUG] No extension found, returning empty string')
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user