latest
This commit is contained in:
177
pkg/dcf/dcf.go
Normal file
177
pkg/dcf/dcf.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package dcf
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
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 struct {
|
||||
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() string {
|
||||
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(d.FullFilePath())
|
||||
}
|
||||
|
||||
func 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.
|
||||
// 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,
|
||||
// walk the filesystems to find images and videos and populate the returned DCFStores.
|
||||
func GetDCFStores(requestedCount int) (*[]DCFStore, error) {
|
||||
dcfStorePaths, err := findDCFMountPoints(requestedCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dcfStores := []DCFStore{}
|
||||
for _, dcfStorePath := range dcfStorePaths {
|
||||
dcfStore := DCFStore{
|
||||
RootDirectory: dcfStorePath,
|
||||
Images: make([]*Image, 0),
|
||||
Videos: make([]*Video, 0),
|
||||
}
|
||||
|
||||
err := filepath.Walk(dcfStorePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && isImageFile(path) {
|
||||
image := Image{}
|
||||
image.Path = strings.TrimPrefix(path, dcfStorePath+"/")
|
||||
image.Size = info.Size()
|
||||
image.Extension = filepath.Ext(path)
|
||||
image.DCFStoreRoot = dcfStorePath
|
||||
dcfStore.Images = append(dcfStore.Images, &image)
|
||||
}
|
||||
if !info.IsDir() && isVideoFile(path) {
|
||||
video := Video{}
|
||||
video.Path = strings.TrimPrefix(path, dcfStorePath+"/")
|
||||
video.Size = info.Size()
|
||||
video.Extension = filepath.Ext(path)
|
||||
video.DCFStoreRoot = dcfStorePath
|
||||
dcfStore.Videos = append(dcfStore.Videos, &video)
|
||||
}
|
||||
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
pkg/dcf/dcf_test.go
Normal file
10
pkg/dcf/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.
|
||||
}
|
||||
75
pkg/dcf/helpers.go
Normal file
75
pkg/dcf/helpers.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package dcf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"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 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{}
|
||||
var erro error
|
||||
mountpoints, err := findAllMountPoints()
|
||||
|
||||
if erro != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, mountpoint := range mountpoints {
|
||||
dcimPath := filepath.Join(mountpoint, "DCIM")
|
||||
privatePath := filepath.Join(mountpoint, "PRIVATE")
|
||||
privatePath = filepath.Join(mountpoint, "M4ROOT")
|
||||
shouldKeep := false
|
||||
if _, err := os.Stat(dcimPath); err == nil {
|
||||
shouldKeep = true
|
||||
slog.Debug(fmt.Sprintf("Found DCIM directory at %s\n", dcimPath))
|
||||
}
|
||||
if _, err := os.Stat(privatePath); err == nil {
|
||||
shouldKeep = true
|
||||
slog.Debug(fmt.Sprintf("Found M4ROOT directory at %s\n", privatePath))
|
||||
}
|
||||
if shouldKeep {
|
||||
slog.Debug(fmt.Sprintf("Keeping mountpoint %s\n", mountpoint))
|
||||
filteredMountpoints = append(filteredMountpoints, mountpoint)
|
||||
if (requestedCount > 0) && (len(filteredMountpoints)+1 >= requestedCount) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredMountpoints, nil
|
||||
}
|
||||
|
||||
func contains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user