diff --git a/source/app.d b/source/app.d index 3f63344..f9d0c15 100644 --- a/source/app.d +++ b/source/app.d @@ -8,9 +8,11 @@ import std.getopt; import filesizes; import progress; +import utils; +import ingest; + const DEFAULT_OUTPUT_DIR = "raw"; const DEFAULT_MEDIA_DIR = "/media"; -const GOPRO_CONTENT_DIR = "DCIM/100GOPRO"; const DEFAULT_BUFFER_SIZE = 1024 * 1024; int main(string[] args) { @@ -63,99 +65,3 @@ int main(string[] args) { writefln!"Found GoPro media at %s."(goProDir); return copyFiles(goProDir, outputDir, bufferSize, force, dryRun); } - -/** - * Tries to find a GoPro's media directory. - * Params: - * baseDir = The base directory to start the search from. - * Returns: A nullable string that, if present, refers to the GoPro's media - * directory. - */ -Nullable!string getGoProDir(string baseDir) { - if (!exists(baseDir) || !isDir(baseDir)) return Nullable!string.init; - foreach (dir; std.file.dirEntries(baseDir, SpanMode.breadth)) { - string mediaPath = buildPath(dir.name, GOPRO_CONTENT_DIR); - if (exists(mediaPath) && isDir(mediaPath)) { - return nullable(mediaPath); - } - } - return Nullable!string.init; -} - -/** - * Determines if we should copy a given file from the media card to the local - * directory. - * Params: - * entry = The entry for the file at its source. - * targetFile = The place to copy the file to. - * force = Whether the force flag has been set. - * Returns: True if we should copy the file, or false otherwise. - */ -bool shouldCopyFile(DirEntry entry, string targetFile, bool force) { - return entry.isFile() && - (entry.name.endsWith(".MP4") || entry.name.endsWith(".WAV")) && - (!exists(targetFile) || getSize(targetFile) != entry.size || force); -} - -/** - * Copies files from a source to a target directory. - * Params: - * sourceDir = The source directory. - * targetDir = The target directory. - * bufferSize = The buffer size to use when copying. - * force = Whether to overwrite existing files. - * dryRun = Whether to perform a dry-run. - * Returns: An exit code. - */ -int copyFiles(string sourceDir, string targetDir, size_t bufferSize, bool force, bool dryRun) { - if (!exists(targetDir) && !dryRun) mkdirRecurse(targetDir); - DirEntry[] filesToCopy; - ulong totalFileSize = 0; - foreach (DirEntry entry; dirEntries(sourceDir, SpanMode.shallow)) { - string targetFile = buildPath(targetDir, baseName(entry.name)); - if (shouldCopyFile(entry, targetFile, force)) { - filesToCopy ~= entry; - totalFileSize += entry.size; - } - } - - if (filesToCopy.length == 0) { - writeln("No new files to copy."); - return 0; - } - - if (getAvailableDiskSpace(targetDir) < totalFileSize) { - writefln!"Not enough disk space to copy all files: %s available, %s needed."( - formatFilesize(getAvailableDiskSpace(targetDir)), - formatFilesize(totalFileSize) - ); - return 1; - } - - writefln!"Copying %d files (%s) to %s."(filesToCopy.length, formatFilesize(totalFileSize), targetDir); - if (dryRun) writeln("(Dry Run)"); - Bar progressBar = new FillingSquaresBar(); - progressBar.width = 80; - progressBar.max = totalFileSize; - progressBar.start(); - ubyte[] buffer = new ubyte[bufferSize]; - foreach (DirEntry entry; filesToCopy) { - string filename = baseName(entry.name); - string targetFile = buildPath(targetDir, filename); - string verb = exists(targetFile) ? "Overwriting" : "Copying"; - progressBar.message = { return std.string.format!"%s %s (%s)"(verb, filename, formatFilesize(entry.size)); }; - if (!dryRun) { - File inputFile = File(entry.name, "rb"); - File outputFile = File(targetFile, "wb"); - foreach (ubyte[] localBuffer; inputFile.byChunk(buffer)) { - outputFile.rawWrite(localBuffer); - progressBar.next(localBuffer.length); - } - } else { - progressBar.next(getSize(entry.name)); - } - } - progressBar.finish(); - - return 0; -} diff --git a/source/ingest.d b/source/ingest.d new file mode 100644 index 0000000..65189c8 --- /dev/null +++ b/source/ingest.d @@ -0,0 +1,102 @@ +module ingest; + +import std.file; +import std.stdio; +import std.path; +import std.algorithm; +import std.string; +import std.typecons; +import filesizes; +import progress; + +private struct IngestData { + DirEntry[] filesToCopy; + + ulong totalFileSize() { + return filesToCopy.map!(f => f.size).sum; + } + + size_t fileCount() { + return filesToCopy.length; + } +} + +/** + * Determines if we should copy a given file from the media card to the local + * directory. + * Params: + * entry = The entry for the file at its source. + * targetFile = The place to copy the file to. + * force = Whether the force flag has been set. + * Returns: True if we should copy the file, or false otherwise. + */ +private bool shouldCopyFile(DirEntry entry, string targetFile, bool force) { + return entry.isFile() && + (entry.name.endsWith(".MP4") || entry.name.endsWith(".WAV")) && + (!exists(targetFile) || getSize(targetFile) != entry.size || force); +} + +private IngestData discoverIngestData(string sourceDir, string targetDir, bool force) { + IngestData data; + foreach (DirEntry entry; dirEntries(sourceDir, SpanMode.shallow)) { + string targetFile = buildPath(targetDir, baseName(entry.name)); + if (shouldCopyFile(entry, targetFile, force)) { + data.filesToCopy ~= entry; + } + } + return data; +} + +/** + * Copies files from a source to a target directory. + * Params: + * sourceDir = The source directory. + * targetDir = The target directory. + * bufferSize = The buffer size to use when copying. + * force = Whether to overwrite existing files. + * dryRun = Whether to perform a dry-run. + * Returns: An exit code. + */ +public int copyFiles(string sourceDir, string targetDir, size_t bufferSize, bool force, bool dryRun) { + IngestData ingestData = discoverIngestData(sourceDir, targetDir, force); + if (ingestData.fileCount == 0) { + writeln("No new files to copy."); + return 0; + } + if (getAvailableDiskSpace(targetDir) < ingestData.totalFileSize) { + writefln!"Not enough disk space to copy all files: %s available, %s needed."( + formatFilesize(getAvailableDiskSpace(targetDir)), + formatFilesize(ingestData.totalFileSize) + ); + return 1; + } + writefln!"Copying %d files (%s) to %s."(ingestData.fileCount, formatFilesize(ingestData.totalFileSize), targetDir); + if (dryRun) writeln("(Dry Run)"); + + if (!exists(targetDir) && !dryRun) mkdirRecurse(targetDir); + + Bar progressBar = new FillingSquaresBar(); + progressBar.width = 80; + progressBar.max = ingestData.totalFileSize; + progressBar.start(); + ubyte[] buffer = new ubyte[bufferSize]; + foreach (DirEntry entry; ingestData.filesToCopy) { + string filename = baseName(entry.name); + string targetFile = buildPath(targetDir, filename); + string verb = exists(targetFile) ? "Overwriting" : "Copying"; + progressBar.message = { return std.string.format!"%s %s (%s)"(verb, filename, formatFilesize(entry.size)); }; + if (!dryRun) { + File inputFile = File(entry.name, "rb"); + File outputFile = File(targetFile, "wb"); + foreach (ubyte[] localBuffer; inputFile.byChunk(buffer)) { + outputFile.rawWrite(localBuffer); + progressBar.next(localBuffer.length); + } + } else { + progressBar.next(getSize(entry.name)); + } + } + progressBar.finish(); + + return 0; +} \ No newline at end of file diff --git a/source/utils.d b/source/utils.d new file mode 100644 index 0000000..97c5372 --- /dev/null +++ b/source/utils.d @@ -0,0 +1,24 @@ +module utils; + +import std.typecons; +import std.file; +import std.path; + +/** + * Tries to find a GoPro's media directory. + * Params: + * baseDir = The base directory to start the search from. + * Returns: A nullable string that, if present, refers to the GoPro's media + * directory. + */ +public Nullable!string getGoProDir(string baseDir) { + if (!exists(baseDir) || !isDir(baseDir)) return Nullable!string.init; + foreach (dir; std.file.dirEntries(baseDir, SpanMode.breadth)) { + // We know that a GoPro contains DCIM/100GOPRO in it. + string mediaPath = buildPath(dir.name, "DCIM", "100GOPRO"); + if (exists(mediaPath) && isDir(mediaPath)) { + return nullable(mediaPath); + } + } + return Nullable!string.init; +} \ No newline at end of file