bring the repo up to org standards
Adopt the standard tooling: a Makefile of thin shims over a full scripts-to-rule-them-all `script/` set, the canonical `.golangci.yml` vendored byte-identical from `sneak/prompts`, docker-only linting via `Dockerfile.lint`, a `Dockerfile` and Gitea workflow that gate every push, and prettier/editorconfig/dockerignore config. `make build` pointed at a `cmd/dcfinfo` that is not in the tree and could never have succeeded; this repo is a library, so `build` is now the compile check over every package. Clear the 57 findings the canonical linter config reports on `pkg/dcf`. Two were real: `findDCFMountPoints` checked a never-assigned `erro` instead of the error from `findAllMountPoints`, discarding it, and `privatePath` was computed twice so the first computation was dead. Mountpoint selection otherwise behaves exactly as before; the defects that survive are filed as issue #6, not fixed here. Rename `DCFStore` to `Store` and `DCFObject` to `Object` (with its `DCFStoreRoot` field to `StoreRoot`), which revive's stutter rule requires and which the `fs.FS` rework in issue #4 will build on. Replace the placeholder test with tests over the exported surface. The filesystem walk stays uncovered: it is reachable only through `GetDCFStores`, which needs real mounted media. README keeps its content, reorganised into the required sections and gaining Entrypoints. (closes #1)
This commit is contained in:
248
pkg/dcf/dcf.go
248
pkg/dcf/dcf.go
@@ -1,177 +1,259 @@
|
||||
// Package dcf reads DCF stores: the directory structures described by
|
||||
// the "Design rule for Camera File system" (JEITA CP-3461) that digital
|
||||
// cameras write to the root of removable media.
|
||||
package dcf
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"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 {
|
||||
// Store represents a DCF store, which is a directory structure that
|
||||
// contains images and videos in the root of a mounted filesystem.
|
||||
type Store 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
|
||||
// Object represents a file in a DCF store. It is embedded in the Image
|
||||
// and Video types.
|
||||
type Object struct {
|
||||
StoreRoot string
|
||||
Path string
|
||||
Size int64
|
||||
Extension string
|
||||
}
|
||||
|
||||
// Image represents an image file on a DCF store.
|
||||
type Image struct {
|
||||
DCFObject
|
||||
Object
|
||||
}
|
||||
|
||||
// Video represents a video file on a DCF store.
|
||||
type Video struct {
|
||||
DCFObject
|
||||
Object
|
||||
}
|
||||
|
||||
func (d *DCFObject) FullFilePath() string {
|
||||
return filepath.Join(d.DCFStoreRoot, d.Path)
|
||||
// FullFilePath returns the path of the object on the local filesystem,
|
||||
// which is its store-relative path resolved against the store root.
|
||||
func (d *Object) FullFilePath() string {
|
||||
return filepath.Join(d.StoreRoot, d.Path)
|
||||
}
|
||||
|
||||
// VideosCount returns the number of videos in the DCF store.
|
||||
func (d *DCFStore) VideosCount() int {
|
||||
func (d *Store) VideosCount() int {
|
||||
return len(d.Videos)
|
||||
}
|
||||
|
||||
// ImagesCount returns the number of images in the DCF store.
|
||||
func (d *DCFStore) ImagesCount() int {
|
||||
func (d *Store) ImagesCount() int {
|
||||
return len(d.Images)
|
||||
}
|
||||
|
||||
func (d *DCFStore) TotalImageSize() int64 {
|
||||
// TotalImageSize returns the summed size in bytes of the store's images.
|
||||
func (d *Store) TotalImageSize() int64 {
|
||||
totalSize := int64(0)
|
||||
for _, image := range d.Images {
|
||||
totalSize += image.Size
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
func (d *DCFStore) TotalVideoSize() int64 {
|
||||
// TotalVideoSize returns the summed size in bytes of the store's videos.
|
||||
func (d *Store) TotalVideoSize() int64 {
|
||||
totalSize := int64(0)
|
||||
for _, video := range d.Videos {
|
||||
totalSize += video.Size
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
func (d *DCFStore) TotalSize() int64 {
|
||||
// TotalSize returns the summed size in bytes of every object in the
|
||||
// store.
|
||||
func (d *Store) TotalSize() int64 {
|
||||
return d.TotalImageSize() + d.TotalVideoSize()
|
||||
}
|
||||
|
||||
func (d *DCFStore) String() string {
|
||||
return fmt.Sprintf("DCFStore{RootDirectory: %s, Images: %d, Videos: %d, TotalSize: %s}",
|
||||
// String renders the store as a single human-readable summary line.
|
||||
func (d *Store) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Store{RootDirectory: %s, Images: %d, Videos: %d, TotalSize: %s}",
|
||||
d.RootDirectory,
|
||||
d.ImagesCount(),
|
||||
d.VideosCount(),
|
||||
humanize.Bytes(uint64(d.TotalSize())),
|
||||
humanize.Bytes(d.totalSizeBytes()),
|
||||
)
|
||||
}
|
||||
|
||||
// totalSizeBytes returns TotalSize as an unsigned byte count for
|
||||
// formatting. Sizes come from the filesystem and are never negative,
|
||||
// but the conversion is guarded rather than assumed.
|
||||
func (d *Store) totalSizeBytes() uint64 {
|
||||
total := d.TotalSize()
|
||||
if total < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint64(total)
|
||||
}
|
||||
|
||||
// String renders the video as a single human-readable line.
|
||||
func (v *Video) String() string {
|
||||
return fmt.Sprintf("Video{Path: %s, Size: %d, Extension: %s}", v.Path, v.Size, v.Extension)
|
||||
return fmt.Sprintf(
|
||||
"Video{Path: %s, Size: %d, Extension: %s}",
|
||||
v.Path, v.Size, v.Extension,
|
||||
)
|
||||
}
|
||||
|
||||
// String renders the image as a single human-readable line.
|
||||
func (i *Image) String() string {
|
||||
return fmt.Sprintf("Image{Path: %s, Size: %d, Extension: %s}", i.Path, i.Size, i.Extension)
|
||||
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) {
|
||||
// Hash returns the SHA-256 digest of the object's file as a hex string.
|
||||
func (d *Object) Hash() (string, error) {
|
||||
return pathToSHA256(d.FullFilePath())
|
||||
}
|
||||
|
||||
// pathToSHA256 returns the SHA-256 digest of the file at path as a hex
|
||||
// string.
|
||||
func pathToSHA256(path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
// The path is supplied by the caller by design: this library exists
|
||||
// to hash files the caller pointed it at.
|
||||
file, err := os.Open(filepath.Clean(path))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
|
||||
_, err = io.Copy(hash, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
|
||||
return hex.EncodeToString(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)
|
||||
// GetDCFStores returns the DCF stores found in the root directories of
|
||||
// the system's mounted filesystems. requestedCount bounds how many
|
||||
// stores are returned; 0 returns all of them.
|
||||
//
|
||||
// It neither mounts filesystems nor searches outside filesystem root
|
||||
// directories. It does walk each store it finds, populating the
|
||||
// returned stores with the images and videos they contain.
|
||||
func GetDCFStores(requestedCount int) (*[]Store, error) {
|
||||
storePaths, 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
|
||||
})
|
||||
stores := []Store{}
|
||||
|
||||
for _, storePath := range storePaths {
|
||||
store, err := scanStore(storePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dcfStores = append(dcfStores, dcfStore)
|
||||
|
||||
stores = append(stores, *store)
|
||||
}
|
||||
return &dcfStores, nil
|
||||
|
||||
return &stores, nil
|
||||
}
|
||||
|
||||
// scanStore walks root and returns a Store holding every image and
|
||||
// video beneath it.
|
||||
func scanStore(root string) (*Store, error) {
|
||||
store := Store{
|
||||
RootDirectory: root,
|
||||
Images: make([]*Image, 0),
|
||||
Videos: make([]*Video, 0),
|
||||
}
|
||||
|
||||
walk := func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return store.addFile(root, path, entry)
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(root, walk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &store, nil
|
||||
}
|
||||
|
||||
// addFile classifies path by extension and appends it to the store's
|
||||
// images or videos. A file that is neither is ignored.
|
||||
func (d *Store) addFile(root, path string, entry fs.DirEntry) error {
|
||||
isImage := isImageFile(path)
|
||||
if !isImage && !isVideoFile(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
object := Object{
|
||||
StoreRoot: root,
|
||||
Path: strings.TrimPrefix(path, root+string(filepath.Separator)),
|
||||
Size: info.Size(),
|
||||
Extension: filepath.Ext(path),
|
||||
}
|
||||
|
||||
if isImage {
|
||||
d.Images = append(d.Images, &Image{Object: object})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Videos = append(d.Videos, &Video{Object: object})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isImageFile reports whether path names a file this package treats as
|
||||
// an image.
|
||||
func isImageFile(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
for _, imageExt := range imageExtensions {
|
||||
if ext == imageExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return hasExtension(path,
|
||||
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".raw", ".arw")
|
||||
}
|
||||
|
||||
// isVideoFile reports whether path names a file this package treats as
|
||||
// a video.
|
||||
func isVideoFile(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
for _, videoExt := range videoExtensions {
|
||||
if ext == videoExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return hasExtension(path, ".mp4", ".mov", ".avi", ".mkv", ".wmv")
|
||||
}
|
||||
|
||||
// hasExtension reports whether path's extension, lowercased, is one of
|
||||
// the given extensions. Extensions are given with their leading dot.
|
||||
func hasExtension(path string, extensions ...string) bool {
|
||||
return slices.Contains(extensions, strings.ToLower(filepath.Ext(path)))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,177 @@
|
||||
package dcf
|
||||
package dcf_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/dcf/pkg/dcf"
|
||||
)
|
||||
|
||||
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.
|
||||
// newStore builds a store with two images and one video of known sizes,
|
||||
// so the count and size accessors have something deterministic to
|
||||
// report.
|
||||
func newStore(root string) *dcf.Store {
|
||||
return &dcf.Store{
|
||||
RootDirectory: root,
|
||||
Images: []*dcf.Image{
|
||||
{Object: dcf.Object{
|
||||
StoreRoot: root,
|
||||
Path: "DCIM/100MSDCF/DSC00001.ARW",
|
||||
Size: 2000,
|
||||
Extension: ".ARW",
|
||||
}},
|
||||
{Object: dcf.Object{
|
||||
StoreRoot: root,
|
||||
Path: "DCIM/100MSDCF/DSC00002.JPG",
|
||||
Size: 500,
|
||||
Extension: ".JPG",
|
||||
}},
|
||||
},
|
||||
Videos: []*dcf.Video{
|
||||
{Object: dcf.Object{
|
||||
StoreRoot: root,
|
||||
Path: "PRIVATE/M4ROOT/CLIP/C0001.MP4",
|
||||
Size: 9000,
|
||||
Extension: ".MP4",
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectFullFilePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
object := dcf.Object{
|
||||
StoreRoot: filepath.Join("/mnt", "card"),
|
||||
Path: filepath.Join("DCIM", "100MSDCF", "DSC00001.ARW"),
|
||||
}
|
||||
|
||||
want := filepath.Join("/mnt", "card", "DCIM", "100MSDCF", "DSC00001.ARW")
|
||||
if got := object.FullFilePath(); got != want {
|
||||
t.Fatalf("FullFilePath() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store := newStore("/mnt/card")
|
||||
|
||||
if got := store.ImagesCount(); got != 2 {
|
||||
t.Errorf("ImagesCount() = %d, want 2", got)
|
||||
}
|
||||
|
||||
if got := store.VideosCount(); got != 1 {
|
||||
t.Errorf("VideosCount() = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreSizes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store := newStore("/mnt/card")
|
||||
|
||||
if got := store.TotalImageSize(); got != 2500 {
|
||||
t.Errorf("TotalImageSize() = %d, want 2500", got)
|
||||
}
|
||||
|
||||
if got := store.TotalVideoSize(); got != 9000 {
|
||||
t.Errorf("TotalVideoSize() = %d, want 9000", got)
|
||||
}
|
||||
|
||||
if got := store.TotalSize(); got != 11500 {
|
||||
t.Errorf("TotalSize() = %d, want 11500", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyStoreSizesAreZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store := dcf.Store{RootDirectory: "/mnt/empty"}
|
||||
if got := store.TotalSize(); got != 0 {
|
||||
t.Fatalf("TotalSize() = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreStringReportsHumanizedTotal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := newStore("/mnt/card").String()
|
||||
for _, want := range []string{"/mnt/card", "Images: 2", "Videos: 1", "12 kB"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("String() = %q, want it to contain %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageAndVideoString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
image := dcf.Image{Object: dcf.Object{
|
||||
Path: "DCIM/100MSDCF/DSC00001.ARW",
|
||||
Size: 2000,
|
||||
Extension: ".ARW",
|
||||
}}
|
||||
|
||||
want := "Image{Path: DCIM/100MSDCF/DSC00001.ARW, Size: 2000, " +
|
||||
"Extension: .ARW}"
|
||||
if got := image.String(); got != want {
|
||||
t.Errorf("Image.String() = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
video := dcf.Video{Object: dcf.Object{
|
||||
Path: "PRIVATE/M4ROOT/CLIP/C0001.MP4",
|
||||
Size: 9000,
|
||||
Extension: ".MP4",
|
||||
}}
|
||||
|
||||
want = "Video{Path: PRIVATE/M4ROOT/CLIP/C0001.MP4, Size: 9000, " +
|
||||
"Extension: .MP4}"
|
||||
if got := video.String(); got != want {
|
||||
t.Errorf("Video.String() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectHashMatchesSHA256OfFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
root := t.TempDir()
|
||||
contents := []byte("DSC00001.ARW contents")
|
||||
|
||||
err := os.MkdirAll(filepath.Join(root, "DCIM"), 0o750)
|
||||
if err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(root, "DCIM", "a.arw"), contents, 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
object := dcf.Object{StoreRoot: root, Path: filepath.Join("DCIM", "a.arw")}
|
||||
|
||||
got, err := object.Hash()
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() returned error: %v", err)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(contents)
|
||||
if want := hex.EncodeToString(sum[:]); got != want {
|
||||
t.Fatalf("Hash() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectHashOnMissingFileReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
object := dcf.Object{StoreRoot: t.TempDir(), Path: "absent.jpg"}
|
||||
|
||||
_, err := object.Hash()
|
||||
if err == nil {
|
||||
t.Fatal("Hash() on a missing file returned no error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,84 @@
|
||||
package dcf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
)
|
||||
|
||||
// dcfMarkerDirectories are the directory names whose presence in a
|
||||
// filesystem root identifies that filesystem as a DCF store.
|
||||
func dcfMarkerDirectories() []string {
|
||||
return []string{"DCIM", "M4ROOT"}
|
||||
}
|
||||
|
||||
// findAllMountPoints returns the distinct mountpoints of the system's
|
||||
// physical filesystems.
|
||||
func findAllMountPoints() ([]string, error) {
|
||||
mountpoints := []string{}
|
||||
partitions, err := disk.Partitions(false) // physical devices only, so false
|
||||
partitions, err := disk.Partitions(false) // physical devices only
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountpoints := []string{}
|
||||
|
||||
for _, partition := range partitions {
|
||||
if !contains(mountpoints, partition.Mountpoint) {
|
||||
if !slices.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.
|
||||
// findDCFMountPoints returns the paths of the mountpoints that contain
|
||||
// one of the marker directories a DCF store is identified by. Its only
|
||||
// argument is how many mountpoints to return; 0 returns all of them.
|
||||
func findDCFMountPoints(requestedCount int) ([]string, error) {
|
||||
filteredMountpoints := []string{}
|
||||
var erro error
|
||||
mountpoints, err := findAllMountPoints()
|
||||
|
||||
if erro != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredMountpoints := []string{}
|
||||
|
||||
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 !isDCFMountPoint(mountpoint) {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
slog.Debug("keeping mountpoint", "mountpoint", 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
|
||||
// isDCFMountPoint reports whether mountpoint holds any of the marker
|
||||
// directories a DCF store is identified by.
|
||||
func isDCFMountPoint(mountpoint string) bool {
|
||||
found := false
|
||||
|
||||
for _, marker := range dcfMarkerDirectories() {
|
||||
markerPath := filepath.Join(mountpoint, marker)
|
||||
|
||||
_, err := os.Stat(markerPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Debug("found marker directory", "path", markerPath)
|
||||
|
||||
found = true
|
||||
}
|
||||
return false
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user