feat: field-sync-gui MVP — Wails GUI ingest SD→local+S3 (4 slots //)
This commit is contained in:
75
internal/detect/detect.go
Normal file
75
internal/detect/detect.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package detect
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
)
|
||||
|
||||
// SDCard represents a detected removable storage device
|
||||
type SDCard struct {
|
||||
Mountpoint string `json:"mountpoint"`
|
||||
Label string `json:"label"`
|
||||
Device string `json:"device"`
|
||||
Fstype string `json:"fstype"`
|
||||
TotalBytes uint64 `json:"totalBytes"`
|
||||
FreeBytes uint64 `json:"freeBytes"`
|
||||
MediaFiles []string `json:"mediaFiles"`
|
||||
}
|
||||
|
||||
// mediaExtensions is the set of file extensions we care about
|
||||
var mediaExtensions = map[string]bool{
|
||||
".mp4": true, ".mov": true, ".jpg": true, ".jpeg": true,
|
||||
".png": true, ".raw": true, ".gpr": true, ".360": true,
|
||||
".lrv": true, ".thm": true,
|
||||
}
|
||||
|
||||
// Detect returns all removable partitions that look like SD cards / media
|
||||
func Detect() []SDCard {
|
||||
partitions, err := disk.Partitions(true)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cards []SDCard
|
||||
for _, p := range partitions {
|
||||
if !isRemovable(p) {
|
||||
continue
|
||||
}
|
||||
usage, err := disk.Usage(p.Mountpoint)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
card := SDCard{
|
||||
Mountpoint: p.Mountpoint,
|
||||
Label: filepath.Base(p.Mountpoint),
|
||||
Device: p.Device,
|
||||
Fstype: p.Fstype,
|
||||
TotalBytes: usage.Total,
|
||||
FreeBytes: usage.Free,
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
// isRemovable heuristic: opts contain "removable" or path looks like /media/
|
||||
func isRemovable(p disk.PartitionStat) bool {
|
||||
opts := strings.Join(p.Opts, ",")
|
||||
if strings.Contains(opts, "removable") {
|
||||
return true
|
||||
}
|
||||
mp := p.Mountpoint
|
||||
if strings.HasPrefix(mp, "/media/") || strings.HasPrefix(mp, "/run/media/") ||
|
||||
strings.HasPrefix(mp, "/Volumes/") || strings.HasPrefix(mp, "/mnt/sd") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasMediaExtension returns true if path has a known media extension
|
||||
func HasMediaExtension(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return mediaExtensions[ext]
|
||||
}
|
||||
25
internal/detect/detect_test.go
Normal file
25
internal/detect/detect_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package detect
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDetectNoPanic(t *testing.T) {
|
||||
cards := Detect()
|
||||
// May be empty in CI — just verify no panic
|
||||
_ = cards
|
||||
}
|
||||
|
||||
func TestHasMediaExtension(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"clip.mp4": true,
|
||||
"photo.JPG": true,
|
||||
"photo.raw": true,
|
||||
"file.txt": false,
|
||||
"archive.zip": false,
|
||||
}
|
||||
for path, want := range cases {
|
||||
got := HasMediaExtension(path)
|
||||
if got != want {
|
||||
t.Errorf("HasMediaExtension(%q) = %v, want %v", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user