package services import ( "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" "time" ) type FSService struct { root string } type Entry struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` IsDir bool `json:"isDir"` ModTime time.Time `json:"modTime"` } func NewFSService(root string) *FSService { return &FSService{root: filepath.Clean(root)} } func (s *FSService) ResolvePath(raw string) (string, error) { clean := filepath.Clean(filepath.Join(s.root, raw)) rootWithSep := s.root + string(os.PathSeparator) if clean != s.root && !strings.HasPrefix(clean, rootWithSep) { return "", errors.New("path out of root directory") } return clean, nil } func (s *FSService) List(raw string) ([]Entry, error) { path, err := s.ResolvePath(raw) if err != nil { return nil, err } entries, err := os.ReadDir(path) if err != nil { return nil, fmt.Errorf("read dir: %w", err) } result := make([]Entry, 0, len(entries)) for _, e := range entries { info, err := e.Info() if err != nil { continue } full := filepath.Join(path, e.Name()) rel, _ := filepath.Rel(s.root, full) result = append(result, Entry{ Name: e.Name(), Path: filepath.ToSlash(rel), Size: info.Size(), IsDir: e.IsDir(), ModTime: info.ModTime(), }) } sort.Slice(result, func(i, j int) bool { if result[i].IsDir != result[j].IsDir { return result[i].IsDir } return strings.ToLower(result[i].Name) < strings.ToLower(result[j].Name) }) return result, nil } func (s *FSService) Delete(raw string) error { path, err := s.ResolvePath(raw) if err != nil { return err } return os.RemoveAll(path) } func (s *FSService) Rename(rawPath, newName string) error { if strings.Contains(newName, "/") || strings.Contains(newName, "\\") { return errors.New("invalid name") } path, err := s.ResolvePath(rawPath) if err != nil { return err } target := filepath.Join(filepath.Dir(path), newName) target, err = s.ResolvePath(mustRel(s.root, target)) if err != nil { return err } return os.Rename(path, target) } func (s *FSService) Move(fromRaw, toRaw string) error { fromPath, err := s.ResolvePath(fromRaw) if err != nil { return err } toPath, err := s.ResolvePath(toRaw) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(toPath), 0o755); err != nil { return err } return os.Rename(fromPath, toPath) } func (s *FSService) SaveUploadedFile(src io.Reader, targetRaw string) error { targetPath, err := s.ResolvePath(targetRaw) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { return err } dst, err := os.Create(targetPath) if err != nil { return err } defer dst.Close() _, err = io.Copy(dst, src) return err } func mustRel(root, target string) string { rel, err := filepath.Rel(root, target) if err != nil { return "." } return rel }