detect PRIVATE/M4ROOT and return the requested store count (closes #6)

A card was detected only with M4ROOT at its root, and asking for two or
more stores returned one too few. A mount point now counts when its root
holds a DCIM or PRIVATE/M4ROOT directory, and the count is exact.

The check and the counting are split from the partition listing so tests
can hand them temporary directories.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #10.
This commit is contained in:
clawbot authored and clawbot committed 2026-11-02 21:01:29 +01:00
1 parent 623bfe9a43
commit 445424ba3e
3 files changed
+122 -35

No files matched your search

+34 -28
View File
@@ -28,45 +28,51 @@ func findAllMountPoints() ([]string, error) {
}
// findDCFMountPoints returns a list of strings of the paths of mountpoints
// that contain a DCIM or M4ROOT 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.
// that hold a DCF store. 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{}
mountpoints, err := findAllMountPoints()
if err != nil {
return nil, err
}
return filterDCFMountPoints(mountpoints, requestedCount), nil
}
// filterDCFMountPoints returns the first requestedCount of the mountpoints that
// hold a DCF store. If requestedCount is 0, it returns all of them.
func filterDCFMountPoints(mountpoints []string, requestedCount int) []string {
filteredMountpoints := []string{}
for _, mountpoint := range mountpoints {
dcimPath := filepath.Join(mountpoint, "DCIM")
m4rootPath := filepath.Join(mountpoint, "M4ROOT")
shouldKeep := false
_, err = os.Stat(dcimPath)
if err == nil {
shouldKeep = true
slog.Debug(fmt.Sprintf("Found DCIM directory at %s\n", dcimPath))
if !isDCFMountPoint(mountpoint) {
continue
}
_, err = os.Stat(m4rootPath)
if err == nil {
shouldKeep = true
slog.Debug(fmt.Sprintf("Keeping mountpoint %s\n", mountpoint))
slog.Debug(fmt.Sprintf("Found M4ROOT directory at %s\n", m4rootPath))
}
if shouldKeep {
slog.Debug(fmt.Sprintf("Keeping mountpoint %s\n", mountpoint))
filteredMountpoints = append(filteredMountpoints, mountpoint)
if (requestedCount > 0) && (len(filteredMountpoints)+1 >= requestedCount) {
break
}
filteredMountpoints = append(filteredMountpoints, mountpoint)
if (requestedCount > 0) && (len(filteredMountpoints) >= requestedCount) {
break
}
}
return filteredMountpoints, nil
return filteredMountpoints
}
// isDCFMountPoint reports whether the root of mountpoint holds a DCIM or
// PRIVATE/M4ROOT directory, which is how we detect DCF stores.
func isDCFMountPoint(mountpoint string) bool {
for _, directory := range []string{"DCIM", "PRIVATE/M4ROOT"} {
path := filepath.Join(mountpoint, directory)
info, err := os.Stat(path)
if err == nil && info.IsDir() {
slog.Debug(fmt.Sprintf("Found %s directory at %s\n", directory, path))
return true
}
}
return false
}
+85
View File
@@ -0,0 +1,85 @@
package dcf
import (
"os"
"path/filepath"
"slices"
"testing"
)
// newMountPoint returns a temporary directory holding the given directories.
func newMountPoint(t *testing.T, directories ...string) string {
t.Helper()
root := t.TempDir()
for _, directory := range directories {
err := os.MkdirAll(filepath.Join(root, directory), 0o750)
if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
}
return root
}
func TestIsDCFMountPoint(t *testing.T) {
t.Parallel()
cases := map[string]bool{
"DCIM": true,
"PRIVATE/M4ROOT": true,
"M4ROOT": false,
"PRIVATE": false,
}
for directory, want := range cases {
got := isDCFMountPoint(newMountPoint(t, directory))
if got != want {
t.Errorf("isDCFMountPoint with only %q = %v, want %v",
directory, got, want)
}
}
}
func TestIsDCFMountPointIgnoresPlainFile(t *testing.T) {
t.Parallel()
root := t.TempDir()
err := os.WriteFile(filepath.Join(root, "DCIM"), nil, 0o600)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
if isDCFMountPoint(root) {
t.Fatal("isDCFMountPoint counted a plain file named DCIM as a directory")
}
}
func TestFilterDCFMountPointsReturnsRequestedCount(t *testing.T) {
t.Parallel()
first := newMountPoint(t, "DCIM")
second := newMountPoint(t, "PRIVATE/M4ROOT")
third := newMountPoint(t, "DCIM")
// This one holds no DCF store, so it must not use up the requested count.
mountpoints := []string{newMountPoint(t), first, second, third}
cases := map[int][]string{
0: {first, second, third},
1: {first},
2: {first, second},
3: {first, second, third},
4: {first, second, third},
}
for requestedCount, want := range cases {
got := filterDCFMountPoints(mountpoints, requestedCount)
if !slices.Equal(got, want) {
t.Errorf("filterDCFMountPoints(%d) = %v, want %v",
requestedCount, got, want)
}
}
}