Initial vcom TUI prototype
This commit is contained in:
commit
059f925e00
16 changed files with 3227 additions and 0 deletions
268
internal/fs/scan.go
Normal file
268
internal/fs/scan.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue