latest
This commit is contained in:
parent
e4ca1a2ec2
commit
0c45e7b301
6
Makefile
6
Makefile
|
@ -4,7 +4,11 @@ test:
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
./dcfinfo
|
DEBUG=1 ./dcfinfo
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
go env -w CGO_ENABLED="0"
|
||||||
cd cmd/dcfinfo && go build -o ../../dcfinfo
|
cd cmd/dcfinfo && go build -o ../../dcfinfo
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f dcfinfo
|
||||||
|
|
|
@ -14,6 +14,12 @@ The DCF specification is why your digital camera puts images and videos in
|
||||||
I wanted to copy images off my memory cards for processing and like
|
I wanted to copy images off my memory cards for processing and like
|
||||||
overengineering and reusable code.
|
overengineering and reusable code.
|
||||||
|
|
||||||
|
# bugs
|
||||||
|
|
||||||
|
does not yet handle "dcf file groups" where multiple objects share the same
|
||||||
|
index number. if you need this, send me a link to a zip or tar of a memory
|
||||||
|
card that uses it and i'll see what i can do.
|
||||||
|
|
||||||
# author
|
# author
|
||||||
|
|
||||||
sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)>
|
sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)>
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -2,10 +2,14 @@ module git.eeqj.de/sneak/dcf
|
||||||
|
|
||||||
go 1.22.1
|
go 1.22.1
|
||||||
|
|
||||||
require github.com/shirou/gopsutil v3.21.11+incompatible
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1
|
||||||
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/lmittmann/tint v1.0.4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -1,9 +1,15 @@
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
|
||||||
|
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
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/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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
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.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package dcf
|
package dcf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/shirou/gopsutil/disk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var imageExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".raw", ".arw"}
|
var imageExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".raw", ".arw"}
|
||||||
|
@ -15,16 +18,16 @@ var videoExtensions = []string{".mp4", ".mov", ".avi", ".mkv", ".wmv"}
|
||||||
// images and videos in the root of a mounted filesystem.
|
// images and videos in the root of a mounted filesystem.
|
||||||
type DCFStore struct {
|
type DCFStore struct {
|
||||||
RootDirectory string
|
RootDirectory string
|
||||||
Images []Image
|
Images []*Image
|
||||||
Videos []Video
|
Videos []*Video
|
||||||
}
|
}
|
||||||
|
|
||||||
// DCFObject represents a file in a DCF store. It is embedded in Image and Video types.
|
// DCFObject represents a file in a DCF store. It is embedded in Image and Video types.
|
||||||
type DCFObject {
|
type DCFObject struct {
|
||||||
DCFStoreRoot string
|
DCFStoreRoot string
|
||||||
Path string
|
Path string
|
||||||
Size int64
|
Size int64
|
||||||
Extension string
|
Extension string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image represents an image file on a DCF store.
|
// Image represents an image file on a DCF store.
|
||||||
|
@ -37,7 +40,7 @@ type Video struct {
|
||||||
DCFObject
|
DCFObject
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DCFObject) FullFilePath() {
|
func (d *DCFObject) FullFilePath() string {
|
||||||
return filepath.Join(d.DCFStoreRoot, d.Path)
|
return filepath.Join(d.DCFStoreRoot, d.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +75,12 @@ func (d *DCFStore) TotalSize() int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DCFStore) String() string {
|
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()))
|
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 {
|
func (v *Video) String() string {
|
||||||
|
@ -85,10 +93,10 @@ func (i *Image) String() string {
|
||||||
|
|
||||||
// Hash() returns the SHA256 hash of the file as a hex string.
|
// Hash() returns the SHA256 hash of the file as a hex string.
|
||||||
func (d *DCFObject) Hash() (string, error) {
|
func (d *DCFObject) Hash() (string, error) {
|
||||||
return pathToSHA256(i.FullFilePath())
|
return pathToSHA256(d.FullFilePath())
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathToSHA256 (path string) (string, error) {
|
func pathToSHA256(path string) (string, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -101,12 +109,12 @@ function pathToSHA256 (path string) (string, error) {
|
||||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
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.
|
// GetDCFStores returns a list of DCF stores found on the system in the root of any mounted filesystems.
|
||||||
|
// the integer argument is the number of DCF stores to return. If the argument is 0, all DCF stores are returned.
|
||||||
// It does not mount filesystems or search outside of filesystem root directories. It does, however,
|
// 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.
|
// walk the filesystems to find images and videos and populate the returned DCFStores.
|
||||||
func GetDCFStores() (*[]DCFStore, error) {
|
func GetDCFStores(requestedCount int) (*[]DCFStore, error) {
|
||||||
dcfStorePaths, err := findDCFMountPoints()
|
dcfStorePaths, err := findDCFMountPoints(requestedCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -114,8 +122,8 @@ func GetDCFStores() (*[]DCFStore, error) {
|
||||||
for _, dcfStorePath := range dcfStorePaths {
|
for _, dcfStorePath := range dcfStorePaths {
|
||||||
dcfStore := DCFStore{
|
dcfStore := DCFStore{
|
||||||
RootDirectory: dcfStorePath,
|
RootDirectory: dcfStorePath,
|
||||||
Images: []Image{},
|
Images: make([]*Image, 0),
|
||||||
Videos: []Video{},
|
Videos: make([]*Video, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := filepath.Walk(dcfStorePath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dcfStorePath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
@ -123,20 +131,20 @@ func GetDCFStores() (*[]DCFStore, error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !info.IsDir() && isImageFile(path) {
|
if !info.IsDir() && isImageFile(path) {
|
||||||
dcfStore.Images = append(dcfStore.Images, Image{
|
image := Image{}
|
||||||
Path: strings.TrimPrefix(path, dcfStorePath+"/"),
|
image.Path = strings.TrimPrefix(path, dcfStorePath+"/")
|
||||||
Size: info.Size(),
|
image.Size = info.Size()
|
||||||
Extension: filepath.Ext(path),
|
image.Extension = filepath.Ext(path)
|
||||||
DCFStoreRoot: dcfStorePath,
|
image.DCFStoreRoot = dcfStorePath
|
||||||
})
|
dcfStore.Images = append(dcfStore.Images, &image)
|
||||||
}
|
}
|
||||||
if !info.IsDir() && isVideoFile(path) {
|
if !info.IsDir() && isVideoFile(path) {
|
||||||
dcfStore.Videos = append(dcfStore.Videos, Video{
|
video := Video{}
|
||||||
Path: strings.TrimPrefix(path, dcfStorePath+"/")
|
video.Path = strings.TrimPrefix(path, dcfStorePath+"/")
|
||||||
Size: info.Size(),
|
video.Size = info.Size()
|
||||||
Extension: filepath.Ext(path),
|
video.Extension = filepath.Ext(path)
|
||||||
DCFStoreRoot: dcfStorePath,
|
video.DCFStoreRoot = dcfStorePath
|
||||||
})
|
dcfStore.Videos = append(dcfStore.Videos, &video)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
|
@ -1,8 +1,11 @@
|
||||||
package dcf
|
package dcf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/disk"
|
"github.com/shirou/gopsutil/disk"
|
||||||
)
|
)
|
||||||
|
@ -21,25 +24,42 @@ func findAllMountPoints() ([]string, error) {
|
||||||
return mountpoints, nil
|
return mountpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findDCFMountPoints() ([]string, error) {
|
func dump(x interface{}) {
|
||||||
|
_, file, line, _ := runtime.Caller(1)
|
||||||
|
slog.Debug(fmt.Sprintf("%s:%d %#v\n", file, line, x))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findDCFMountPoints returns a list of strings of the paths of mountpoints that
|
||||||
|
// contain a DCIM or PRIVATE directory, which is how we detect DCF stores. Its
|
||||||
|
// only argument is an integer of how many mountpoints to return. If the
|
||||||
|
// argument is 0, all mountpoints are returned.
|
||||||
|
func findDCFMountPoints(requestedCount int) ([]string, error) {
|
||||||
filteredMountpoints := []string{}
|
filteredMountpoints := []string{}
|
||||||
var err error
|
var erro error
|
||||||
mountpoints, err := findAllMountPoints()
|
mountpoints, err := findAllMountPoints()
|
||||||
if err != nil {
|
|
||||||
|
if erro != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, mountpoint := range mountpoints {
|
for _, mountpoint := range mountpoints {
|
||||||
dcimPath := filepath.Join(mountpoint, "DCIM")
|
dcimPath := filepath.Join(mountpoint, "DCIM")
|
||||||
privatePath := filepath.Join(mountpoint, "PRIVATE")
|
privatePath := filepath.Join(mountpoint, "PRIVATE")
|
||||||
|
privatePath = filepath.Join(mountpoint, "M4ROOT")
|
||||||
shouldKeep := false
|
shouldKeep := false
|
||||||
if _, err := os.Stat(dcimPath); err == nil {
|
if _, err := os.Stat(dcimPath); err == nil {
|
||||||
shouldKeep = true
|
shouldKeep = true
|
||||||
|
slog.Debug(fmt.Sprintf("Found DCIM directory at %s\n", dcimPath))
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(privatePath); err == nil {
|
if _, err := os.Stat(privatePath); err == nil {
|
||||||
shouldKeep = true
|
shouldKeep = true
|
||||||
|
slog.Debug(fmt.Sprintf("Found M4ROOT directory at %s\n", privatePath))
|
||||||
}
|
}
|
||||||
if shouldKeep {
|
if shouldKeep {
|
||||||
|
slog.Debug(fmt.Sprintf("Keeping mountpoint %s\n", mountpoint))
|
||||||
filteredMountpoints = append(filteredMountpoints, mountpoint)
|
filteredMountpoints = append(filteredMountpoints, mountpoint)
|
||||||
|
if (requestedCount > 0) && (len(filteredMountpoints)+1 >= requestedCount) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filteredMountpoints, nil
|
return filteredMountpoints, nil
|
Loading…
Reference in New Issue