fix: resolve critical issues and improve task management

- Fix path traversal vulnerability in downloadVideo handler by adding
  download directory whitelist validation
- Add graceful shutdown with signal handling for task persistence
- Fix division by zero panic in progressBar.percent() when total <= 0
- Add GetDB() function that returns error instead of using log.Fatal
- Change deleteTask to only remove database records, preserve downloaded files
- Add paused status for interrupted tasks on shutdown

Co-Authored-By: Claude
This commit is contained in:
2026-04-09 16:51:22 +08:00
parent 449b5fe3a2
commit a0ef988bd5
5 changed files with 125 additions and 47 deletions
+53 -9
View File
@@ -1,15 +1,20 @@
package main
import (
"context"
"database/sql"
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"bilidown/internal/logger"
"bilidown/internal/router"
"bilidown/internal/task"
"bilidown/internal/util"
_ "modernc.org/sqlite"
@@ -21,11 +26,12 @@ var staticFiles embed.FS
const (
HTTP_PORT = 8098 // HTTP 服务器端口
HTTP_HOST = "" // HTTP 服务器主机
VERSION = "v2.1.1" // 软件版本号
VERSION = "v2.1.2" // 软件版本号
)
var urlLocal = fmt.Sprintf("http://127.0.0.1:%d", HTTP_PORT)
var ffmpegAvailable bool
var server *http.Server
func main() {
ffmpegAvailable = checkFFmpeg()
@@ -33,7 +39,14 @@ func main() {
mustInitTables()
mustRunServer()
logger.ServerStarted(HTTP_PORT, VERSION)
select {} // 保持运行
// 等待中断信号,实现优雅退出
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("正在关闭服务...")
gracefulShutdown()
}
// checkFFmpeg 检测 ffmpeg 的安装情况,返回是否可用
@@ -59,14 +72,44 @@ func mustRunServer() {
})
http.Handle("/api/", http.StripPrefix("/api", apiRouter))
// 启动 HTTP 服务器
server = &http.Server{
Addr: fmt.Sprintf("%s:%d", HTTP_HOST, HTTP_PORT),
Handler: nil,
}
go func() {
err := http.ListenAndServe(fmt.Sprintf("%s:%d", HTTP_HOST, HTTP_PORT), nil)
if err != nil {
log.Fatal("http.ListenAndServe:", err)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("http.ListenAndServe: " + err.Error())
}
}()
}
// gracefulShutdown 优雅退出:保存任务状态并关闭服务
func gracefulShutdown() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 等待活跃任务完成或超时
task.GlobalTaskMux.Lock()
activeCount := len(task.GlobalTaskList)
task.GlobalTaskMux.Unlock()
if activeCount > 0 {
logger.Infof("等待 %d 个活跃任务完成(最多10秒)...", activeCount)
// 标记所有运行中的任务为暂停状态
task.MarkAllTasksPaused()
// 等待下载信号量释放
task.GlobalDownloadSem.Wait()
task.GlobalMergeSem.Wait()
}
// 关闭 HTTP 服务器
if err := server.Shutdown(ctx); err != nil {
logger.Error("服务器关闭失败: " + err.Error())
}
logger.Info("服务已关闭")
}
// mustInitTables 初始化数据表
func mustInitTables() {
db := util.MustGetDB()
@@ -129,17 +172,18 @@ func addMissingColumns(db *sql.DB) error {
return nil
}
// initHistoryTask 将上一次程序运行时未完成的任务状态变为 error
// initHistoryTask 将上一次程序运行时未完成的任务状态变为 paused,支持恢复
func initHistoryTask(db *sql.DB) error {
util.SqliteLock.Lock()
result, err := db.Exec(`UPDATE "task" SET "status" = 'error' WHERE "status" IN ('waiting', 'running')`)
// 将 waitingrunning 状态改为 paused,而非 error
result, err := db.Exec(`UPDATE "task" SET "status" = 'paused' WHERE "status" IN ('waiting', 'running')`)
util.SqliteLock.Unlock()
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
logger.Infof("重置 %d 个未完成任务状态为 error", rowsAffected)
logger.Infof("发现 %d 个未完成任务状态已设为 paused", rowsAffected)
}
return nil
}