From e4ca1a2ec20e6ac23d1d13e02a1b6069912a4fd9 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 12 May 2024 09:37:34 -0700 Subject: [PATCH] initial, not sure if it works --- .gitignore | 1 + LICENSE | 15 +++++ Makefile | 10 ++++ README.md | 23 +++++++ dcf.go | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++ dcf_test.go | 10 ++++ go.mod | 11 ++++ go.sum | 9 +++ helpers.go | 55 +++++++++++++++++ 9 files changed, 303 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 dcf.go create mode 100644 dcf_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helpers.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73f3499 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dcfinfo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6486431 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2bc4cc0 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +default: test + +test: + go test -v ./... + +run: build + ./dcfinfo + +build: + cd cmd/dcfinfo && go build -o ../../dcfinfo diff --git a/README.md b/README.md new file mode 100644 index 0000000..76ee17e --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# dcf + +Golang reader implementation of the so-called "Design rule for Camera File +system" or DCF, aka JEITA (Japan Electronics and Information Technology +Industries Association) specification number CP-3461. + +[wikipedia.org/wiki/Design_rule_for_Camera_File_system](https://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system) + +The DCF specification is why your digital camera puts images and videos in +`DCIM` and `PRIVATE/M4ROOT` directories on the memory card. + +# why + +I wanted to copy images off my memory cards for processing and like +overengineering and reusable code. + +# author + +sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)> + +# license + +WTFPL diff --git a/dcf.go b/dcf.go new file mode 100644 index 0000000..46234de --- /dev/null +++ b/dcf.go @@ -0,0 +1,169 @@ +package dcf + +import ( + "os" + "path/filepath" + "strings" + "github.com/dustin/go-humanize" + "github.com/shirou/gopsutil/disk" +) + +var imageExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".raw", ".arw"} +var videoExtensions = []string{".mp4", ".mov", ".avi", ".mkv", ".wmv"} + +// DCFStore represents a DCF store, which is a directory sturcture that contains +// images and videos in the root of a mounted filesystem. +type DCFStore struct { + RootDirectory string + Images []Image + Videos []Video +} + +// DCFObject represents a file in a DCF store. It is embedded in Image and Video types. +type DCFObject { + DCFStoreRoot string + Path string + Size int64 + Extension string +} + +// Image represents an image file on a DCF store. +type Image struct { + DCFObject +} + +// Video represents a video file on a DCF store. +type Video struct { + DCFObject +} + +func (d *DCFObject) FullFilePath() { + return filepath.Join(d.DCFStoreRoot, d.Path) +} + +// VideosCount returns the number of videos in the DCF store. +func (d *DCFStore) VideosCount() int { + return len(d.Videos) +} + +// ImagesCount returns the number of images in the DCF store. +func (d *DCFStore) ImagesCount() int { + return len(d.Images) +} + +func (d *DCFStore) TotalImageSize() int64 { + totalSize := int64(0) + for _, image := range d.Images { + totalSize += image.Size + } + return totalSize +} + +func (d *DCFStore) TotalVideoSize() int64 { + totalSize := int64(0) + for _, video := range d.Videos { + totalSize += video.Size + } + return totalSize +} + +func (d *DCFStore) TotalSize() int64 { + return d.TotalImageSize() + d.TotalVideoSize() +} + +func (d *DCFStore) String() string { + return fmt.Sprintf("DCFStore{RootDirectory: %s, Images: %d, Videos: %d, TotalSize: %s}", d.RootDirectory, d.ImagesCount(), d.VideosCount(), humanize.Bytes(uint64(d.TotalSize())) +} + +func (v *Video) String() string { + return fmt.Sprintf("Video{Path: %s, Size: %d, Extension: %s}", v.Path, v.Size, v.Extension) +} + +func (i *Image) String() string { + return fmt.Sprintf("Image{Path: %s, Size: %d, Extension: %s}", i.Path, i.Size, i.Extension) +} + +// Hash() returns the SHA256 hash of the file as a hex string. +func (d *DCFObject) Hash() (string, error) { + return pathToSHA256(i.FullFilePath()) +} + +function pathToSHA256 (path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} + + +// GetDCFStores returns a list of DCF stores found on the system in the root of any mounted filesystems. +// It does not mount filesystems or search outside of filesystem root directories. It does, however, +// walk the filesystems to find images and videos and populate the returned DCFStores. +func GetDCFStores() (*[]DCFStore, error) { + dcfStorePaths, err := findDCFMountPoints() + if err != nil { + return nil, err + } + dcfStores := []DCFStore{} + for _, dcfStorePath := range dcfStorePaths { + dcfStore := DCFStore{ + RootDirectory: dcfStorePath, + Images: []Image{}, + Videos: []Video{}, + } + + err := filepath.Walk(dcfStorePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && isImageFile(path) { + dcfStore.Images = append(dcfStore.Images, Image{ + Path: strings.TrimPrefix(path, dcfStorePath+"/"), + Size: info.Size(), + Extension: filepath.Ext(path), + DCFStoreRoot: dcfStorePath, + }) + } + if !info.IsDir() && isVideoFile(path) { + dcfStore.Videos = append(dcfStore.Videos, Video{ + Path: strings.TrimPrefix(path, dcfStorePath+"/") + Size: info.Size(), + Extension: filepath.Ext(path), + DCFStoreRoot: dcfStorePath, + }) + } + return nil + }) + if err != nil { + return nil, err + } + dcfStores = append(dcfStores, dcfStore) + } + return &dcfStores, nil +} + +func isImageFile(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + for _, imageExt := range imageExtensions { + if ext == imageExt { + return true + } + } + return false +} + +func isVideoFile(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + for _, videoExt := range videoExtensions { + if ext == videoExt { + return true + } + } + return false +} diff --git a/dcf_test.go b/dcf_test.go new file mode 100644 index 0000000..1cbca2a --- /dev/null +++ b/dcf_test.go @@ -0,0 +1,10 @@ +package dcf + +import ( + "testing" +) + +func TestCompile(t *testing.T) { + // This is a placeholder test to ensure that the module compiles successfully. + // You can add more meaningful tests here once you start implementing your code. +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0155051 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.eeqj.de/sneak/dcf + +go 1.22.1 + +require github.com/shirou/gopsutil v3.21.11+incompatible + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1753c58 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..fbfa667 --- /dev/null +++ b/helpers.go @@ -0,0 +1,55 @@ +package dcf + +import ( + "os" + "path/filepath" + + "github.com/shirou/gopsutil/disk" +) + +func findAllMountPoints() ([]string, error) { + mountpoints := []string{} + partitions, err := disk.Partitions(false) // physical devices only, so false + if err != nil { + return nil, err + } + for _, partition := range partitions { + if !contains(mountpoints, partition.Mountpoint) { + mountpoints = append(mountpoints, partition.Mountpoint) + } + } + return mountpoints, nil +} + +func findDCFMountPoints() ([]string, error) { + filteredMountpoints := []string{} + var err error + mountpoints, err := findAllMountPoints() + if err != nil { + return nil, err + } + for _, mountpoint := range mountpoints { + dcimPath := filepath.Join(mountpoint, "DCIM") + privatePath := filepath.Join(mountpoint, "PRIVATE") + shouldKeep := false + if _, err := os.Stat(dcimPath); err == nil { + shouldKeep = true + } + if _, err := os.Stat(privatePath); err == nil { + shouldKeep = true + } + if shouldKeep { + filteredMountpoints = append(filteredMountpoints, mountpoint) + } + } + return filteredMountpoints, nil +} + +func contains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +}