mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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: 'ファイルサイズ(新しい -> 古い)',
|
||||
|
||||
@@ -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: '파일 크기 (새로운 -> 오래된)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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: 'Размер файла (новый -> старый)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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: '子目錄',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user