mirror of
https://github.com/putyy/res-downloader.git
synced 2026-01-12 06:04:55 +08:00
feat: add cancel download
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -48,9 +49,12 @@ type FileDownloader struct {
|
||||
Headers map[string]string
|
||||
DownloadTaskList []*DownloadTask
|
||||
progressCallback ProgressCallback
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewFileDownloader(url, filename string, totalTasks int, headers map[string]string) *FileDownloader {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
return &FileDownloader{
|
||||
Url: url,
|
||||
FileName: filename,
|
||||
@@ -60,6 +64,8 @@ func NewFileDownloader(url, filename string, totalTasks int, headers map[string]
|
||||
TotalSize: 0,
|
||||
Headers: headers,
|
||||
DownloadTaskList: make([]*DownloadTask, 0),
|
||||
ctx: ctx,
|
||||
cancelFunc: cancelFunc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,11 +277,21 @@ func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan cha
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "cancelled") {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
task.err = err
|
||||
globalLogger.Warn().Msgf("Task %d failed (attempt %d/%d): %v", task.taskID, retries+1, MaxRetries, err)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
for {
|
||||
select {
|
||||
case <-fd.ctx.Done():
|
||||
return fmt.Errorf("download cancelled")
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
writeSize := int64(n)
|
||||
@@ -353,9 +381,9 @@ func (fd *FileDownloader) verifyDownload() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fd *FileDownloader) Start() (*FileDownloader, error) {
|
||||
func (fd *FileDownloader) Start() error {
|
||||
if err := fd.init(); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
fd.createDownloadTasks()
|
||||
|
||||
@@ -365,5 +393,19 @@ func (fd *FileDownloader) Start() (*FileDownloader, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
18
core/http.go
18
core/http.go
@@ -345,6 +345,24 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
var data struct {
|
||||
MediaInfo
|
||||
|
||||
@@ -56,6 +56,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpServerOnce.delete(w, r)
|
||||
case "/api/download":
|
||||
httpServerOnce.download(w, r)
|
||||
case "/api/cancel":
|
||||
httpServerOnce.cancel(w, r)
|
||||
case "/api/wx-file-decode":
|
||||
httpServerOnce.wxFileDecode(w, r)
|
||||
case "/api/batch-export":
|
||||
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
@@ -22,6 +23,7 @@ type WxFileDecodeResult struct {
|
||||
|
||||
type Resource struct {
|
||||
mediaMark sync.Map
|
||||
tasks sync.Map
|
||||
resType map[string]bool
|
||||
resTypeMux sync.RWMutex
|
||||
}
|
||||
@@ -86,6 +88,15 @@ func (r *Resource) delete(sign string) {
|
||||
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) {
|
||||
if globalConfig.SaveDirectory == "" {
|
||||
return
|
||||
@@ -149,10 +160,13 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
|
||||
downloader.progressCallback = func(totalDownloaded, totalSize float64, taskID int, taskProgress float64) {
|
||||
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", shared.DownloadStatusRunning)
|
||||
}
|
||||
fd, err := downloader.Start()
|
||||
mediaInfo.SavePath = fd.FileName
|
||||
r.tasks.Store(mediaInfo.Id, downloader)
|
||||
err := downloader.Start()
|
||||
mediaInfo.SavePath = downloader.FileName
|
||||
if err != nil {
|
||||
r.progressEventsEmit(mediaInfo, err.Error())
|
||||
if !strings.Contains(err.Error(), "cancelled") {
|
||||
r.progressEventsEmit(mediaInfo, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if decodeStr != "" {
|
||||
|
||||
@@ -92,6 +92,13 @@ export default {
|
||||
data: data
|
||||
})
|
||||
},
|
||||
cancel(data: object) {
|
||||
return request({
|
||||
url: 'api/cancel',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
},
|
||||
download(data: object) {
|
||||
return request({
|
||||
url: 'api/download',
|
||||
|
||||
@@ -23,6 +23,16 @@
|
||||
</NIcon>
|
||||
</template>
|
||||
<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')">
|
||||
<n-icon
|
||||
size="28"
|
||||
@@ -76,6 +86,7 @@ import {
|
||||
LockOpenSharp,
|
||||
LinkOutline,
|
||||
GridSharp,
|
||||
CloseOutline,
|
||||
TrashOutline
|
||||
} from "@vicons/ionicons5"
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
<span class="ml-1">{{ t("index.direct_download") }}</span>
|
||||
</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">
|
||||
<n-icon
|
||||
size="28"
|
||||
@@ -91,6 +99,7 @@ import {
|
||||
LinkOutline,
|
||||
LockOpenSharp,
|
||||
GridSharp,
|
||||
CloseOutline,
|
||||
TrashOutline
|
||||
} from "@vicons/ionicons5"
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"open_link": "Open Link",
|
||||
"open_file": "Open File",
|
||||
"delete_row": "Delete Row",
|
||||
"cancel_down": "Cancel Download",
|
||||
"more_operation": "More Operations",
|
||||
"video_decode": "WxDecrypt",
|
||||
"video_decode_loading": "Decrypting",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"open_link": "打开链接",
|
||||
"open_file": "打开文件",
|
||||
"delete_row": "删除记录",
|
||||
"cancel_down": "取消下载",
|
||||
"more_operation": "更多操作",
|
||||
"video_decode": "视频解密",
|
||||
"video_decode_loading": "解密中",
|
||||
|
||||
@@ -480,6 +480,22 @@ const dataAction = (row: appType.MediaInfo, index: number, type: string) => {
|
||||
case "down":
|
||||
download(row, index)
|
||||
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":
|
||||
ClipboardSetText(row.Url).then((is: boolean) => {
|
||||
if (is) {
|
||||
|
||||
Reference in New Issue
Block a user