Files

132 lines
3.3 KiB
Go

package files
import (
"errors"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"tea.chunkbyte.com/kato/go-worm/lib/config"
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
"tea.chunkbyte.com/kato/go-worm/lib/models"
)
var (
ErrInvalidPath = errors.New("path must be an absolute accessible Windows path")
ErrOutsideRoot = errors.New("path is outside AGENT_FILE_ROOT")
)
func AllowedPath(root, raw string, requireExisting bool) (string, error) {
if strings.TrimSpace(raw) == "" || !filepath.IsAbs(raw) {
return "", ErrInvalidPath
}
clean := filepath.Clean(raw)
if requireExisting {
resolved, err := CanonicalExistingPath(clean)
if err != nil {
return "", err
}
clean = resolved
}
if root != "" && !isWithin(root, clean) {
return "", ErrOutsideRoot
}
return clean, nil
}
func CanonicalExistingPath(path string) (string, error) {
abs, err := filepath.Abs(filepath.Clean(path))
if err != nil {
return "", err
}
if _, err := os.Stat(abs); err != nil {
return "", err
}
resolved, err := filepath.EvalSymlinks(abs)
if err == nil {
return filepath.Abs(resolved)
}
return abs, nil
}
func ParseDepth(raw string) (int, error) {
if raw == "" {
return 0, nil
}
depth, err := strconv.Atoi(raw)
if err != nil || (depth != 0 && depth != 1) {
return 0, errors.New("depth must be 0 or 1")
}
return depth, nil
}
func ListDirectory(dir string, depth int) ([]models.FileItem, error) {
result := make([]models.FileItem, 0)
var walk func(string, string, int) error
walk = func(current, relative string, remaining int) error {
entries, err := os.ReadDir(current)
if err != nil {
return err
}
for _, entry := range entries {
if len(result) >= config.MaxListEntries {
return errors.New("directory listing exceeds entry limit")
}
info, err := entry.Info()
if err != nil {
continue
}
entryPath := filepath.Join(current, entry.Name())
rel := filepath.Join(relative, entry.Name())
kind := "file"
if info.IsDir() {
kind = "dir"
}
result = append(result, models.FileItem{
Name: entry.Name(),
Path: rel,
Type: kind,
Size: info.Size(),
Modified: info.ModTime().UTC(),
})
if remaining > 0 && info.IsDir() && entry.Type()&os.ModeSymlink == 0 {
if err := walk(entryPath, rel, remaining-1); err != nil {
return err
}
}
}
return nil
}
if err := walk(dir, "", depth); err != nil {
return nil, err
}
sort.Slice(result, func(i, j int) bool {
return strings.ToLower(result[i].Path) < strings.ToLower(result[j].Path)
})
return result, nil
}
func WritePathError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, ErrInvalidPath):
helpers.WriteError(w, http.StatusBadRequest, err.Error())
case errors.Is(err, ErrOutsideRoot), errors.Is(err, os.ErrPermission):
helpers.WriteError(w, http.StatusForbidden, "path is not accessible")
case errors.Is(err, os.ErrNotExist):
helpers.WriteError(w, http.StatusNotFound, "path does not exist")
default:
helpers.WriteError(w, http.StatusBadRequest, "path is not accessible")
}
}
func isWithin(root, candidate string) bool {
rel, err := filepath.Rel(root, candidate)
if err != nil {
return false
}
return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) && !filepath.IsAbs(rel)
}