vcom/internal/fs/scan.go
2026-04-22 22:10:50 +03:00

268 lines
5.2 KiB
Go

package vfs
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
)
type ListOptions struct {
ShowHidden bool
DirsFirst bool
SortBy string
SortReverse bool
}
func ListDir(path string, options ListOptions) ([]Entry, error) {
resolvedPath := path
if resolvedPath == "" {
resolvedPath = "."
}
dirEntries, err := os.ReadDir(resolvedPath)
if err != nil {
return nil, fmt.Errorf("read dir %s: %w", resolvedPath, err)
}
entries := make([]Entry, 0, len(dirEntries)+1)
if parent := filepath.Dir(resolvedPath); parent != resolvedPath {
entries = append(entries, Entry{
Name: "..",
Path: parent,
IsDir: true,
IsParent: true,
})
}
for _, dirEntry := range dirEntries {
name := dirEntry.Name()
hidden := strings.HasPrefix(name, ".")
if hidden && !options.ShowHidden {
continue
}
fullPath := filepath.Join(resolvedPath, name)
info, err := dirEntry.Info()
if err != nil {
continue
}
entry := Entry{
Name: name,
Path: fullPath,
Extension: ext(name),
Mode: info.Mode(),
Size: info.Size(),
ModifiedAt: info.ModTime(),
IsDir: info.IsDir(),
IsHidden: hidden,
}
if createdAt, ok := statBirthTime(fullPath); ok {
entry.CreatedAt = createdAt
entry.CreatedKnown = true
}
entries = append(entries, entry)
}
sort.SliceStable(entries, func(i, j int) bool {
left, right := entries[i], entries[j]
if left.IsParent != right.IsParent {
return left.IsParent
}
if options.DirsFirst && left.IsDir != right.IsDir {
return left.IsDir
}
comparison := compareEntries(left, right, options.SortBy)
if options.SortReverse {
return comparison > 0
}
return comparison < 0
})
return entries, nil
}
func compareEntries(left Entry, right Entry, sortBy string) int {
switch strings.ToLower(strings.TrimSpace(sortBy)) {
case "size":
if left.Size != right.Size {
return cmpInt64(left.Size, right.Size)
}
case "modified":
if !left.ModifiedAt.Equal(right.ModifiedAt) {
return cmpTimeDesc(left.ModifiedAt, right.ModifiedAt)
}
case "created":
if left.CreatedKnown != right.CreatedKnown {
if left.CreatedKnown {
return -1
}
return 1
}
if !left.CreatedAt.Equal(right.CreatedAt) {
return cmpTimeDesc(left.CreatedAt, right.CreatedAt)
}
case "extension":
if left.Extension != right.Extension {
return strings.Compare(left.Extension, right.Extension)
}
}
return strings.Compare(strings.ToLower(left.Name), strings.ToLower(right.Name))
}
func cmpInt64(left int64, right int64) int {
switch {
case left < right:
return -1
case left > right:
return 1
default:
return 0
}
}
func cmpTimeDesc(left time.Time, right time.Time) int {
switch {
case left.Equal(right):
return 0
case left.After(right):
return -1
default:
return 1
}
}
func statBirthTime(path string) (time.Time, bool) {
var stx unix.Statx_t
if err := unix.Statx(unix.AT_FDCWD, path, unix.AT_STATX_SYNC_AS_STAT, unix.STATX_BTIME, &stx); err == nil {
if stx.Mask&unix.STATX_BTIME != 0 {
return time.Unix(int64(stx.Btime.Sec), int64(stx.Btime.Nsec)), true
}
}
info, err := os.Lstat(path)
if err != nil {
return time.Time{}, false
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return time.Time{}, false
}
seconds := int64(stat.Ctim.Sec)
nanos := int64(stat.Ctim.Nsec)
if seconds == 0 && nanos == 0 {
return time.Time{}, false
}
return time.Unix(seconds, nanos), true
}
func DirectorySize(path string) (int64, error) {
var total int64
err := filepath.WalkDir(path, func(current string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
total += info.Size()
return nil
})
if err != nil {
return 0, err
}
return total, nil
}
func FindSelected(entries []Entry, key string) int {
for idx, entry := range entries {
if entry.MatchKey() == key {
return idx
}
}
return 0
}
func HumanSize(size int64) string {
if size < 0 {
return "?"
}
if size < 1024 {
return fmt.Sprintf("%d B", size)
}
units := []string{"KB", "MB", "GB", "TB"}
value := float64(size)
for _, unit := range units {
value /= 1024
if value < 1024 {
return fmt.Sprintf("%.1f %s", value, unit)
}
}
return fmt.Sprintf("%.1f PB", value/1024)
}
func ShortTime(t time.Time) string {
if t.IsZero() {
return "n/a"
}
return t.Format("2006-01-02 15:04")
}
func Permissions(mode fs.FileMode) string {
return mode.String()
}
func IsBinarySample(data []byte) bool {
if len(data) == 0 {
return false
}
var controls int
for _, b := range data {
if b == 0 {
return true
}
if b < 9 || (b > 13 && b < 32) {
controls++
}
}
return controls > len(data)/10
}
func SafeBase(path string) string {
base := filepath.Base(path)
if base == "." || base == string(filepath.Separator) {
return path
}
return base
}
func JoinPath(path string, name string) string {
return filepath.Join(path, name)
}
func ErrOverwrite(path string) error {
return fmt.Errorf("target already exists: %s", path)
}
func IsNotExist(err error) bool {
return errors.Is(err, os.ErrNotExist)
}