Store.Import copies every member of every file group and the images and videos under PRIVATE/M4ROOT to dest/YYYY-MM-DD/<file name>. The day comes from the new Object.ModTime. A file group stays in the folder of its primary member. Each copy is read back and compared by SHA256. A file with the same hash already there is skipped, and anything else there is a conflict and left alone. One failed file does not stop the others. A cancelled context stops at once and removes the partial copy. Object.Hash now shares its reading code with the import. Model: opus-5-5
9.6 KiB
dcf
Description
dcf is a WTFPL-licensed Go library by @sneak that
reads DCF memory cards from digital cameras and copies the files off them.
DCF is the "Design rule for Camera File system". It is also JEITA specification number CP-3461. JEITA is the Japan Electronics and Information Technology Industries Association. Wikipedia describes it at 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.
dcf is incomplete and under development. It does not work yet.
Getting Started
Add the library to your Go module:
go get sneak.berlin/go/dcf
Then list the DCF stores on the mounted filesystems:
package main
import (
"context"
"fmt"
"log"
"sneak.berlin/go/dcf/pkg/dcf"
)
func main() {
stores, err := dcf.GetStores(context.Background())
if err != nil {
log.Fatal(err)
}
for _, store := range stores {
fmt.Println(store.String())
}
}
Call dcf.GetStore instead when you expect exactly one DCF store. It returns
dcf.ErrNoStore when none is mounted, and dcf.ErrMultipleStores when more
than one is.
To copy the files off a card, call Import with a destination directory:
package main
import (
"context"
"fmt"
"log"
"sneak.berlin/go/dcf/pkg/dcf"
)
func main() {
ctx := context.Background()
store, err := dcf.GetStore(ctx)
if err != nil {
log.Fatal(err)
}
result, err := store.Import(ctx, "/path/to/photos")
if err != nil {
log.Fatal(err)
}
fmt.Println("copied:", result.Copied)
fmt.Println("skipped:", result.Skipped)
fmt.Println("conflicts:", result.Conflicts)
}
To work on the library itself, you need docker and git:
git clone git@git.eeqj.de:sneak/dcf.git
cd dcf
script/setup
make check
Entrypoints
This repo adheres to the
Scripts to Rule Them All
standard. Each entrypoint is a POSIX sh script in script/. Use these instead
of running the underlying tools directly.
script/bootstrap(make bootstrap) installs the tools the host needs. It does not install docker or the linter. It installs:makeand git- Go into
~/.local/go, at the version in theDockerfile, unless thegoon thePATHis already that version - the Go modules
- node and yarn
prettier
script/setup(make setup) runsscript/bootstrapand then installs the git pre-commit hook.script/test(make test) runs the tests by building theteststage of theDockerfile.script/lint(make lint) checks the Go code by building thelintstage of theDockerfile.script/fmt(make fmt) rewrites the Go code withgo fmtand the Markdown files withprettier.script/fmt-check(make fmt-check) checks the same formatting and changes nothing.script/check(make check) runs the tests, then the linter, then the formatting check.script/build(make build) compiles every package. It needs the Go thatscript/bootstrapinstalls.script/docker(make docker) builds the development image, taggeddcf.script/cibuildruns the whole CI build. The Gitea workflow runs it on every push.script/precommitis what the git pre-commit hook runs. It runsscript/check.script/install-precommit(make hooks) installs the git pre-commit hook.script/projectnameprints the project name for the other scripts.
The development image cannot be built unless the lint and test stages pass.
Run docker run --rm -it dcf for a shell inside it, with Go and make.
Rationale
I wanted to copy images off my memory cards for processing. I also like overengineering and reusable code.
Design
All the code is in the pkg/dcf package. The repo has no commands.
dcf.goholds the public API for reading a card.GetStoresfinds the DCF stores and reads each one.GetStorereturns the one DCF store when exactly one is mounted.helpers.gofinds the mount points that hold a DCF store.parse.goreads one DCF store through anfs.FSrooted at its mount point.filegroup.gosorts the files of one DCF directory into file groups.import.goholdsStore.Import, which copies files off a card.
The main types are:
Storeis one DCF store on one mounted filesystem.Directoryis one DCF directory. It has a directory number, a name, the objects inside it and their file groups.Objectis one file in a store. A file in a DCF directory also has a directory number and a file number.Object.ModTimeis its modification time.Object.Hashreads the file through thefs.FSof its store. AnObjectyou build yourself has none, so itsHashreturns an error.FileGroupis one file group in a DCF directory.ImageandVideoembedObject. The file extension decides which one a file becomes. A file with any other extension, such as aTHMthumbnail, is neither.ImportResultlists whatStore.Importdid with each file.
A mounted filesystem is a DCF store when its root holds a DCIM or
PRIVATE/M4ROOT directory. It needs only one of them. dcf never mounts a
filesystem. Media anywhere else on the filesystem is not part of the store.
dcf never follows a symlink.
GetStores and GetStore take a context.Context. When it is cancelled, the
read stops before the next directory. The returned error matches
context.Canceled.
dcf applies the naming rules of the DCF specification, CIPA DC-009-2010, under
DCIM:
- A DCF directory sits directly under
DCIM. Its name is a directory number from 100 to 999, then five characters, such as100MSDCF. - A DCF file sits directly in a DCF directory. Its name is four characters, then
a file number from 0001 to 9999, then a dot and a three-character extension,
such as
DSC00001.ARW. - The characters allowed are
AtoZ,0to9and_. Lower-case letters count as upper-case. - Directories that share a directory number are not DCF directories.
JPGfiles that share a file number in one directory are not DCF files.
Store.NonConforming lists the path of everything else under DCIM. It
includes everything inside a directory that breaks the rules. It also includes
every symlink, whatever its name. These entries have no DCF numbers, and they
are not counted as images or videos.
The naming rules do not apply under PRIVATE/M4ROOT. The images and videos
there are in Store.Images and Store.Videos, and their DCF numbers are 0.
dcf skips any symlink there.
A file group is made of the DCF files in one DCF directory that share a file
number. The specification calls it a DCF object. A RAW file and its JPEG form a
file group, and so do a video and its THM thumbnail.
- The first four characters and the extension can differ within a group.
- A DCF file that shares its number with no other file is a group of one.
- Files in different DCF directories are never in the same group.
- Only DCF files are grouped. Files in
Store.NonConformingand underPRIVATE/M4ROOTare in no group.
Directory.FileGroups lists the groups in file number order. Each FileGroup
has:
DirectoryNumberandFileNumberMembers, which is every file in the groupPrimary, which is the member that stands for the whole groupMembersCount, which returns the number of membersTotalSize, which returns the total size of the members in bytes
The primary member is the first of these that the group holds:
- a
JPGfile - any other file in
Store.Images - a file in
Store.Videos - any other file except a
THM - a
THM
Store.Images and Store.Videos list each file on its own, whatever its group.
Store.Import copies files off a card to a destination directory. It verifies
each copy against the SHA256 hash of its source. It only reads the card, so it
never deletes anything there.
It copies:
- every member of every file group
- the images and videos under
PRIVATE/M4ROOT
It does not copy the files in Store.NonConforming.
Each file goes to YYYY-MM-DD/<file name> under the destination directory. The
date is the day of the file's modification time, in the computer's time zone.
Every member of a file group goes to the day of its primary member, so a group
never splits across two folders. Each copy keeps the modification time of its
source. Only the user who ran the import can read the copies.
ImportResult lists each destination path in one of three lists:
Copiedholds the copies this import wrote and verified.Skippedholds the paths that already held a file with the same SHA256 hash.Conflictsholds the paths that already held anything else.dcfleaves that alone and does not copy the file.
A conflict is not an error, so a caller must check Conflicts. These are the
usual causes:
- Two different files have the same name and the same day. Two cards can reuse a file number, and so can two DCF directories on one card. The first file is copied, and the second is a conflict.
- A partial file remains because the process died during a copy. Delete it and import again.
A copy that fails or does not match the hash of its source is removed. One file that fails does not stop the others. The returned error joins the failures, each naming its file. The result still lists what was done.
When the context is cancelled, the import stops at once, even inside a file, and
removes the partial copy. The returned error matches context.Canceled.
TODO
No open items.
License
WTFPL. See LICENSE.