feat: add cancel download

This commit is contained in:
putyy
2025-09-13 22:19:42 +08:00
committed by putyy
parent 55d3f06cb6
commit 2d75bbb5c3
10 changed files with 129 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
package core package core
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -48,9 +49,12 @@ type FileDownloader struct {
Headers map[string]string Headers map[string]string
DownloadTaskList []*DownloadTask DownloadTaskList []*DownloadTask
progressCallback ProgressCallback progressCallback ProgressCallback
ctx context.Context
cancelFunc context.CancelFunc
} }
func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader { func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
ctx, cancelFunc := context.WithCancel(context.Background())
return &FileDownloader{ return &FileDownloader{
Url: url, Url: url,
FileName: filename, FileName: filename,
@@ -60,6 +64,8 @@ func NewFileDownloader(url, filename string, totalTasks int, headers map[string]
TotalSize: 0, TotalSize: 0,
Headers: headers, Headers: headers,
DownloadTaskList: make([]*DownloadTask, 0), DownloadTaskList: make([]*DownloadTask, 0),
ctx: ctx,
cancelFunc: cancelFunc,
} }
} }
@@ -271,11 +277,21 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
return return
} }
if strings.Contains(err.Error(), "cancelled") {
errorChan <- err
return
}
task.err = err task.err = err
globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err) globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)
if retries < MaxRetries-1 { if retries < MaxRetries-1 {
time.Sleep(RetryDelay) select {
case <-fd.ctx.Done():
errorChan <- fmt.Errorf("task %d cancelled during retry", task.taskID)
return
case <-time.After(RetryDelay):
}
} }
} }
@@ -283,7 +299,13 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
} }
func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error { func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *DownloadTask) error {
request, err := http.NewRequest("GET", fd.Url, nil) select {
case <-fd.ctx.Done():
return fmt.Errorf("download cancelled")
default:
}
request, err := http.NewRequestWithContext(fd.ctx, "GET", fd.Url, nil)
if err != nil { if err != nil {
return fmt.Errorf("create request failed: %w", err) return fmt.Errorf("create request failed: %w", err)
} }
@@ -310,6 +332,12 @@ func (fd *FileDownloader) doDownloadTask(progressChan chan ProgressChan, task *D
buf := make([]byte, 32*1024) buf := make([]byte, 32*1024)
for { for {
select {
case <-fd.ctx.Done():
return fmt.Errorf("download cancelled")
default:
}
n, err := resp.Body.Read(buf) n, err := resp.Body.Read(buf)
if n > 0 { if n > 0 {
writeSize := int64(n) writeSize := int64(n)
@@ -353,9 +381,9 @@ func (fd *FileDownloader) verifyDownload() error {
return nil return nil
} }
func (fd *FileDownloader) Start() (*FileDownloader, error) { func (fd *FileDownloader) Start() error {
if err := fd.init(); err != nil { if err := fd.init(); err != nil {
return nil, err return err
} }
fd.createDownloadTasks() fd.createDownloadTasks()
@@ -365,5 +393,19 @@ func (fd *FileDownloader) Start() (*FileDownloader, error) {
fd.File.Close() fd.File.Close()
} }
return fd, err return err
}
func (fd *FileDownloader) Cancel() {
if fd.cancelFunc != nil {
fd.cancelFunc()
}
if fd.File != nil {
fd.File.Close()
}
if fd.FileName != "" {
_ = os.Remove(fd.FileName)
}
} }

View File

@@ -345,6 +345,24 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
h.success(w) h.success(w)
} }
func (h *HttpServer) cancel(w http.ResponseWriter, r *http.Request) {
var data struct {
MediaInfo
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.error(w, err.Error())
return
}
err := resourceOnce.cancel(data.Id)
if err != nil {
h.error(w, err.Error())
return
}
h.success(w)
}
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
var data struct { var data struct {
MediaInfo MediaInfo

View File

@@ -56,6 +56,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
httpServerOnce.delete(w, r) httpServerOnce.delete(w, r)
case "/api/download": case "/api/download":
httpServerOnce.download(w, r) httpServerOnce.download(w, r)
case "/api/cancel":
httpServerOnce.cancel(w, r)
case "/api/wx-file-decode": case "/api/wx-file-decode":
httpServerOnce.wxFileDecode(w, r) httpServerOnce.wxFileDecode(w, r)
case "/api/batch-export": case "/api/batch-export":

View File

@@ -3,6 +3,7 @@ package core
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
@@ -22,6 +23,7 @@ type WxFileDecodeResult struct {
type Resource struct { type Resource struct {
mediaMark sync.Map mediaMark sync.Map
tasks sync.Map
resType map[string]bool resType map[string]bool
resTypeMux sync.RWMutex resTypeMux sync.RWMutex
} }
@@ -86,6 +88,15 @@ func (r *Resource) delete(sign string) {
r.mediaMark.Delete(sign) r.mediaMark.Delete(sign)
} }
func (r *Resource) cancel(id string) error {
if d, ok := r.tasks.Load(id); ok {
d.(*FileDownloader).Cancel()
r.tasks.Delete(id) // 可选:取消后清理
return nil
}
return errors.New("task not found")
}
func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) { func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
if globalConfig.SaveDirectory == "" { if globalConfig.SaveDirectory == "" {
return return
@@ -149,10 +160,13 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) { downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning) r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
} }
fd, err := downloader.Start() r.tasks.Store(mediaInfo.Id, downloader)
mediaInfo.SavePath = fd.FileName err := downloader.Start()
mediaInfo.SavePath = downloader.FileName
if err != nil { if err != nil {
r.progressEventsEmit(mediaInfo, err.Error()) if !strings.Contains(err.Error(), "cancelled") {
r.progressEventsEmit(mediaInfo, err.Error())
}
return return
} }
if decodeStr != "" { if decodeStr != "" {

View File

@@ -92,6 +92,13 @@ export default {
data: data data: data
}) })
}, },
cancel(data: object) {
return request({
url: 'api/cancel',
method: 'post',
data: data
})
},
download(data: object) { download(data: object) {
return request({ return request({
url: 'api/download', url: 'api/download',

View File

@@ -23,6 +23,16 @@
</NIcon> </NIcon>
</template> </template>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('cancel')">
<n-icon
size="28"
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors"
>
<CloseOutline/>
</n-icon>
<span class="ml-1">{{ t("index.cancel_down") }}</span>
</div>
<div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('copy')"> <div class="flex items-center justify-start p-1.5 cursor-pointer" @click="action('copy')">
<n-icon <n-icon
size="28" size="28"
@@ -76,6 +86,7 @@ import {
LockOpenSharp, LockOpenSharp,
LinkOutline, LinkOutline,
GridSharp, GridSharp,
CloseOutline,
TrashOutline TrashOutline
} from "@vicons/ionicons5" } from "@vicons/ionicons5"

View File

@@ -18,6 +18,14 @@
<span class="ml-1">{{ t("index.direct_download") }}</span> <span class="ml-1">{{ t("index.direct_download") }}</span>
</div> </div>
<div class="flex items-center justify-start p-1.5">
<n-icon size="28"
class="text-red-500 dark:text-red-300 bg-red-500/20 dark:bg-red-500/30 rounded-full flex items-center justify-center p-1.5 cursor-pointer hover:bg-red-500/40 transition-colors">
<CloseOutline/>
</n-icon>
<span class="ml-1">{{ t("index.cancel_down") }}</span>
</div>
<div class="flex items-center justify-start p-1.5"> <div class="flex items-center justify-start p-1.5">
<n-icon <n-icon
size="28" size="28"
@@ -91,6 +99,7 @@ import {
LinkOutline, LinkOutline,
LockOpenSharp, LockOpenSharp,
GridSharp, GridSharp,
CloseOutline,
TrashOutline TrashOutline
} from "@vicons/ionicons5" } from "@vicons/ionicons5"

View File

@@ -71,6 +71,7 @@
"open_link": "Open Link", "open_link": "Open Link",
"open_file": "Open File", "open_file": "Open File",
"delete_row": "Delete Row", "delete_row": "Delete Row",
"cancel_down": "Cancel Download",
"more_operation": "More Operations", "more_operation": "More Operations",
"video_decode": "WxDecrypt", "video_decode": "WxDecrypt",
"video_decode_loading": "Decrypting", "video_decode_loading": "Decrypting",

View File

@@ -71,6 +71,7 @@
"open_link": "打开链接", "open_link": "打开链接",
"open_file": "打开文件", "open_file": "打开文件",
"delete_row": "删除记录", "delete_row": "删除记录",
"cancel_down": "取消下载",
"more_operation": "更多操作", "more_operation": "更多操作",
"video_decode": "视频解密", "video_decode": "视频解密",
"video_decode_loading": "解密中", "video_decode_loading": "解密中",

View File

@@ -480,6 +480,22 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
case "down": case "down":
download(row, index) download(row, index)
break break
case "cancel":
if (row.Status === "running") {
appApi.cancel({id: row.Id}).then((res)=>{
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
updateItem(row.Id, item => {
item.Status = 'ready'
item.SavePath = ''
})
cacheData()
checkQueue()
})
}
break
case "copy": case "copy":
ClipboardSetText(row.Url).then((is: boolean) => { ClipboardSetText(row.Url).then((is: boolean) => {
if (is) { if (is) {