feat: add stop functionality for wget file downloads (#12245)

#9791
This commit is contained in:
2026-03-20 09:05:54 +00:00
committed by GitHub
parent 59c657e403
commit e635a7beba
16 changed files with 157 additions and 11 deletions
+23
View File
@@ -515,6 +515,29 @@ func (b *BaseApi) WgetFile(c *gin.Context) {
})
}
// @Tags File
// @Summary Stop wget file download
// @Accept json
// @Param request body request.FileProcessReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/stop [post]
// @x-panel-log {"bodyKeys":["key"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"停止下载任务 [key]","formatEN":"Stop wget task [key]"}
func (b *BaseApi) StopWget(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if strings.TrimSpace(req.Key) == "" {
helper.BadRequest(c, errors.New("key is required"))
return
}
files.CancelDownload(req.Key)
helper.Success(c)
}
// @Tags File
// @Summary Move file
// @Accept json
+1
View File
@@ -33,6 +33,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.POST("/chunkupload", baseApi.UploadChunkFiles)
fileRouter.POST("/rename", baseApi.ChangeFileName)
fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/wget/stop", baseApi.StopWget)
fileRouter.POST("/move", baseApi.MoveFile)
fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/chunkdownload", baseApi.DownloadChunkFiles)
+63 -4
View File
@@ -17,6 +17,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -333,6 +334,17 @@ func (f FileOp) Rename(oldName string, newName string) error {
return f.Fs.Rename(oldName, newName)
}
type downloadTask struct {
resp *http.Response
file *os.File
dst string
}
var (
downloadMu sync.Mutex
downloadTasks = make(map[string]*downloadTask)
)
type WriteCounter struct {
Total uint64
Written uint64
@@ -382,37 +394,65 @@ func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate
}
}
defer client.CloseIdleConnections()
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil
}
request.Header.Set("Accept-Encoding", "identity")
resp, err := client.Do(request)
if err != nil {
global.LOG.Errorf("get download file [%s] error, err %s", dst, err.Error())
return err
}
out, err := os.Create(dst)
if err != nil {
global.LOG.Errorf("create download file [%s] error, err %s", dst, err.Error())
resp.Body.Close()
return err
}
downloadMu.Lock()
downloadTasks[key] = &downloadTask{
resp: resp,
file: out,
dst: dst,
}
downloadMu.Unlock()
go func() {
defer func() {
out.Close()
resp.Body.Close()
downloadMu.Lock()
delete(downloadTasks, key)
downloadMu.Unlock()
}()
counter := &WriteCounter{}
counter.Key = key
if resp.ContentLength > 0 {
counter.Total = uint64(resp.ContentLength)
}
counter.Name = filepath.Base(dst)
if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
if _, err := io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
global.LOG.Errorf("save download file [%s] error, err %s", dst, err.Error())
global.CACHE.Del(counter.Key)
return
}
out.Close()
resp.Body.Close()
value := global.CACHE.Get(counter.Key)
if value == "" {
return
}
process := &Process{}
_ = json.Unmarshal([]byte(value), process)
if err := json.Unmarshal([]byte(value), process); err != nil {
return
}
process.Percent = 100
process.Name = counter.Name
process.Total = process.Written
@@ -422,6 +462,25 @@ func (f FileOp) DownloadFileWithProcess(url, dst, key string, ignoreCertificate
return nil
}
func CancelDownload(key string) {
downloadMu.Lock()
task, ok := downloadTasks[key]
if !ok {
downloadMu.Unlock()
return
}
dst := task.dst
downloadMu.Unlock()
_ = task.file.Close()
_ = task.resp.Body.Close()
if dst != "" {
_ = os.Remove(dst)
}
global.CACHE.Del(key)
}
func (f FileOp) DownloadFile(url, dst string) error {
resp, err := req_helper.HandleGet(url)
if err != nil {
+1 -1
View File
@@ -144,7 +144,7 @@ func getDownloadProcess(progress DownloadProgress) (res []byte, err error) {
for _, k := range progress.Keys {
value := global.CACHE.Get(k)
if value == "" {
return nil, fmt.Errorf("get cache error,err value is nil")
continue
}
downloadProcess := &files.Process{}
_ = json.Unmarshal([]byte(value), downloadProcess)
+4
View File
@@ -98,6 +98,10 @@ export const wgetFile = (params: File.FileWget) => {
return http.post<File.FileWgetRes>('files/wget', params);
};
export const stopWgetFile = (key: string) => {
return http.post('files/wget/stop', { key });
};
export const moveFile = (params: File.FileMove) => {
return http.post<File.File>('files/move', params, TimeoutEnum.T_5M);
};
+2
View File
@@ -87,6 +87,7 @@ const message = {
hide: 'Hide',
visit: 'Visit',
migrate: 'Migrate',
tip: 'Tip',
},
operate: {
start: 'Start',
@@ -1647,6 +1648,7 @@ const message = {
uploadSuccess: 'Successfully upload',
downloadProcess: 'Download progress',
downloading: 'Downloading...',
stopWgetConfirm: 'Are you sure you want to stop this download task?',
infoDetail: 'File properties',
root: 'Root directory',
list: 'File list',
+2
View File
@@ -88,6 +88,7 @@ const message = {
migrate: 'Migrar',
disConn: 'Desconectar',
visit: 'Visitar',
tip: 'Aviso',
},
operate: {
start: 'Iniciar',
@@ -1747,6 +1748,7 @@ const message = {
previewLargeFile: 'Vista previa',
panelInstallDir: 'El directorio de instalación de 1Panel no puede eliminarse',
wgetTask: 'Tarea de descarga',
stopWgetConfirm: '¿Confirmar que desea detener esta tarea de descarga?',
existFileTitle: 'Archivo con el mismo nombre',
existFileHelper: 'El archivo cargado contiene un archivo con el mismo nombre, ¿desea sobrescribirlo?',
existFileSize: 'Tamaño del archivo (nuevo -> viejo)',
+2
View File
@@ -88,6 +88,7 @@ const message = {
next: '次へ',
setDefault: 'デフォルトに戻す',
bind: 'バインド',
tip: 'ヒント',
},
operate: {
start: '開始',
@@ -1727,6 +1728,7 @@ const message = {
previewLargeFile: 'プレビュー',
panelInstallDir: '1Panelインストールディレクトリは削除できません',
wgetTask: 'ダウンロードタスク',
stopWgetConfirm: 'このダウンロードタスクを停止しますか',
existFileTitle: '同名ファイルの警告',
existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています上書きしますか',
existFileSize: 'ファイルサイズ新しい -> 古い',
+2
View File
@@ -88,6 +88,7 @@ const message = {
next: '다음',
setDefault: '기본값 복원',
bind: '바인딩',
tip: '안내',
},
operate: {
start: '시작',
@@ -1695,6 +1696,7 @@ const message = {
previewLargeFile: '미리보기',
panelInstallDir: '1Panel 설치 디렉터리는 삭제할 없습니다.',
wgetTask: '다운로드 작업',
stopWgetConfirm: ' 다운로드 작업을 중지하시겠습니까?',
existFileTitle: '동일한 이름의 파일 경고',
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
existFileSize: '파일 크기 (새로운 -> 오래된)',
+2
View File
@@ -88,6 +88,7 @@ const message = {
next: 'Seterusnya',
setDefault: 'Pulihkan lalai',
bind: 'Ikat',
tip: 'Tip',
},
operate: {
start: 'Mula',
@@ -1750,6 +1751,7 @@ const message = {
previewLargeFile: 'Pratonton',
panelInstallDir: 'Direktori pemasangan 1Panel tidak boleh dipadamkan',
wgetTask: 'Tugas Muat Turun',
stopWgetConfirm: 'Adakah anda pasti mahu menghentikan tugas muat turun ini?',
existFileTitle: 'Amaran fail dengan nama yang sama',
existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?',
existFileSize: 'Saiz fail (baru -> lama)',
+2
View File
@@ -88,6 +88,7 @@ const message = {
next: 'Próximo',
setDefault: 'Restaurar padrão',
bind: 'Vincular',
tip: 'Dica',
},
operate: {
start: 'Iniciar',
@@ -1866,6 +1867,7 @@ const message = {
previewLargeFile: 'Visualizar',
panelInstallDir: 'O diretório de instalação do 1Panel não pode ser excluído',
wgetTask: 'Tarefa de Download',
stopWgetConfirm: 'Tem certeza de que deseja parar esta tarefa de download?',
existFileTitle: 'Aviso de arquivo com o mesmo nome',
existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?',
existFileSize: 'Tamanho do arquivo (novo -> antigo)',
+2
View File
@@ -88,6 +88,7 @@ const message = {
next: 'Далее',
setDefault: 'Сбросить по умолчанию',
bind: 'Привязать',
tip: 'Подсказка',
},
operate: {
start: 'Запустить',
@@ -1738,6 +1739,7 @@ const message = {
previewLargeFile: 'Предпросмотр',
panelInstallDir: 'Директорию установки 1Panel нельзя удалить',
wgetTask: 'Задача загрузки',
stopWgetConfirm: 'Вы уверены, что хотите остановить эту задачу загрузки?',
existFileTitle: 'Предупреждение о файле с тем же именем',
existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?',
existFileSize: 'Размер файла (новый -> старый)',
+2
View File
@@ -88,6 +88,7 @@ const message = {
visit: 'Visit',
migrate: 'Taşı',
disConn: 'Bağlantıyı kes',
tip: 'İpucu',
},
operate: {
start: 'Başlat',
@@ -1743,6 +1744,7 @@ const message = {
previewLargeFile: 'Önizleme',
panelInstallDir: '1Panel kurulum dizini silinemez',
wgetTask: 'İndirme Görevi',
stopWgetConfirm: 'Bu indirme görevini durdurmak istediğinizden emin misiniz?',
existFileTitle: 'Aynı ada sahip dosya uyarısı',
existFileHelper: 'Yüklenen dosya, aynı ada sahip bir dosya içeriyor, üzerine yazmak istiyor musunuz?',
existFileSize: 'Dosya boyutu (yeni -> eski)',
+2
View File
@@ -87,6 +87,7 @@ const message = {
hide: '隱藏',
visit: '存取',
migrate: '遷移',
tip: '提示',
},
operate: {
start: '啟動',
@@ -1557,6 +1558,7 @@ const message = {
downloadProcess: '下載進度',
downloading: '正在下載...',
infoDetail: '檔案屬性',
stopWgetConfirm: '確認停止該下載任務',
root: '根目錄',
list: '檔案列表',
sub: '子目錄',
+2
View File
@@ -87,6 +87,7 @@ const message = {
hide: '隐藏',
visit: '访问',
migrate: '迁移',
tip: '提示',
},
operate: {
start: '启动',
@@ -1557,6 +1558,7 @@ const message = {
uploadSuccess: '上传成功!',
downloadProcess: '下载进度',
downloading: '正在下载...',
stopWgetConfirm: '确认停止该下载任务',
infoDetail: '文件属性',
root: '根目录',
list: '文件列表',
@@ -10,7 +10,7 @@
>
<div class="flex items-center gap-3">
<div class="flex-1">
<MsgInfo :info="value.name" class="text-gray-700" />
<MsgInfo :info="value.name" :width="300" class="text-gray-700" />
<div class="text-gray-500">
{{ value.percent === 100 ? $t('file.downloadSuccess') : $t('file.downloading') }}
</div>
@@ -18,9 +18,22 @@
</div>
<div class="space-y-2">
<div class="flex justify-end text-gray-500 mb-1">
<span>{{ getFileSize(value.written) }}</span>
<span v-if="value.total > 0" class="text-gray-400">/{{ getFileSize(value.total) }}</span>
<div class="flex justify-between items-center mb-1 text-gray-500">
<div>
<span>{{ getFileSize(value.written) }}</span>
<span v-if="value.total > 0" class="text-gray-400">
/{{ getFileSize(value.total) }}
</span>
</div>
<el-button
v-if="value.percent !== 100"
link
type="danger"
size="small"
@click="onStop(index)"
>
{{ $t('commons.button.stop') }}
</el-button>
</div>
<div class="w-full">
<el-progress
@@ -48,11 +61,14 @@
</template>
<script lang="ts" setup>
import { fileWgetKeys } from '@/api/modules/files';
import { fileWgetKeys, stopWgetFile } from '@/api/modules/files';
import { computeSize } from '@/utils/util';
import { onBeforeUnmount, ref } from 'vue';
import MsgInfo from '@/components/msg-info/index.vue';
import { GlobalStore } from '@/store';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const globalStore = GlobalStore();
let processSocket = ref(null) as unknown as WebSocket;
@@ -131,6 +147,29 @@ const getFileSize = (size: number) => {
return computeSize(size);
};
const onStop = async (index: number) => {
const key = keys.value[index];
if (!key) return;
try {
await ElMessageBox.confirm(i18n.global.t('file.stopWgetConfirm'), i18n.global.t('commons.button.tip'), {
type: 'warning',
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
});
} catch {
return;
}
try {
await stopWgetFile(key);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
keys.value = keys.value.filter((_, i) => i !== index);
res.value = res.value.filter((_, i) => i !== index);
if (keys.value.length === 0 || res.value.length === 0) {
handleClose();
}
} catch (e) {}
};
onBeforeUnmount(() => {
closeSocket();
});
@@ -143,7 +182,7 @@ const acceptParams = () => {
defineExpose({ acceptParams });
</script>
<style type="scss" scoped>
<style lang="scss" scoped>
.download-item.completed {
@apply bg-green-50/50;
}