package service import ( "fmt" "io" "io/fs" "os" "os/exec" "path" "path/filepath" "strings" "time" "unicode/utf8" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "golang.org/x/net/html/charset" "golang.org/x/sys/unix" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/pkg/errors" ) type FileService struct { } type IFileService interface { GetFileList(op request.FileOption) (response.FileInfo, error) SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) GetFileTree(op request.FileOption) ([]response.FileTree, error) Create(op request.FileCreate) error Delete(op request.FileDelete) error BatchDelete(op request.FileBatchDelete) error Compress(c request.FileCompress) error DeCompress(c request.FileDeCompress) error GetContent(op request.FileContentReq) (response.FileInfo, error) SaveContent(edit request.FileEdit) error FileDownload(d request.FileDownload) (string, error) DirSize(req request.DirSizeReq) (response.DirSizeRes, error) ChangeName(req request.FileRename) error Wget(w request.FileWget) (string, error) MvFile(m request.FileMove) error ChangeOwner(req request.FileRoleUpdate) error ChangeMode(op request.FileCreate) error BatchChangeModeAndOwner(op request.FileRoleReq) error ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) } var filteredPaths = []string{ "/.1panel_clash", } func NewIFileService() IFileService { return &FileService{} } func (f *FileService) GetFileList(op request.FileOption) (response.FileInfo, error) { var fileInfo response.FileInfo if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { return fileInfo, nil } info, err := files.NewFileInfo(op.FileOption) if err != nil { return fileInfo, err } fileInfo.FileInfo = *info return fileInfo, nil } func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) { var ( files []response.UploadInfo backData []response.UploadInfo ) _ = filepath.Walk(req.Path, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if !info.IsDir() { files = append(files, response.UploadInfo{ CreatedAt: info.ModTime().Format(constant.DateTimeLayout), Size: int(info.Size()), Name: info.Name(), }) } return nil }) total, start, end := len(files), (req.Page-1)*req.PageSize, req.Page*req.PageSize if start > total { backData = make([]response.UploadInfo, 0) } else { if end >= total { end = total } backData = files[start:end] } return int64(total), backData, nil } func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, error) { var treeArray []response.FileTree if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { return treeArray, nil } info, err := files.NewFileInfo(op.FileOption) if err != nil { return nil, err } node := response.FileTree{ ID: common.GetUuid(), Name: info.Name, Path: info.Path, IsDir: info.IsDir, Extension: info.Extension, } err = f.buildFileTree(&node, info.Items, op, 2) if err != nil { return nil, err } return append(treeArray, node), nil } func shouldFilterPath(path string) bool { cleanedPath := filepath.Clean(path) for _, filteredPath := range filteredPaths { cleanedFilteredPath := filepath.Clean(filteredPath) if cleanedFilteredPath == cleanedPath || strings.HasPrefix(cleanedPath, cleanedFilteredPath+"/") { return true } } return false } // 递归构建文件树(只取当前目录以及当前目录下的第一层子节点) func (f *FileService) buildFileTree(node *response.FileTree, items []*files.FileInfo, op request.FileOption, level int) error { for _, v := range items { if shouldFilterPath(v.Path) { global.LOG.Infof("File Tree: Skipping %s due to filter\n", v.Path) continue } childNode := response.FileTree{ ID: common.GetUuid(), Name: v.Name, Path: v.Path, IsDir: v.IsDir, Extension: v.Extension, } if level > 1 && v.IsDir { if err := f.buildChildNode(&childNode, v, op, level); err != nil { return err } } node.Children = append(node.Children, childNode) } return nil } func (f *FileService) buildChildNode(childNode *response.FileTree, fileInfo *files.FileInfo, op request.FileOption, level int) error { op.Path = fileInfo.Path subInfo, err := files.NewFileInfo(op.FileOption) if err != nil { if os.IsPermission(err) || errors.Is(err, unix.EACCES) { global.LOG.Infof("File Tree: Skipping %s due to permission denied\n", fileInfo.Path) return nil } global.LOG.Errorf("File Tree: Skipping %s due to error: %s\n", fileInfo.Path, err.Error()) return nil } return f.buildFileTree(childNode, subInfo.Items, op, level-1) } func (f *FileService) Create(op request.FileCreate) error { if files.IsInvalidChar(op.Path) { return buserr.New("ErrInvalidChar") } fo := files.NewFileOp() if fo.Stat(op.Path) { return buserr.New(constant.ErrFileIsExist) } mode := op.Mode if mode == 0 { fileInfo, err := os.Stat(filepath.Dir(op.Path)) if err == nil { mode = int64(fileInfo.Mode().Perm()) } else { mode = 0755 } } if op.IsDir { return fo.CreateDirWithMode(op.Path, fs.FileMode(mode)) } if op.IsLink { if !fo.Stat(op.LinkPath) { return buserr.New(constant.ErrLinkPathNotFound) } return fo.LinkFile(op.LinkPath, op.Path, op.IsSymlink) } return fo.CreateFileWithMode(op.Path, fs.FileMode(mode)) } func (f *FileService) Delete(op request.FileDelete) error { fo := files.NewFileOp() recycleBinStatus, _ := settingRepo.Get(settingRepo.WithByKey("FileRecycleBin")) if recycleBinStatus.Value == "disable" { op.ForceDelete = true } if op.ForceDelete { if op.IsDir { return fo.DeleteDir(op.Path) } else { return fo.DeleteFile(op.Path) } } if err := NewIRecycleBinService().Create(request.RecycleBinCreate{SourcePath: op.Path}); err != nil { return err } return favoriteRepo.Delete(favoriteRepo.WithByPath(op.Path)) } func (f *FileService) BatchDelete(op request.FileBatchDelete) error { fo := files.NewFileOp() if op.IsDir { for _, file := range op.Paths { if err := fo.DeleteDir(file); err != nil { return err } } } else { for _, file := range op.Paths { if err := fo.DeleteFile(file); err != nil { return err } } } return nil } func (f *FileService) ChangeMode(op request.FileCreate) error { fo := files.NewFileOp() return fo.ChmodR(op.Path, op.Mode, op.Sub) } func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error { fo := files.NewFileOp() for _, path := range op.Paths { if !fo.Stat(path) { return buserr.New(constant.ErrPathNotFound) } if err := fo.ChownR(path, op.User, op.Group, op.Sub); err != nil { return err } if err := fo.ChmodR(path, op.Mode, op.Sub); err != nil { return err } } return nil } func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error { fo := files.NewFileOp() return fo.ChownR(req.Path, req.User, req.Group, req.Sub) } func (f *FileService) Compress(c request.FileCompress) error { fo := files.NewFileOp() if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) { return buserr.New(constant.ErrFileIsExist) } return fo.Compress(c.Files, c.Dst, c.Name, files.CompressType(c.Type), c.Secret) } func (f *FileService) DeCompress(c request.FileDeCompress) error { fo := files.NewFileOp() if c.Type == "tar" && len(c.Secret) != 0 { c.Type = "tar.gz" } return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type), c.Secret) } func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) { info, err := files.NewFileInfo(files.FileOption{ Path: op.Path, Expand: true, IsDetail: op.IsDetail, }) if err != nil { return response.FileInfo{}, err } content := []byte(info.Content) if len(content) > 1024 { content = content[:1024] } if !utf8.Valid(content) { _, decodeName, _ := charset.DetermineEncoding(content, "") if decodeName == "windows-1252" { reader := strings.NewReader(info.Content) item := transform.NewReader(reader, simplifiedchinese.GBK.NewDecoder()) contents, err := io.ReadAll(item) if err != nil { return response.FileInfo{}, err } info.Content = string(contents) } } return response.FileInfo{FileInfo: *info}, nil } func (f *FileService) SaveContent(edit request.FileEdit) error { info, err := files.NewFileInfo(files.FileOption{ Path: edit.Path, Expand: false, }) if err != nil { return err } fo := files.NewFileOp() return fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode) } func (f *FileService) ChangeName(req request.FileRename) error { if files.IsInvalidChar(req.NewName) { return buserr.New("ErrInvalidChar") } fo := files.NewFileOp() return fo.Rename(req.OldName, req.NewName) } func (f *FileService) Wget(w request.FileWget) (string, error) { fo := files.NewFileOp() key := "file-wget-" + common.GetUuid() return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, w.IgnoreCertificate) } func (f *FileService) MvFile(m request.FileMove) error { fo := files.NewFileOp() if !fo.Stat(m.NewPath) { return buserr.New(constant.ErrPathNotFound) } for _, oldPath := range m.OldPaths { if !fo.Stat(oldPath) { return buserr.WithName(constant.ErrFileNotFound, oldPath) } if oldPath == m.NewPath || strings.Contains(m.NewPath, filepath.Clean(oldPath)+"/") { return buserr.New(constant.ErrMovePathFailed) } } if m.Type == "cut" { return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover) } var errs []error if m.Type == "copy" { for _, src := range m.OldPaths { if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil { errs = append(errs, err) global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error()) } } } var errString string for _, err := range errs { errString += err.Error() + "\n" } if errString != "" { return errors.New(errString) } return nil } func (f *FileService) FileDownload(d request.FileDownload) (string, error) { filePath := d.Paths[0] if d.Compress { tempPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().UnixNano())) if err := os.MkdirAll(tempPath, os.ModePerm); err != nil { return "", err } fo := files.NewFileOp() if err := fo.Compress(d.Paths, tempPath, d.Name, files.CompressType(d.Type), ""); err != nil { return "", err } filePath = filepath.Join(tempPath, d.Name) } return filePath, nil } func (f *FileService) DirSize(req request.DirSizeReq) (response.DirSizeRes, error) { var ( res response.DirSizeRes ) if req.Path == "/proc" { return res, nil } cmd := exec.Command("du", "-s", req.Path) output, err := cmd.Output() if err == nil { fields := strings.Fields(string(output)) if len(fields) == 2 { var cmdSize int64 _, err = fmt.Sscanf(fields[0], "%d", &cmdSize) if err == nil { res.Size = float64(cmdSize * 1024) return res, nil } } } fo := files.NewFileOp() size, err := fo.GetDirSize(req.Path) if err != nil { return res, err } res.Size = size return res, nil } func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) { logFilePath := "" switch req.Type { case constant.TypeWebsite: website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID)) if err != nil { return nil, err } nginx, err := getNginxFull(&website) if err != nil { return nil, err } sitePath := path.Join(nginx.SiteDir, "sites", website.Alias) logFilePath = path.Join(sitePath, "log", req.Name) case constant.TypePhp: php, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) if err != nil { return nil, err } logFilePath = php.GetLogPath() case constant.TypeSSL: ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.ID)) if err != nil { return nil, err } logFilePath = ssl.GetLogPath() case constant.TypeSystem: fileName := "" if len(req.Name) == 0 || req.Name == time.Now().Format("2006-01-02") { fileName = "1Panel.log" } else { fileName = "1Panel-" + req.Name + ".log" } logFilePath = path.Join(global.CONF.System.DataDir, "log", fileName) if _, err := os.Stat(logFilePath); err != nil { fileGzPath := path.Join(global.CONF.System.DataDir, "log", fileName+".gz") if _, err := os.Stat(fileGzPath); err != nil { return nil, buserr.New("ErrHttpReqNotFound") } if err := handleGunzip(fileGzPath); err != nil { return nil, fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err) } } case constant.TypeTask: var opts []repo.DBOption if req.TaskID != "" { opts = append(opts, taskRepo.WithByID(req.TaskID)) } else { opts = append(opts, commonRepo.WithByType(req.TaskType), taskRepo.WithOperate(req.TaskOperate), taskRepo.WithResourceID(req.ID)) } taskModel, err := taskRepo.GetFirst(opts...) if err != nil { return nil, err } logFilePath = taskModel.LogFile case constant.TypeImagePull, constant.TypeImagePush, constant.TypeImageBuild, constant.TypeComposeCreate: logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name)) } lines, isEndOfFile, total, err := files.ReadFileByLine(logFilePath, req.Page, req.PageSize, req.Latest) if err != nil { return nil, err } res := &response.FileLineContent{ Content: strings.Join(lines, "\n"), End: isEndOfFile, Path: logFilePath, Total: total, } return res, nil }