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
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
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
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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 != "" {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "解密中",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user