mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
@@ -901,3 +901,44 @@ func (b *BaseApi) GetUsersAndGroups(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Convert file
|
||||
// @Accept json
|
||||
// @Param request body request.FileConvert true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/convert [post]
|
||||
func (b *BaseApi) ConvertFile(c *gin.Context) {
|
||||
var req request.FileConvertRequest
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
fileService.Convert(req)
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Convert file
|
||||
// @Accept json
|
||||
// @Param request body dto.PageInfo true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/convert/log [post]
|
||||
func (b *BaseApi) ConvertLog(c *gin.Context) {
|
||||
var req dto.PageInfo
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, logs, err := fileService.ConvertLog(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: logs,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,3 +148,19 @@ type FileExistReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Dir string `json:"dir" validate:"required"`
|
||||
}
|
||||
|
||||
type FileConvert struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
InputFile string `json:"inputFile" validate:"required"`
|
||||
Extension string `json:"extension" validate:"required"`
|
||||
OutputFormat string `json:"outputFormat" validate:"required"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type FileConvertRequest struct {
|
||||
Files []FileConvert `json:"files" validate:"required"`
|
||||
OutputPath string `json:"outputPath" validate:"required"`
|
||||
DeleteSource bool `json:"deleteSource"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
@@ -73,3 +73,11 @@ type DepthDirSizeRes struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type FileConvertLog struct {
|
||||
Date string `json:"date"`
|
||||
Type string `json:"type"`
|
||||
Log string `json:"log"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ package service
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/convert"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -66,6 +70,8 @@ type IFileService interface {
|
||||
BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo
|
||||
GetHostMount() []dto.DiskInfo
|
||||
GetUsersAndGroups() (*response.UserGroupResponse, error)
|
||||
Convert(req request.FileConvertRequest)
|
||||
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
|
||||
}
|
||||
|
||||
var filteredPaths = []string{
|
||||
@@ -723,3 +729,102 @@ func getValidUsers(validGroups map[string]bool) ([]response.UserInfo, map[string
|
||||
}
|
||||
return users, groupSet, nil
|
||||
}
|
||||
|
||||
func (f *FileService) Convert(req request.FileConvertRequest) {
|
||||
convertTask, err := task.NewTaskWithOps(i18n.GetMsgByKey("FileConvert"), task.TaskExec, task.TaskScopeFileConvert, req.TaskID, 1)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Create convert task failed %v", err)
|
||||
return
|
||||
}
|
||||
convertTask.AddSubTask(task.GetTaskName(i18n.GetMsgByKey("FileConvert"), task.TaskExec, task.TaskScopeFileConvert), func(t *task.Task) (err error) {
|
||||
for _, file := range req.Files {
|
||||
input := filepath.Join(file.Path, file.InputFile)
|
||||
nameOnly := file.InputFile[0 : len(file.InputFile)-len(file.Extension)]
|
||||
output := filepath.Join(req.OutputPath, nameOnly+"."+file.OutputFormat)
|
||||
status, errMsg := convert.MediaFile(input, output, file.OutputFormat, req.DeleteSource)
|
||||
if status == "FAILED" {
|
||||
convertTask.Log(fmt.Sprintf("%s -> %s [%s]: %s\n",
|
||||
input, output, status, errMsg))
|
||||
} else {
|
||||
convertTask.Log(fmt.Sprintf("%s -> %s [%s]: %s\n",
|
||||
input, output, status, "SUCCESS"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
go func() {
|
||||
_ = convertTask.Execute()
|
||||
}()
|
||||
}
|
||||
|
||||
func (f *FileService) ConvertLog(req dto.PageInfo) (total int64, data []response.FileConvertLog, err error) {
|
||||
logFilePath := filepath.Join(global.Dir.ConvertLogDir, "convert.log")
|
||||
file, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
size := stat.Size()
|
||||
|
||||
buffer := make([]byte, 1)
|
||||
var line strings.Builder
|
||||
var lines []response.FileConvertLog
|
||||
var lineCount int64
|
||||
|
||||
skipCount := int64((req.Page - 1) * req.PageSize)
|
||||
|
||||
for offset := size - 1; offset >= 0; offset-- {
|
||||
_, err := file.ReadAt(buffer, offset)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if buffer[0] == '\n' {
|
||||
text := reverse(line.String())
|
||||
line.Reset()
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var entry response.FileConvertLog
|
||||
if err := json.Unmarshal([]byte(text), &entry); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if lineCount >= skipCount && len(lines) < req.PageSize {
|
||||
lines = append(lines, entry)
|
||||
}
|
||||
lineCount++
|
||||
} else {
|
||||
line.WriteByte(buffer[0])
|
||||
}
|
||||
}
|
||||
|
||||
if line.Len() > 0 {
|
||||
text := reverse(line.String())
|
||||
var entry response.FileConvertLog
|
||||
if err := json.Unmarshal([]byte(text), &entry); err == nil {
|
||||
if lineCount >= skipCount && len(lines) < req.PageSize {
|
||||
lines = append(lines, entry)
|
||||
}
|
||||
lineCount++
|
||||
}
|
||||
}
|
||||
|
||||
total = lineCount
|
||||
data = lines
|
||||
return total, data, nil
|
||||
}
|
||||
|
||||
func reverse(s string) string {
|
||||
runes := []rune(s)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ const (
|
||||
TaskExec = "TaskExec"
|
||||
TaskBatch = "TaskBatch"
|
||||
TaskProtect = "TaskProtect"
|
||||
TaskConvert = "TaskConvert"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -93,6 +94,7 @@ const (
|
||||
TaskScopeRuntimeExtension = "RuntimeExtension"
|
||||
TaskScopeCustomAppstore = "CustomAppstore"
|
||||
TaskScopeTamper = "Tamper"
|
||||
TaskScopeFileConvert = "Convert"
|
||||
)
|
||||
|
||||
func GetTaskName(resourceName, operate, scope string) string {
|
||||
|
||||
@@ -44,6 +44,7 @@ type SystemDir struct {
|
||||
RecycleBinDir string
|
||||
SSLLogDir string
|
||||
McpDir string
|
||||
ConvertLogDir string
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
|
||||
@@ -369,6 +369,7 @@ TaskIsExecuting: 'Task is running'
|
||||
CustomAppstore: 'Custom application warehouse'
|
||||
TaskExec: 'Execute'
|
||||
TaskBatch: "Batch Operation"
|
||||
FileConvert: 'File Conversion'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "Scan {{ .name }}"
|
||||
|
||||
@@ -368,6 +368,7 @@ CustomAppstore: 'Almacén de aplicaciones personalizado'
|
||||
TaskClean: "Limpieza"
|
||||
TaskExec: "Ejecutar"
|
||||
TaskBatch: "Operación por Lotes"
|
||||
FileConvert: 'Conversión de Formato de Archivo'
|
||||
|
||||
# task - ai
|
||||
OllamaModelPull: 'Descargar modelo Ollama {{ .name }}'
|
||||
|
||||
@@ -368,6 +368,7 @@ TaskIsExecuting: 'タスクは実行中です'
|
||||
CustomAppstore: 'カスタム アプリケーション ウェアハウス'
|
||||
TaskExec: '実行'
|
||||
TaskBatch: "一括操作"
|
||||
FileConvert: 'ファイル形式の変換'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "{{ .name }} をスキャン"
|
||||
|
||||
@@ -369,6 +369,7 @@ TaskIsExecuting: '작업이 실행 중입니다'
|
||||
CustomAppstore: '사용자 정의 애플리케이션 웨어하우스'
|
||||
TaskExec: '실행'
|
||||
TaskBatch: "일괄 작업"
|
||||
FileConvert: '파일 형식 변환'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "{{ .name }} 스캔"
|
||||
|
||||
@@ -369,6 +369,7 @@ TaskIsExecuting: 'Tugas sedang berjalan'
|
||||
CustomAppstore: 'Gudang aplikasi tersuai'
|
||||
TaskExec: 'Laksanakan'
|
||||
TaskBatch: "Operasi Batch"
|
||||
FileConvert: 'Penukaran Format Fail'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "Imbas {{ .name }}"
|
||||
|
||||
@@ -369,6 +369,7 @@ TaskIsExecuting: 'A tarefa está em execução'
|
||||
CustomAppstore: 'Armazém de aplicativos personalizados'
|
||||
TaskExec: 'Executar'
|
||||
TaskBatch: "Operação em Lote"
|
||||
FileConvert: 'Conversão de Formato de Arquivo'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "Escanear {{ .name }}"
|
||||
|
||||
@@ -369,6 +369,7 @@ TaskIsExecuting: 'Задача выполняется'
|
||||
CustomAppstore: 'Хранилище пользовательских приложений'
|
||||
TaskExec: 'Выполнить'
|
||||
TaskBatch: "Пакетная операция"
|
||||
FileConvert: 'Преобразование формата файла'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "Сканировать {{ .name }}"
|
||||
|
||||
@@ -370,6 +370,7 @@ TaskIsExecuting: 'Görev çalışıyor'
|
||||
CustomAppstore: 'Özel uygulama deposu'
|
||||
TaskExec: 'Çalıştır'
|
||||
TaskBatch: "Toplu İşlem"
|
||||
FileConvert: 'Dosya Formatı Dönüştürme'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "{{ .name }} Tara"
|
||||
|
||||
@@ -368,6 +368,7 @@ TaskIsExecuting: '任務正在運作'
|
||||
CustomAppstore: '自訂應用程式倉庫'
|
||||
TaskExec: '執行'
|
||||
TaskBatch: "批量操作"
|
||||
FileConvert: '文件格式轉換'
|
||||
|
||||
# task - clam
|
||||
Clamscan: "掃描 {{ .name }}"
|
||||
|
||||
@@ -95,7 +95,7 @@ ErrAppVersionDeprecated: " {{ .name }} 应用不适配当前 1Panel 版本,跳
|
||||
ErrDockerFailed: "Docker 状态异常,请检查服务状态"
|
||||
ErrDockerComposeCmdNotFound: "Docker Compose 命令不存在,请先在宿主机安装此命令"
|
||||
|
||||
#ssh
|
||||
#ssh
|
||||
ExportIP: "登录 IP"
|
||||
ExportArea: "归属地"
|
||||
ExportPort: "端口"
|
||||
@@ -369,6 +369,7 @@ TaskIsExecuting: "任务正在运行"
|
||||
CustomAppstore: "自定义应用仓库"
|
||||
TaskExec: "执行"
|
||||
TaskBatch: "批量操作"
|
||||
FileConvert: "文件格式转换"
|
||||
|
||||
# task - clam
|
||||
Clamscan: "扫描 {{ .name }}"
|
||||
|
||||
@@ -31,4 +31,5 @@ func Init() {
|
||||
global.Dir.RecycleBinDir, _ = fileOp.CreateDirWithPath(true, "/.1panel_clash")
|
||||
global.Dir.SSLLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/ssl"))
|
||||
global.Dir.McpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/mcp"))
|
||||
global.Dir.ConvertLogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/convert"))
|
||||
}
|
||||
|
||||
@@ -52,5 +52,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
fileRouter.GET("/path/:type", baseApi.GetPathByType)
|
||||
fileRouter.POST("/mount", baseApi.GetHostMount)
|
||||
fileRouter.POST("/user/group", baseApi.GetUsersAndGroups)
|
||||
fileRouter.POST("/convert", baseApi.ConvertFile)
|
||||
fileRouter.POST("/convert/log", baseApi.ConvertLog)
|
||||
}
|
||||
}
|
||||
|
||||
181
agent/utils/convert/convert.go
Normal file
181
agent/utils/convert/convert.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FormatOption struct {
|
||||
Type string
|
||||
Codec string
|
||||
}
|
||||
|
||||
var FormatMap = map[string]FormatOption{
|
||||
// images
|
||||
"png": {Type: "image", Codec: "png"},
|
||||
"jpg": {Type: "image", Codec: "mjpeg"},
|
||||
"jpeg": {Type: "image", Codec: "mjpeg"},
|
||||
"webp": {Type: "image", Codec: "libwebp"},
|
||||
"gif": {Type: "image", Codec: "gif"},
|
||||
"bmp": {Type: "image", Codec: "bmp"},
|
||||
"tiff": {Type: "image", Codec: "tiff"},
|
||||
|
||||
// videos
|
||||
"mp4": {Type: "video", Codec: "libx264"},
|
||||
"avi": {Type: "video", Codec: "libx264"},
|
||||
"mov": {Type: "video", Codec: "libx264"},
|
||||
"mkv": {Type: "video", Codec: "libx264"},
|
||||
|
||||
// audios
|
||||
"mp3": {Type: "audio", Codec: "libmp3lame"},
|
||||
"wav": {Type: "audio", Codec: "pcm_s16le"},
|
||||
"flac": {Type: "audio", Codec: "flac"},
|
||||
"aac": {Type: "audio", Codec: "aac"},
|
||||
}
|
||||
|
||||
func hasFfmpeg() (string, bool) {
|
||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||
return ffmpegPath, err == nil
|
||||
}
|
||||
|
||||
func MediaFile(inputFile, outputFile, outputFormat string, deleteSource bool) (state string, err error) {
|
||||
status := "FAILED"
|
||||
msg := ""
|
||||
ffmpegPath, flag := hasFfmpeg()
|
||||
if !flag {
|
||||
return status, fmt.Errorf("ffmpeg not found, cannot convert file")
|
||||
}
|
||||
logFile, logErr := os.OpenFile(filepath.Join(global.Dir.ConvertLogDir, "convert.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
allLogFile, allErr := os.OpenFile(filepath.Join(global.Dir.ConvertLogDir, "convert-all.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
|
||||
if logErr != nil || allErr != nil {
|
||||
return status, fmt.Errorf("cannot open log file: %w", err)
|
||||
}
|
||||
defer logFile.Close()
|
||||
args, fileType, err := buildFFmpegArgs(inputFile, outputFile, outputFormat)
|
||||
if err != nil {
|
||||
return status, fmt.Errorf("FFmpeg args failed: %w", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
cmdr := exec.CommandContext(ctx, ffmpegPath, args...)
|
||||
cmdr.Env = append(os.Environ(),
|
||||
"PATH="+filepath.Dir(ffmpegPath)+":"+os.Getenv("PATH"),
|
||||
"LD_LIBRARY_PATH=/usr/local/lib:"+os.Getenv("LD_LIBRARY_PATH"),
|
||||
)
|
||||
var buf bytes.Buffer
|
||||
cmdr.Stdout = &buf
|
||||
cmdr.Stderr = &buf
|
||||
err = cmdr.Run()
|
||||
logStr := buf.String()
|
||||
|
||||
stat, statErr := os.Stat(outputFile)
|
||||
if err != nil || statErr != nil || stat.Size() == 0 {
|
||||
status = "FAILED"
|
||||
msg = extractFFmpegError(logStr)
|
||||
_ = os.Remove(outputFile)
|
||||
} else {
|
||||
status = "SUCCESS"
|
||||
msg = "SUCCESS"
|
||||
}
|
||||
|
||||
entry := response.FileConvertLog{
|
||||
Date: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Type: fileType,
|
||||
Log: fmt.Sprintf("%s -> %s", inputFile, outputFile),
|
||||
Status: status,
|
||||
Message: msg,
|
||||
}
|
||||
_ = appendJSONLog(logFile, entry)
|
||||
|
||||
allLogEntry := fmt.Sprintf("[%s] %s %s -> %s [%s]: %s\n",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
fileType, inputFile, outputFile, status, logStr)
|
||||
_ = appendLog(allLogFile, allLogEntry)
|
||||
if err == nil && deleteSource {
|
||||
_ = os.Remove(inputFile)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func buildFFmpegArgs(inputFile, outputFile, outputFormat string) ([]string, string, error) {
|
||||
args := []string{"-y", "-i", inputFile}
|
||||
opt, ok := FormatMap[outputFormat]
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("unsupported format: %s", outputFormat)
|
||||
}
|
||||
|
||||
switch opt.Type {
|
||||
case "image":
|
||||
switch outputFormat {
|
||||
case "webp":
|
||||
args = append(args, "-c:v", "libwebp", "-lossless", "0", "-q:v", "75")
|
||||
case "png", "gif", "jpg", "jpeg", "bmp", "tiff":
|
||||
args = append(args, "-c:v", opt.Codec)
|
||||
}
|
||||
|
||||
case "video":
|
||||
args = append(args, "-c:v", opt.Codec, "-preset", "fast", "-crf", "23", "-c:a", "aac", "-b:a", "192k")
|
||||
|
||||
case "audio":
|
||||
args = append(args, "-c:a", opt.Codec, "-b:a", "192k")
|
||||
|
||||
default:
|
||||
return nil, opt.Type, fmt.Errorf("unsupported media type: %s", opt.Type)
|
||||
}
|
||||
|
||||
args = append(args, outputFile)
|
||||
return args, opt.Type, nil
|
||||
}
|
||||
|
||||
func appendLog(f *os.File, content string) error {
|
||||
_, err := f.WriteString(content)
|
||||
return err
|
||||
}
|
||||
|
||||
func appendJSONLog(f *os.File, entry response.FileConvertLog) error {
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString(string(data) + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFFmpegError(logStr string) string {
|
||||
priority := []string{"Error", "Invalid", "failed", "No "}
|
||||
matches := make(map[string]string)
|
||||
lines := strings.Split(strings.TrimSpace(logStr), "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
for _, kw := range priority {
|
||||
if _, ok := matches[kw]; !ok && strings.Contains(line, kw) {
|
||||
matches[kw] = line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, kw := range priority {
|
||||
if line, ok := matches[kw]; ok {
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) > 0 {
|
||||
return lines[len(lines)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const i18nLocale = computed(() => {
|
||||
if (globalStore.language === 'pt-br') return ptBR;
|
||||
if (globalStore.language === 'ko') return ko;
|
||||
if (globalStore.language === 'tr') return tr;
|
||||
if (globalStore.language === 'es-es') return esES;
|
||||
if (globalStore.language === 'es-ES') return esES;
|
||||
return zhCn;
|
||||
});
|
||||
|
||||
|
||||
@@ -222,4 +222,27 @@ export namespace File {
|
||||
username: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
export interface ConvertFile {
|
||||
type: string;
|
||||
path: string;
|
||||
extension: string;
|
||||
inputFile: string;
|
||||
outputFormat: string;
|
||||
}
|
||||
|
||||
export interface ConvertFileRequest {
|
||||
files: ConvertFile[];
|
||||
outputPath: string;
|
||||
deleteSource: boolean;
|
||||
taskID: string;
|
||||
}
|
||||
|
||||
export interface ConvertLogResponse {
|
||||
date: string;
|
||||
type: string;
|
||||
log: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,3 +158,11 @@ export const searchHostMount = () => {
|
||||
export const searchUserGroup = () => {
|
||||
return http.post<File.UserGroupResponse>(`/files/user/group`);
|
||||
};
|
||||
|
||||
export const convertFiles = (params: File.ConvertFileRequest) => {
|
||||
return http.post<File.ConvertFile>('files/convert', params, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
export const convertLogs = (params: ReqPage) => {
|
||||
return http.post<ResPage<File.ConvertLogResponse>>('files/convert/log', params, TimeoutEnum.T_5M);
|
||||
};
|
||||
|
||||
@@ -223,6 +223,7 @@ function handleRowClick(row: any, column: any, event: any) {
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('.el-switch') ||
|
||||
target.closest('.el-select') ||
|
||||
target.closest('.table-link') ||
|
||||
target.closest('.cursor-pointer')
|
||||
) {
|
||||
|
||||
@@ -268,5 +268,10 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
:deep(.xterm) {
|
||||
padding: 5px !important;
|
||||
background-color: var(--panel-logs-bg-color) !important;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport) {
|
||||
background-color: var(--panel-logs-bg-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1549,6 +1549,19 @@ const message = {
|
||||
cancelUploadHelper: 'Whether to cancel the upload, after cancellation the upload list will be cleared.',
|
||||
keepOneTab: 'Keep at least one tab',
|
||||
notCanTab: 'Cannot add more tabs',
|
||||
convert: 'Convert Format',
|
||||
converting: 'Convert To',
|
||||
fileCanNotConvert: 'This file does not support format conversion',
|
||||
formatType: 'Format Type',
|
||||
sourceFormat: 'Source Format',
|
||||
sourceFile: 'Source File',
|
||||
saveDir: 'Save Directory',
|
||||
deleteSourceFile: 'Delete Source File',
|
||||
convertHelper: 'Convert the selected files to another format',
|
||||
convertHelper1: 'Please select the files to be converted',
|
||||
execConvert: 'Start conversion. You can view the conversion logs in the Task Center',
|
||||
convertLogs: 'Conversion Logs',
|
||||
formatConvert: 'Format Conversion',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Auto start',
|
||||
|
||||
@@ -1545,6 +1545,19 @@ const message = {
|
||||
cancelUploadHelper: 'Indica si se cancela la carga; después de la cancelación, la lista de cargas se borrará.',
|
||||
keepOneTab: 'Mantener al menos una pestaña',
|
||||
notCanTab: 'No se pueden añadir más pestañas',
|
||||
convert: 'Convertir Formato',
|
||||
converting: 'Convirtiendo',
|
||||
fileCanNotConvert: 'Este archivo no admite conversión de formato',
|
||||
formatType: 'Tipo de Formato',
|
||||
sourceFormat: 'Formato de Origen',
|
||||
sourceFile: 'Archivo de Origen',
|
||||
saveDir: 'Directorio de Guardado',
|
||||
deleteSourceFile: 'Eliminar Archivo de Origen',
|
||||
convertHelper: 'Convertir los archivos seleccionados a otro formato',
|
||||
convertHelper1: 'Por favor, seleccione los archivos a convertir',
|
||||
execConvert: 'Iniciar conversión. Puede ver los registros de conversión en el Centro de Tareas',
|
||||
convertLogs: 'Registros de Conversión',
|
||||
formatConvert: 'Conversión de Formato',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Inicio automático',
|
||||
|
||||
@@ -1494,6 +1494,19 @@ const message = {
|
||||
cancelUploadHelper: 'アップロードをキャンセルするかどうか、キャンセル後、アップロードリストはクリアされます。',
|
||||
keepOneTab: '少なくとも1つのタブを保持してください',
|
||||
notCanTab: 'これ以上タブを追加できません',
|
||||
convert: 'フォーマットを変換',
|
||||
converting: 'に変換中',
|
||||
fileCanNotConvert: 'このファイルはフォーマット変換に対応していません',
|
||||
formatType: 'フォーマットタイプ',
|
||||
sourceFormat: '元のフォーマット',
|
||||
sourceFile: '元ファイル',
|
||||
saveDir: '保存ディレクトリ',
|
||||
deleteSourceFile: '元ファイルを削除するかどうか',
|
||||
convertHelper: '選択したファイルを別のフォーマットに変換します',
|
||||
convertHelper1: '変換するファイルを選択してください',
|
||||
execConvert: '変換を開始します。タスクセンターで変換ログを確認できます',
|
||||
convertLogs: '変換ログ',
|
||||
formatConvert: 'フォーマット変換',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'オートスタート',
|
||||
|
||||
@@ -1478,6 +1478,19 @@ const message = {
|
||||
cancelUploadHelper: '업로드를 취소할지 여부, 취소 후 업로드 목록이 비워집니다.',
|
||||
keepOneTab: '최소한 하나의 탭을 유지하세요',
|
||||
notCanTab: '더 이상 탭을 추가할 수 없습니다',
|
||||
convert: '파일 형식 변환',
|
||||
converting: '로 변환 중',
|
||||
fileCanNotConvert: '이 파일은 형식 변환을 지원하지 않습니다',
|
||||
formatType: '형식 종류',
|
||||
sourceFormat: '원본 형식',
|
||||
sourceFile: '원본 파일',
|
||||
saveDir: '저장 디렉토리',
|
||||
deleteSourceFile: '원본 파일 삭제 여부',
|
||||
convertHelper: '선택한 파일을 다른 형식으로 변환합니다',
|
||||
convertHelper1: '변환할 파일을 선택하세요',
|
||||
execConvert: '변환을 시작합니다. 작업 센터에서 변환 로그를 확인할 수 있습니다',
|
||||
convertLogs: '변환 로그',
|
||||
formatConvert: '형식 변환',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '자동 시작',
|
||||
|
||||
@@ -1537,6 +1537,19 @@ const message = {
|
||||
'Adakah hendak membatalkan muat naik, selepas pembatalan senarai muat naik akan dikosongkan.',
|
||||
keepOneTab: 'Pastikan sekurang-kurangnya satu tab dikekalkan',
|
||||
notCanTab: 'Tidak dapat menambah tab lagi',
|
||||
convert: 'Tukar Format',
|
||||
converting: 'Menukar Ke',
|
||||
fileCanNotConvert: 'Fail ini tidak menyokong penukaran format',
|
||||
formatType: 'Jenis Format',
|
||||
sourceFormat: 'Format Asal',
|
||||
sourceFile: 'Fail Asal',
|
||||
saveDir: 'Direktori Simpanan',
|
||||
deleteSourceFile: 'Padam Fail Asal',
|
||||
convertHelper: 'Tukar fail yang dipilih ke format lain',
|
||||
convertHelper1: 'Sila pilih fail yang hendak ditukar',
|
||||
execConvert: 'Mulakan penukaran. Anda boleh melihat log penukaran di Pusat Tugas',
|
||||
convertLogs: 'Log Penukaran',
|
||||
formatConvert: 'Penukaran Format',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Mula automatik',
|
||||
|
||||
@@ -1527,6 +1527,19 @@ const message = {
|
||||
cancelUploadHelper: 'Deseja cancelar o upload, após o cancelamento, a lista de upload será limpa.',
|
||||
keepOneTab: 'Mantenha pelo menos uma aba',
|
||||
notCanTab: 'Não é possível adicionar mais abas',
|
||||
convert: 'Converter Formato',
|
||||
converting: 'Convertendo Para',
|
||||
fileCanNotConvert: 'Este arquivo não suporta conversão de formato',
|
||||
formatType: 'Tipo de Formato',
|
||||
sourceFormat: 'Formato de Origem',
|
||||
sourceFile: 'Arquivo de Origem',
|
||||
saveDir: 'Diretório de Salvamento',
|
||||
deleteSourceFile: 'Excluir Arquivo de Origem',
|
||||
convertHelper: 'Converter os arquivos selecionados para outro formato',
|
||||
convertHelper1: 'Por favor, selecione os arquivos a serem convertidos',
|
||||
execConvert: 'Iniciar conversão. Você pode visualizar os logs de conversão no Centro de Tarefas',
|
||||
convertLogs: 'Logs de Conversão',
|
||||
formatConvert: 'Conversão de Formato',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Início automático',
|
||||
|
||||
@@ -1530,6 +1530,19 @@ const message = {
|
||||
cancelUploadHelper: 'Отменить загрузку или нет, после отмены список загрузок будет очищен.',
|
||||
keepOneTab: 'Необходимо оставить как минимум одну вкладку',
|
||||
notCanTab: 'Невозможно добавить больше вкладок',
|
||||
convert: 'Конвертировать формат',
|
||||
converting: 'Конвертация в',
|
||||
fileCanNotConvert: 'Этот файл не поддерживает конвертацию формата',
|
||||
formatType: 'Тип формата',
|
||||
sourceFormat: 'Исходный формат',
|
||||
sourceFile: 'Исходный файл',
|
||||
saveDir: 'Каталог сохранения',
|
||||
deleteSourceFile: 'Удалить исходный файл',
|
||||
convertHelper: 'Конвертировать выбранные файлы в другой формат',
|
||||
convertHelper1: 'Пожалуйста, выберите файлы для конвертации',
|
||||
execConvert: 'Начать конвертацию. Вы можете просмотреть журналы конвертации в Центре задач',
|
||||
convertLogs: 'Журналы конвертации',
|
||||
formatConvert: 'Конвертация формата',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Автозапуск',
|
||||
|
||||
@@ -1566,6 +1566,19 @@ const message = {
|
||||
cancelUploadHelper: 'Yüklemeyi iptal etmek ister misiniz, iptal sonrası yükleme listesi temizlenecektir.',
|
||||
keepOneTab: 'En az bir sekme açık kalmalıdır',
|
||||
notCanTab: 'Daha fazla sekme eklenemez',
|
||||
convert: 'Formatı Dönüştür',
|
||||
converting: 'Dönüştürülüyor',
|
||||
fileCanNotConvert: 'Bu dosya format dönüşümünü desteklemiyor',
|
||||
formatType: 'Format Türü',
|
||||
sourceFormat: 'Kaynak Format',
|
||||
sourceFile: 'Kaynak Dosya',
|
||||
saveDir: 'Kaydetme Dizini',
|
||||
deleteSourceFile: 'Kaynak Dosyayı Sil',
|
||||
convertHelper: 'Seçilen dosyaları başka bir formata dönüştür',
|
||||
convertHelper1: 'Lütfen dönüştürülecek dosyaları seçin',
|
||||
execConvert: 'Dönüştürmeyi başlatın. Dönüştürme günlüklerini Görev Merkezi’nde görüntüleyebilirsiniz',
|
||||
convertLogs: 'Dönüştürme Günlükleri',
|
||||
formatConvert: 'Format Dönüştürme',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: 'Otomatik başlat',
|
||||
|
||||
@@ -1474,6 +1474,19 @@ const message = {
|
||||
cancelUploadHelper: '是否取消上傳,取消後將清空上傳列表',
|
||||
keepOneTab: '至少保留一個分頁',
|
||||
notCanTab: '無法新增更多分頁',
|
||||
convert: '文件格式轉換',
|
||||
converting: '轉換為',
|
||||
fileCanNotConvert: '此文件不支援轉換格式',
|
||||
formatType: '格式類型',
|
||||
sourceFormat: '來源格式',
|
||||
sourceFile: '來源檔案',
|
||||
saveDir: '儲存目錄',
|
||||
deleteSourceFile: '是否刪除來源檔案',
|
||||
convertHelper: '將選中的檔案轉換為其他格式',
|
||||
convertHelper1: '請選擇需要轉換格式的檔案',
|
||||
execConvert: '開始轉換,可在任務中心查看轉換日誌',
|
||||
convertLogs: '轉換日誌',
|
||||
formatConvert: '格式轉換',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '開機自啟',
|
||||
|
||||
@@ -1470,6 +1470,19 @@ const message = {
|
||||
cancelUploadHelper: '是否取消上传,取消后将清空上传列表',
|
||||
keepOneTab: '至少保留一个标签页',
|
||||
notCanTab: '不可增加更多的标签页',
|
||||
convert: '转换格式',
|
||||
converting: '转换为',
|
||||
fileCanNotConvert: '此文件不支持转换格式',
|
||||
formatType: '格式类型',
|
||||
sourceFormat: '源格式',
|
||||
sourceFile: '源文件',
|
||||
saveDir: '保存目录',
|
||||
deleteSourceFile: '是否删除源文件',
|
||||
convertHelper: '是否为选中的文件进行格式转换',
|
||||
convertHelper1: '请选择需要转换格式的文件',
|
||||
execConvert: '开始转换,可以在任务中心查看转换日志',
|
||||
convertLogs: '转换日志',
|
||||
formatConvert: '格式转换',
|
||||
},
|
||||
ssh: {
|
||||
autoStart: '开机自启',
|
||||
|
||||
@@ -48,8 +48,8 @@ html {
|
||||
--panel-terminal-tag-active-bg-color: #575758;
|
||||
--panel-terminal-tag-active-text-color: #ebeef5;
|
||||
--panel-terminal-tag-hover-text-color: #575758;
|
||||
--panel-terminal-bg-color: #1e1e1e;
|
||||
--panel-logs-bg-color: #1e1e1e;
|
||||
--panel-terminal-bg-color: #1b1b1b;
|
||||
--panel-logs-bg-color: #1b1b1b;
|
||||
--panel-alert-bg-color: rgba(0, 94, 235, 0.03);
|
||||
|
||||
--panel-alert-bg: #e2e4ec;
|
||||
|
||||
@@ -861,3 +861,10 @@ export const isSensitiveLinuxPath = (path) => {
|
||||
];
|
||||
return sensitivePath.indexOf(path) !== -1;
|
||||
};
|
||||
|
||||
const convertTypes = ['image', 'video', 'audio'] as const;
|
||||
type ConvertType = (typeof convertTypes)[number];
|
||||
|
||||
export function isConvertible(extension: string, mimeType: string): boolean {
|
||||
return convertTypes.includes(getFileType(extension) as ConvertType) && /^(image|audio|video)\//.test(mimeType);
|
||||
}
|
||||
|
||||
408
frontend/src/views/host/file-management/convert/index.vue
Normal file
408
frontend/src/views/host/file-management/convert/index.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<div>
|
||||
<DrawerPro v-model="open" :header="$t('file.formatConvert')" @close="handleClose" size="60%">
|
||||
<template #content>
|
||||
<el-tabs v-model="activeName" type="card" @tab-click="handleClick">
|
||||
<el-tab-pane :label="$t('file.formatConvert')" name="convert">
|
||||
<el-form v-loading="loading">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<el-button type="primary" plain :icon="Plus" @click="addFile" />
|
||||
</div>
|
||||
<ComplexTable v-model:selects="selects" :data="data">
|
||||
<el-table-column fixed type="selection" width="30" />
|
||||
<el-table-column
|
||||
:label="$t('file.sourceFile')"
|
||||
show-overflow-tooltip
|
||||
prop="inputFile"
|
||||
min-width="150"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('file.sourceFormat')"
|
||||
show-overflow-tooltip
|
||||
prop="extension"
|
||||
width="90"
|
||||
></el-table-column>
|
||||
<el-table-column prop="outputFile" width="160">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-start gap-x-1">
|
||||
<span>{{ $t('file.converting') }}</span>
|
||||
<el-select
|
||||
v-model="handleOutputFile"
|
||||
placeholder="Select"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
class="font-normal"
|
||||
@change="handleHeaderSelectChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.outputFormat"
|
||||
style="width: 135px"
|
||||
:value="row.outputFormat || getOptionsByType(row.type)[0]?.value"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getOptionsByType(row.type)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fix
|
||||
width="100"
|
||||
/>
|
||||
</ComplexTable>
|
||||
<div class="flex-row justify-start items-center pt-6">
|
||||
<div class="flex justify-start items-center">
|
||||
<el-breadcrumb-item>{{ $t('file.saveDir') + ':' }}</el-breadcrumb-item>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item>{{ outputPath }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<el-checkbox v-model="deleteSource" :label="$t('file.deleteSourceFile')" size="large" />
|
||||
</div>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('file.convertLogs')" name="log">
|
||||
<ComplexTable :pagination-config="paginationConfig" @search="search()" :data="logs">
|
||||
<el-table-column
|
||||
:label="$t('commons.table.date')"
|
||||
show-overflow-tooltip
|
||||
prop="date"
|
||||
width="160"
|
||||
></el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.table.type')"
|
||||
show-overflow-tooltip
|
||||
prop="type"
|
||||
width="60"
|
||||
></el-table-column>
|
||||
<el-table-column :label="$t('commons.button.log')" prop="log" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="row.log" placement="top">
|
||||
{{ row.log }}
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.message')" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="row.message" placement="top">
|
||||
<el-button
|
||||
:type="
|
||||
row.status === 'FAILED'
|
||||
? 'danger'
|
||||
: row.status === 'SUCCESS'
|
||||
? 'success'
|
||||
: 'default'
|
||||
"
|
||||
link
|
||||
>
|
||||
{{ row.status }}
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ComplexTable>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<template #footer v-if="activeName === 'convert'">
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="removeSelected" :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="convert" :disabled="selects.length === 0">
|
||||
{{ $t('commons.button.handle') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
<FileList ref="chooseFileRef" @choose="chooseFile" />
|
||||
<TaskLog ref="taskLogRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import i18n from '@/lang';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { File } from '@/api/interface/file';
|
||||
import FileList from '@/components/file-list/index.vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { MsgWarning, MsgSuccess } from '@/utils/message';
|
||||
import { convertFiles, convertLogs, getFileContent } from '@/api/modules/files';
|
||||
import { getFileType, isConvertible } from '@/utils/util';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import { TabsPaneContext } from 'element-plus';
|
||||
import { GlobalStore } from '@/store';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const activeName = ref('convert');
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const deleteSource = ref(false);
|
||||
const handleOutputFile = ref('png');
|
||||
const chooseFileRef = ref();
|
||||
const outputPath = ref('');
|
||||
const data = ref<File.ConvertFile[]>([]);
|
||||
const em = defineEmits(['close']);
|
||||
let selects = ref<any>([]);
|
||||
const taskLogRef = ref();
|
||||
const logs = ref([]);
|
||||
|
||||
const CONVERT_KEY = 'convert-files:' + globalStore.currentNode;
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
handleOutputFile.value = 'png';
|
||||
activeName.value = 'convert';
|
||||
em('close', false);
|
||||
};
|
||||
interface DialogProps {
|
||||
files: File.ConvertFile[];
|
||||
outputPath: string;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ value: 'png', label: 'png', type: 'image' },
|
||||
{ value: 'jpg', label: 'jpg', type: 'image' },
|
||||
{ value: 'webp', label: 'webp', type: 'image' },
|
||||
{ value: 'gif', label: 'gif', type: 'image' },
|
||||
{ value: 'jpeg', label: 'jpeg', type: 'image' },
|
||||
{ value: 'bmp', label: 'bmp', type: 'image' },
|
||||
{ value: 'tiff', label: 'tiff', type: 'image' },
|
||||
|
||||
{ value: 'mp4', label: 'mp4', type: 'video' },
|
||||
{ value: 'avi', label: 'avi', type: 'video' },
|
||||
{ value: 'mov', label: 'mov', type: 'video' },
|
||||
{ value: 'mkv', label: 'mkv', type: 'video' },
|
||||
|
||||
{ value: 'mp3', label: 'mp3', type: 'audio' },
|
||||
{ value: 'wav', label: 'wav', type: 'audio' },
|
||||
{ value: 'flac', label: 'flac', type: 'audio' },
|
||||
{ value: 'aac', label: 'aac', type: 'audio' },
|
||||
];
|
||||
|
||||
let req = reactive({
|
||||
name: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
orderBy: 'favorite',
|
||||
order: 'descending',
|
||||
});
|
||||
|
||||
const getOptionsByType = (type: string) => {
|
||||
return options.filter((opt) => opt.type === type);
|
||||
};
|
||||
|
||||
const initTableData = () => {
|
||||
data.value.map((row) => {
|
||||
const matchedOption = getOptionsByType(row.type).find((item) => row.extension.concat(item.value));
|
||||
const defaultOption = getOptionsByType(row.type)[0]?.value;
|
||||
row.outputFormat = matchedOption?.value || defaultOption;
|
||||
});
|
||||
};
|
||||
|
||||
const handleHeaderSelectChange = (value: string) => {
|
||||
if (value && data.value.length) {
|
||||
data.value.forEach((row) => {
|
||||
const isValid = getOptionsByType(row.type).some((item) => item.value === value);
|
||||
row.outputFormat = isValid ? value : row.outputFormat;
|
||||
});
|
||||
}
|
||||
};
|
||||
const addFile = () => {
|
||||
chooseFileRef.value?.acceptParams({ dir: false });
|
||||
};
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'file-convert-log-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('file-convert-log-page-size')) || 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
req.page = paginationConfig.currentPage;
|
||||
req.pageSize = paginationConfig.pageSize;
|
||||
loading.value = true;
|
||||
data.value = [];
|
||||
await convertLogs(req)
|
||||
.then((res) => {
|
||||
logs.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const chooseFile = async (path: string) => {
|
||||
await getFileContent({ path: path, expand: false, page: 1, pageSize: 1, isDetail: true }).then((res) => {
|
||||
if (!isConvertible(res.data.extension, res.data.mimeType)) {
|
||||
MsgWarning(i18n.global.t('file.fileCanNotConvert'));
|
||||
return;
|
||||
}
|
||||
updateFiles([
|
||||
{
|
||||
type: getFileType(res.data.extension),
|
||||
inputFile: res.data.name,
|
||||
extension: res.data.extension,
|
||||
path: getDirPath(res.data.path, res.data.name),
|
||||
outputFormat: res.data.extension.slice(1),
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
const getDirPath = (fullPath: string, fileName: string): string => {
|
||||
if (!fullPath || !fileName) return fullPath;
|
||||
if (fullPath.endsWith(fileName)) {
|
||||
const dir = fullPath.slice(0, fullPath.length - fileName.length);
|
||||
return dir.replace(/\/$/, '');
|
||||
}
|
||||
return fullPath;
|
||||
};
|
||||
|
||||
const delFile = async (path: string, name: string) => {
|
||||
ElMessageBox.confirm(i18n.global.t('file.deleteHelper2'), i18n.global.t('commons.msg.remove'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
removeByPathAndName(path, name);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeByPathAndName = (path: string, name: string) => {
|
||||
const fullPath = `${path}/${name}`;
|
||||
data.value = data.value.filter((item) => `${item.path}/${item.inputFile}` !== fullPath);
|
||||
saveFiles();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (selects.value.length > 0) {
|
||||
ElMessageBox.confirm(i18n.global.t('file.convertHelper'), i18n.global.t('file.convert'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
loading.value = true;
|
||||
const reqFiles = selects.value;
|
||||
const output = outputPath.value;
|
||||
const deleteSrc = deleteSource.value;
|
||||
const taskID = uuidv4();
|
||||
const singleReq = {
|
||||
files: reqFiles,
|
||||
outputPath: output,
|
||||
deleteSource: deleteSrc,
|
||||
taskID: taskID,
|
||||
};
|
||||
await convertFiles(singleReq)
|
||||
.then(() => {
|
||||
removeSelected();
|
||||
deleteSource.value = false;
|
||||
MsgSuccess(i18n.global.t('file.execConvert'));
|
||||
openTaskLog(taskID);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
MsgWarning(i18n.global.t('file.convertHelper1'));
|
||||
}
|
||||
};
|
||||
|
||||
const openTaskLog = (taskID: string) => {
|
||||
taskLogRef.value.openWithTaskID(taskID);
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: any) => {
|
||||
delFile(row.path, row.inputFile);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const loadFiles = () => {
|
||||
const stored = localStorage.getItem(CONVERT_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
data.value = JSON.parse(stored);
|
||||
} catch {
|
||||
data.value = [];
|
||||
}
|
||||
} else {
|
||||
data.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const saveFiles = () => {
|
||||
localStorage.setItem(CONVERT_KEY, JSON.stringify(data.value));
|
||||
};
|
||||
|
||||
const updateFiles = (newFiles: File.ConvertFile[]) => {
|
||||
const stored = localStorage.getItem(CONVERT_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
const oldFiles: File.ConvertFile[] = JSON.parse(stored);
|
||||
const merged = [...oldFiles];
|
||||
for (const file of newFiles) {
|
||||
if (!merged.some((item) => item.path === file.path && item.inputFile === file.inputFile)) {
|
||||
merged.push(file);
|
||||
}
|
||||
}
|
||||
data.value = merged;
|
||||
} catch {
|
||||
data.value = [...newFiles];
|
||||
}
|
||||
} else {
|
||||
data.value = [...newFiles];
|
||||
}
|
||||
saveFiles();
|
||||
};
|
||||
|
||||
const handleClick = (tab: TabsPaneContext) => {
|
||||
if (tab.paneName == 'log') {
|
||||
search();
|
||||
} else {
|
||||
loadFiles();
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelected = () => {
|
||||
const selectedKeys = new Set(selects.value.map((s) => `${s.path}/${s.inputFile}`));
|
||||
data.value = data.value.filter((d) => !selectedKeys.has(`${d.path}/${d.inputFile}`));
|
||||
saveFiles();
|
||||
};
|
||||
|
||||
const acceptParams = (param: DialogProps): void => {
|
||||
open.value = true;
|
||||
outputPath.value = param.outputPath;
|
||||
loadFiles();
|
||||
updateFiles(param.files);
|
||||
initTableData();
|
||||
};
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
||||
@@ -638,6 +638,7 @@
|
||||
<VscodeOpenDialog ref="dialogVscodeOpenRef" />
|
||||
<Preview ref="previewRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
<Convert ref="convertRef" @close="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -655,7 +656,16 @@ import {
|
||||
searchFavorite,
|
||||
searchHostMount,
|
||||
} from '@/api/modules/files';
|
||||
import { computeSize, copyText, dateFormat, downloadFile, getFileType, getIcon, getRandomStr } from '@/utils/util';
|
||||
import {
|
||||
computeSize,
|
||||
copyText,
|
||||
dateFormat,
|
||||
downloadFile,
|
||||
getFileType,
|
||||
getIcon,
|
||||
getRandomStr,
|
||||
isConvertible,
|
||||
} from '@/utils/util';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { Languages, Mimetypes } from '@/global/mimetype';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -685,11 +695,14 @@ import Favorite from './favorite/index.vue';
|
||||
import BatchRole from './batch-role/index.vue';
|
||||
import Preview from './preview/index.vue';
|
||||
import VscodeOpenDialog from '@/components/vscode-open/index.vue';
|
||||
import Convert from './convert/index.vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import TerminalDialog from './terminal/index.vue';
|
||||
import { Dashboard } from '@/api/interface/dashboard';
|
||||
import { CompressExtension, CompressType } from '@/enums/files';
|
||||
import type { TabPaneName } from 'element-plus';
|
||||
import { getComponentInfo } from '@/api/modules/host';
|
||||
import { routerToNameWithQuery } from '@/utils/router';
|
||||
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
@@ -741,6 +754,22 @@ const fileUpload = reactive({ path: '' });
|
||||
const fileRename = reactive({ path: '', oldName: '', newName: '' });
|
||||
const fileWget = reactive({ path: '' });
|
||||
const fileMove = reactive({ oldPaths: [''], allNames: [''], type: '', path: '', name: '', count: 0, isDir: false });
|
||||
const fileConvert = reactive<{
|
||||
outputPath: string;
|
||||
files: File.ConvertFile[];
|
||||
}>({
|
||||
outputPath: '',
|
||||
files: [
|
||||
{
|
||||
type: '',
|
||||
inputFile: '',
|
||||
extension: '',
|
||||
path: '',
|
||||
outputFormat: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
const ffmpegExist = ref(false);
|
||||
|
||||
const createRef = ref();
|
||||
const roleRef = ref();
|
||||
@@ -775,6 +804,7 @@ const dirNum = ref(0);
|
||||
const fileNum = ref(0);
|
||||
const imageFiles = ref([]);
|
||||
const isEdit = ref(false);
|
||||
const convertRef = ref();
|
||||
|
||||
const renameRefs = ref<Record<string, any>>({});
|
||||
|
||||
@@ -1584,6 +1614,15 @@ const buttons = [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('file.convert'),
|
||||
click: (row: File.File) => {
|
||||
openConvert(row);
|
||||
},
|
||||
disabled: (row: File.File) => {
|
||||
return row?.isDir || !isConvertible(row?.extension, row?.mimeType);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('file.openWithVscode'),
|
||||
click: openWithVSCode,
|
||||
@@ -1595,6 +1634,36 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
|
||||
const openConvert = (item: File.File) => {
|
||||
if (!ffmpegExist.value) {
|
||||
ElMessageBox.confirm(i18n.global.t('cronjob.library.noSuchApp', ['FFmpeg']), i18n.global.t('file.convert'), {
|
||||
confirmButtonText: i18n.global.t('app.toInstall'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(() => {
|
||||
routerToNameWithQuery('Library', { t: Date.now(), uncached: 'true' });
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (!isConvertible(item.extension, item.mimeType)) {
|
||||
MsgWarning(i18n.global.t('file.fileCanNotConvert'));
|
||||
return;
|
||||
}
|
||||
const fileType = getFileType(item.extension);
|
||||
fileConvert.outputPath = req.path;
|
||||
fileConvert.files = [
|
||||
{
|
||||
type: fileType,
|
||||
path: req.path,
|
||||
extension: item.extension,
|
||||
inputFile: item.name,
|
||||
outputFormat: item.extension.slice(1),
|
||||
},
|
||||
];
|
||||
|
||||
convertRef.value.acceptParams(fileConvert);
|
||||
}
|
||||
};
|
||||
|
||||
const isDecompressFile = (row: File.File) => {
|
||||
if (row.isDir) {
|
||||
return false;
|
||||
@@ -1644,6 +1713,7 @@ onMounted(() => {
|
||||
initTabsAndPaths();
|
||||
getHostMount();
|
||||
initHistory();
|
||||
checkFFmpeg();
|
||||
nextTick(function () {
|
||||
handlePath();
|
||||
observeResize();
|
||||
@@ -1826,6 +1896,12 @@ const removeTab = (targetId: TabPaneName) => {
|
||||
changeTab(String(nextActive));
|
||||
};
|
||||
|
||||
const checkFFmpeg = () => {
|
||||
getComponentInfo('ffmpeg', globalStore.currentNode).then((res) => {
|
||||
ffmpegExist.value = res.data.exists ?? false;
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', watchTitleHeight);
|
||||
|
||||
Reference in New Issue
Block a user