initial, not sure if it works
This commit is contained in:
commit
e4ca1a2ec2
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dcfinfo
|
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
10
Makefile
Normal file
10
Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
default: test
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./dcfinfo
|
||||||
|
|
||||||
|
build:
|
||||||
|
cd cmd/dcfinfo && go build -o ../../dcfinfo
|
23
README.md
Normal file
23
README.md
Normal file
@ -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
|
169
dcf.go
Normal file
169
dcf.go
Normal file
@ -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
|
||||||
|
}
|
10
dcf_test.go
Normal file
10
dcf_test.go
Normal file
@ -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.
|
||||||
|
}
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -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
|
||||||
|
)
|
9
go.sum
Normal file
9
go.sum
Normal file
@ -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=
|
55
helpers.go
Normal file
55
helpers.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user