From 9f1cb52cfb2de2d61d49cf732ab52dc45c159173 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 2 Sep 2025 11:54:04 +0800 Subject: [PATCH] feat(explorer): preview archive file content and extract selected files (#2852) --- application/constants/constants.go | 2 +- assets | 2 +- inventory/migration.go | 47 +++++++++++ inventory/setting.go | 73 +++++++++------- inventory/types/types.go | 25 +++--- pkg/filemanager/fs/fs.go | 2 + pkg/filemanager/manager/archive.go | 120 +++++++++++++++++++++++++++ pkg/filemanager/manager/manager.go | 3 + pkg/filemanager/workflows/extract.go | 50 +++++++++-- routers/controllers/file.go | 14 ++++ routers/router.go | 4 + service/explorer/file.go | 30 +++++++ service/explorer/response.go | 10 +++ service/explorer/workflows.go | 3 +- 14 files changed, 329 insertions(+), 56 deletions(-) diff --git a/application/constants/constants.go b/application/constants/constants.go index a50d415..983d516 100644 --- a/application/constants/constants.go +++ b/application/constants/constants.go @@ -3,7 +3,7 @@ package constants // These values will be injected at build time, DO NOT EDIT. // BackendVersion 当前后端版本号 -var BackendVersion = "4.1.0" +var BackendVersion = "4.7.0" // IsPro 是否为Pro版本 var IsPro = "false" diff --git a/assets b/assets index 3596160..463794a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 35961604a187a49591fa57a50de8c0dad4bb5b78 +Subproject commit 463794a71e6e19b9d4ee35248f00ff64f9485f30 diff --git a/inventory/migration.go b/inventory/migration.go index 9f1e28b..e2276f2 100644 --- a/inventory/migration.go +++ b/inventory/migration.go @@ -279,6 +279,53 @@ type ( ) var patches = []Patch{ + { + Name: "apply_default_archive_viewer", + EndVersion: "4.7.0", + Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error { + fileViewersSetting, err := client.Setting.Query().Where(setting.Name("file_viewers")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query file_viewers setting: %w", err) + } + + var fileViewers []types.ViewerGroup + if err := json.Unmarshal([]byte(fileViewersSetting.Value), &fileViewers); err != nil { + return fmt.Errorf("failed to unmarshal file_viewers setting: %w", err) + } + + fileViewerExisted := false + for _, viewer := range fileViewers[0].Viewers { + if viewer.ID == "archive" { + fileViewerExisted = true + break + } + } + + // 2.2 If not existed, add it + if !fileViewerExisted { + // Found existing archive viewer default setting + var defaultArchiveViewer types.Viewer + for _, viewer := range defaultFileViewers[0].Viewers { + if viewer.ID == "archive" { + defaultArchiveViewer = viewer + break + } + } + + fileViewers[0].Viewers = append(fileViewers[0].Viewers, defaultArchiveViewer) + newFileViewersSetting, err := json.Marshal(fileViewers) + if err != nil { + return fmt.Errorf("failed to marshal file_viewers setting: %w", err) + } + + if _, err := client.Setting.UpdateOne(fileViewersSetting).SetValue(string(newFileViewersSetting)).Save(ctx); err != nil { + return fmt.Errorf("failed to update file_viewers setting: %w", err) + } + } + + return nil + }, + }, { Name: "apply_default_excalidraw_viewer", EndVersion: "4.1.0", diff --git a/inventory/setting.go b/inventory/setting.go index d769ed9..19b4aa6 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -321,6 +321,15 @@ var ( }, }, }, + { + ID: "archive", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.archivePreview", + Exts: []string{"zip", "7z"}, + RequiredGroupPermission: []types.GroupPermission{ + types.GroupPermissionArchiveTask, + }, + }, }, }, } @@ -347,19 +356,19 @@ var ( type MailTemplateContent struct { Language string - EmailIsAutoSend string // Translation of `此邮件由系统自动发送。` + EmailIsAutoSend string // Translation of `此邮件由系统自动发送。` - ActiveTitle string // Translation of `激活你的账号` - ActiveDes string // Translation of `请点击下方按钮确认你的电子邮箱并完成账号注册,此链接有效期为 24 小时。` - ActiveButton string // Translation of `确认激活` + ActiveTitle string // Translation of `激活你的账号` + ActiveDes string // Translation of `请点击下方按钮确认你的电子邮箱并完成账号注册,此链接有效期为 24 小时。` + ActiveButton string // Translation of `确认激活` - ResetTitle string // Translation of `重设密码` - ResetDes string // Translation of `请点击下方按钮重设你的密码,此链接有效期为 1 小时。` - ResetButton string // Translation of `重设密码` + ResetTitle string // Translation of `重设密码` + ResetDes string // Translation of `请点击下方按钮重设你的密码,此链接有效期为 1 小时。` + ResetButton string // Translation of `重设密码` } var mailTemplateContents = []MailTemplateContent{ - { + { Language: "en-US", EmailIsAutoSend: "This email is sent automatically.", ActiveTitle: "Confirm your account", @@ -368,8 +377,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Reset your password", ResetDes: "Please click the button below to reset your password. This link is valid for 1 hour.", ResetButton: "Reset", - }, - { + }, + { Language: "zh-CN", EmailIsAutoSend: "此邮件由系统自动发送。", ActiveTitle: "激活你的账号", @@ -378,8 +387,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "重设密码", ResetDes: "请点击下方按钮重设你的密码,此链接有效期为 1 小时。", ResetButton: "重设密码", - }, - { + }, + { Language: "zh-TW", EmailIsAutoSend: "此郵件由系統自動發送。", ActiveTitle: "激活你的帳號", @@ -388,8 +397,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "重設密碼", ResetDes: "請點擊下方按鈕重設你的密碼,此連結有效期為 1 小時。", ResetButton: "重設密碼", - }, - { + }, + { Language: "de-DE", EmailIsAutoSend: "Diese E-Mail wird automatisch vom System gesendet.", ActiveTitle: "Bestätigen Sie Ihr Konto", @@ -398,8 +407,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Passwort zurücksetzen", ResetDes: "Bitte klicken Sie auf die Schaltfläche unten, um Ihr Passwort zurückzusetzen. Dieser Link ist 1 Stunde lang gültig.", ResetButton: "Passwort zurücksetzen", - }, - { + }, + { Language: "es-ES", EmailIsAutoSend: "Este correo electrónico se envía automáticamente.", ActiveTitle: "Confirma tu cuenta", @@ -408,8 +417,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Restablecer tu contraseña", ResetDes: "Por favor, haz clic en el botón de abajo para restablecer tu contraseña. Este enlace es válido por 1 hora.", ResetButton: "Restablecer", - }, - { + }, + { Language: "fr-FR", EmailIsAutoSend: "Cet e-mail est envoyé automatiquement.", ActiveTitle: "Confirmer votre compte", @@ -418,8 +427,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Réinitialiser votre mot de passe", ResetDes: "Veuillez cliquer sur le bouton ci-dessous pour réinitialiser votre mot de passe. Ce lien est valable 1 heure.", ResetButton: "Réinitialiser", - }, - { + }, + { Language: "it-IT", EmailIsAutoSend: "Questa email è inviata automaticamente.", ActiveTitle: "Conferma il tuo account", @@ -428,8 +437,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Reimposta la tua password", ResetDes: "Per favore, clicca sul pulsante qui sotto per reimpostare la tua password. Questo link è valido per 1 ora.", ResetButton: "Reimposta", - }, - { + }, + { Language: "ja-JP", EmailIsAutoSend: "このメールはシステムによって自動的に送信されました。", ActiveTitle: "アカウントを確認する", @@ -438,8 +447,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "パスワードをリセットする", ResetDes: "以下のボタンをクリックしてパスワードをリセットしてください。このリンクは1時間有効です。", ResetButton: "リセットする", - }, - { + }, + { Language: "ko-KR", EmailIsAutoSend: "이 이메일은 시스템에 의해 자동으로 전송됩니다.", ActiveTitle: "계정 확인", @@ -448,8 +457,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "비밀번호 재설정", ResetDes: "아래 버튼을 클릭하여 비밀번호를 재설정하세요. 이 링크는 1시간 동안 유효합니다.", ResetButton: "비밀번호 재설정", - }, - { + }, + { Language: "pt-BR", EmailIsAutoSend: "Este e-mail é enviado automaticamente.", ActiveTitle: "Confirme sua conta", @@ -458,8 +467,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Redefinir sua senha", ResetDes: "Por favor, clique no botão abaixo para redefinir sua senha. Este link é válido por 1 hora.", ResetButton: "Redefinir", - }, - { + }, + { Language: "ru-RU", EmailIsAutoSend: "Это письмо отправлено автоматически.", ActiveTitle: "Подтвердите вашу учетную запись", @@ -468,7 +477,7 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Сбросить ваш пароль", ResetDes: "Пожалуйста, нажмите кнопку ниже, чтобы сбросить ваш пароль. Эта ссылка действительна в течение 1 часа.", ResetButton: "Сбросить пароль", - }, + }, } var DefaultSettings = map[string]string{ @@ -675,7 +684,7 @@ func init() { activeMails = append(activeMails, map[string]string{ "language": langContents.Language, "title": "[{{ .CommonContext.SiteBasic.Name }}] " + langContents.ActiveTitle, - "body": util.Replace(map[string]string{ + "body": util.Replace(map[string]string{ "[[ .Language ]]": langContents.Language, "[[ .ActiveTitle ]]": langContents.ActiveTitle, "[[ .ActiveDes ]]": langContents.ActiveDes, @@ -695,7 +704,7 @@ func init() { resetMails = append(resetMails, map[string]string{ "language": langContents.Language, "title": "[{{ .CommonContext.SiteBasic.Name }}] " + langContents.ResetTitle, - "body": util.Replace(map[string]string{ + "body": util.Replace(map[string]string{ "[[ .Language ]]": langContents.Language, "[[ .ResetTitle ]]": langContents.ResetTitle, "[[ .ResetDes ]]": langContents.ResetDes, @@ -709,4 +718,4 @@ func init() { panic(err) } DefaultSettings["mail_reset_template"] = string(mailResetTemplates) -} \ No newline at end of file +} diff --git a/inventory/types/types.go b/inventory/types/types.go index 2306ac4..9ba0713 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -293,18 +293,19 @@ const ( type ( Viewer struct { - ID string `json:"id"` - Type ViewerType `json:"type"` - DisplayName string `json:"display_name"` - Exts []string `json:"exts"` - Url string `json:"url,omitempty"` - Icon string `json:"icon,omitempty"` - WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` - Props map[string]string `json:"props,omitempty"` - MaxSize int64 `json:"max_size,omitempty"` - Disabled bool `json:"disabled,omitempty"` - Templates []NewFileTemplate `json:"templates,omitempty"` - Platform string `json:"platform,omitempty"` + ID string `json:"id"` + Type ViewerType `json:"type"` + DisplayName string `json:"display_name"` + Exts []string `json:"exts"` + Url string `json:"url,omitempty"` + Icon string `json:"icon,omitempty"` + WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` + Props map[string]string `json:"props,omitempty"` + MaxSize int64 `json:"max_size,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Templates []NewFileTemplate `json:"templates,omitempty"` + Platform string `json:"platform,omitempty"` + RequiredGroupPermission []GroupPermission `json:"required_group_permission,omitempty"` } ViewerGroup struct { Viewers []Viewer `json:"viewers"` diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 0121547..20681d6 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -699,6 +699,8 @@ func LockSessionToContext(ctx context.Context, session LockSession) context.Cont return context.WithValue(ctx, LockSessionCtxKey{}, session) } +// FindDesiredEntity finds the desired entity from the file. +// entityType is optional, if it is not nil, it will only return the entity with the given type. func FindDesiredEntity(file File, version string, hasher hashid.Encoder, entityType *types.EntityType) (bool, Entity) { if version == "" { return true, file.PrimaryEntity() diff --git a/pkg/filemanager/manager/archive.go b/pkg/filemanager/manager/archive.go index fa2440a..667ca6f 100644 --- a/pkg/filemanager/manager/archive.go +++ b/pkg/filemanager/manager/archive.go @@ -3,19 +3,95 @@ package manager import ( "archive/zip" "context" + "encoding/gob" "fmt" "io" "path" "path/filepath" "strings" + "time" + "github.com/bodgit/sevenzip" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" + "github.com/cloudreve/Cloudreve/v4/pkg/util" "golang.org/x/tools/container/intsets" ) +type ( + ArchivedFile struct { + Name string `json:"name"` + Size int64 `json:"size"` + UpdatedAt *time.Time `json:"updated_at"` + IsDirectory bool `json:"is_directory"` + } +) + +const ( + ArchiveListCacheTTL = 3600 // 1 hour +) + +func init() { + gob.Register([]ArchivedFile{}) +} + +func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity string) ([]ArchivedFile, error) { + file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile)) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + if file.Type() != types.FileTypeFile { + return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("path %s is not a file", uri)) + } + + // Validate file size + if m.user.Edges.Group.Settings.DecompressSize > 0 && file.Size() > m.user.Edges.Group.Settings.DecompressSize { + return nil, fs.ErrFileSizeTooBig.WithError(fmt.Errorf("file size %d exceeds the limit %d", file.Size(), m.user.Edges.Group.Settings.DecompressSize)) + } + + found, targetEntity := fs.FindDesiredEntity(file, entity, m.hasher, nil) + if !found { + return nil, fs.ErrEntityNotExist + } + + cacheKey := getArchiveListCacheKey(targetEntity.ID()) + kv := m.kv + res, found := kv.Get(cacheKey) + if found { + return res.([]ArchivedFile), nil + } + + es, err := m.GetEntitySource(ctx, 0, fs.WithEntity(targetEntity)) + if err != nil { + return nil, fmt.Errorf("failed to get entity source: %w", err) + } + + es.Apply(entitysource.WithContext(ctx)) + defer es.Close() + + var readerFunc func(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) + switch file.Ext() { + case "zip": + readerFunc = getZipFileList + case "7z": + readerFunc = get7zFileList + default: + return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("not supported archive format: %s", file.Ext())) + } + + sr := io.NewSectionReader(es, 0, targetEntity.Size()) + fileList, err := readerFunc(ctx, sr, targetEntity.Size()) + if err != nil { + return nil, fmt.Errorf("failed to read file list: %w", err) + } + + kv.Set(cacheKey, fileList, ArchiveListCacheTTL) + return fileList, nil +} + func (m *manager) CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) { o := newOption() for _, opt := range opts { @@ -122,3 +198,47 @@ func (m *manager) compressFileToArchive(ctx context.Context, parent string, file return err } + +func getZipFileList(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) { + zr, err := zip.NewReader(file, size) + if err != nil { + return nil, fmt.Errorf("failed to create zip reader: %w", err) + } + + fileList := make([]ArchivedFile, 0, len(zr.File)) + for _, f := range zr.File { + info := f.FileInfo() + modTime := info.ModTime() + fileList = append(fileList, ArchivedFile{ + Name: util.FormSlash(f.Name), + Size: info.Size(), + UpdatedAt: &modTime, + IsDirectory: info.IsDir(), + }) + } + return fileList, nil +} + +func get7zFileList(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) { + zr, err := sevenzip.NewReader(file, size) + if err != nil { + return nil, fmt.Errorf("failed to create 7z reader: %w", err) + } + + fileList := make([]ArchivedFile, 0, len(zr.File)) + for _, f := range zr.File { + info := f.FileInfo() + modTime := info.ModTime() + fileList = append(fileList, ArchivedFile{ + Name: util.FormSlash(f.Name), + Size: info.Size(), + UpdatedAt: &modTime, + IsDirectory: info.IsDir(), + }) + } + return fileList, nil +} + +func getArchiveListCacheKey(entity int) string { + return fmt.Sprintf("archive_list_%d", entity) +} diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index ad04af5..98d01b9 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -85,7 +85,10 @@ type ( } Archiver interface { + // CreateArchive creates an archive CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) + // ListArchiveFiles lists files in an archive + ListArchiveFiles(ctx context.Context, uri *fs.URI, entity string) ([]ArchivedFile, error) } FileManager interface { diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index 00616f7..f51d226 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -47,14 +47,15 @@ type ( } ExtractArchiveTaskPhase string ExtractArchiveTaskState struct { - Uri string `json:"uri,omitempty"` - Encoding string `json:"encoding,omitempty"` - Dst string `json:"dst,omitempty"` - TempPath string `json:"temp_path,omitempty"` - TempZipFilePath string `json:"temp_zip_file_path,omitempty"` - ProcessedCursor string `json:"processed_cursor,omitempty"` - SlaveTaskID int `json:"slave_task_id,omitempty"` - Password string `json:"password,omitempty"` + Uri string `json:"uri,omitempty"` + Encoding string `json:"encoding,omitempty"` + Dst string `json:"dst,omitempty"` + TempPath string `json:"temp_path,omitempty"` + TempZipFilePath string `json:"temp_zip_file_path,omitempty"` + ProcessedCursor string `json:"processed_cursor,omitempty"` + SlaveTaskID int `json:"slave_task_id,omitempty"` + Password string `json:"password,omitempty"` + FileMask []string `json:"file_mask,omitempty"` NodeState `json:",inline"` Phase ExtractArchiveTaskPhase `json:"phase,omitempty"` } @@ -119,13 +120,14 @@ var encodings = map[string]encoding.Encoding{ } // NewExtractArchiveTask creates a new ExtractArchiveTask -func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string) (queue.Task, error) { +func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string, mask []string) (queue.Task, error) { state := &ExtractArchiveTaskState{ Uri: src, Dst: dst, Encoding: encoding, NodeState: NodeState{}, Password: password, + FileMask: mask, } stateBytes, err := json.Marshal(state) if err != nil { @@ -247,6 +249,7 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep Dst: m.state.Dst, UserID: user.ID, Password: m.state.Password, + FileMask: m.state.FileMask, } payloadStr, err := json.Marshal(payload) @@ -416,6 +419,14 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen rawPath := util.FormSlash(f.NameInArchive) savePath := dst.JoinRaw(rawPath) + // If file mask is not empty, check if the path is in the mask + if len(m.state.FileMask) > 0 && !isFileInMask(rawPath, m.state.FileMask) { + m.l.Warning("File %q is not in the mask, skipping...", f.NameInArchive) + atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1) + atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size()) + return nil + } + // Check if path is legit if !strings.HasPrefix(savePath.Path(), util.FillSlash(path.Clean(dst.Path()))) { m.l.Warning("Path %q is not legit, skipping...", f.NameInArchive) @@ -599,6 +610,7 @@ type ( TempZipFilePath string `json:"temp_zip_file_path,omitempty"` ProcessedCursor string `json:"processed_cursor,omitempty"` Password string `json:"password,omitempty"` + FileMask []string `json:"file_mask,omitempty"` } ) @@ -779,6 +791,12 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { rawPath := util.FormSlash(f.NameInArchive) savePath := dst.JoinRaw(rawPath) + // If file mask is not empty, check if the path is in the mask + if len(m.state.FileMask) > 0 && !isFileInMask(rawPath, m.state.FileMask) { + m.l.Debug("File %q is not in the mask, skipping...", f.NameInArchive) + return nil + } + // Check if path is legit if !strings.HasPrefix(savePath.Path(), util.FillSlash(path.Clean(dst.Path()))) { atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1) @@ -846,3 +864,17 @@ func (m *SlaveExtractArchiveTask) Progress(ctx context.Context) queue.Progresses defer m.Unlock() return m.progress } + +func isFileInMask(path string, mask []string) bool { + if len(mask) == 0 { + return true + } + + for _, m := range mask { + if path == m || strings.HasPrefix(path, m+"/") { + return true + } + } + + return false +} diff --git a/routers/controllers/file.go b/routers/controllers/file.go index e09a95f..1875d17 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -412,3 +412,17 @@ func PatchView(c *gin.Context) { c.JSON(200, serializer.Response{}) } + +func ListArchiveFiles(c *gin.Context) { + service := ParametersFromContext[*explorer.ArchiveListFilesService](c, explorer.ArchiveListFilesParamCtx{}) + resp, err := service.List(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + c.Abort() + return + } + + c.JSON(200, serializer.Response{ + Data: resp, + }) +} diff --git a/routers/router.go b/routers/router.go index a866d88..d474ccf 100644 --- a/routers/router.go +++ b/routers/router.go @@ -566,6 +566,10 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { controllers.FromQuery[explorer.ListFileService](explorer.ListFileParameterCtx{}), controllers.ListDirectory, ) + file.GET("archive", + controllers.FromQuery[explorer.ArchiveListFilesService](explorer.ArchiveListFilesParamCtx{}), + controllers.ListArchiveFiles, + ) // Create file file.POST("create", controllers.FromJSON[explorer.CreateFileService](explorer.CreateFileParameterCtx{}), diff --git a/service/explorer/file.go b/service/explorer/file.go index 02ca564..e82a60c 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -716,3 +716,33 @@ func (s *PatchViewService) Patch(c *gin.Context) error { return nil } + +type ( + ArchiveListFilesParamCtx struct{} + ArchiveListFilesService struct { + Uri string `form:"uri" binding:"required"` + Entity string `form:"entity"` + } +) + +func (s *ArchiveListFilesService) List(c *gin.Context) (*ArchiveListFilesResponse, error) { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + m := manager.NewFileManager(dep, user) + defer m.Recycle() + if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionArchiveTask)) { + return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "Group not allowed to extract archive files", nil) + } + + uri, err := fs.NewUriFromString(s.Uri) + if err != nil { + return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err) + } + + files, err := m.ListArchiveFiles(c, uri, s.Entity) + if err != nil { + return nil, fmt.Errorf("failed to list archive files: %w", err) + } + + return BuildArchiveListFilesResponse(files), nil +} diff --git a/service/explorer/response.go b/service/explorer/response.go index 991039b..ee03137 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -26,6 +26,16 @@ import ( "github.com/samber/lo" ) +type ArchiveListFilesResponse struct { + Files []manager.ArchivedFile `json:"files"` +} + +func BuildArchiveListFilesResponse(files []manager.ArchivedFile) *ArchiveListFilesResponse { + return &ArchiveListFilesResponse{ + Files: files, + } +} + type PutRelativeResponse struct { Name string Url string diff --git a/service/explorer/workflows.go b/service/explorer/workflows.go index 138b292..ad7cf09 100644 --- a/service/explorer/workflows.go +++ b/service/explorer/workflows.go @@ -174,6 +174,7 @@ type ( Dst string `json:"dst" binding:"required"` Encoding string `json:"encoding"` Password string `json:"password"` + FileMask []string `json:"file_mask"` } CreateArchiveParamCtx struct{} ) @@ -204,7 +205,7 @@ func (service *ArchiveWorkflowService) CreateExtractTask(c *gin.Context) (*TaskR } // Create task - t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding, service.Password) + t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding, service.Password, service.FileMask) if err != nil { return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err) }