Skip to content

Instantly share code, notes, and snippets.

@danielrbradley
Created August 3, 2018 22:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielrbradley/dba4de51b9a17abc6bbdc6e08d9f80e0 to your computer and use it in GitHub Desktop.
Save danielrbradley/dba4de51b9a17abc6bbdc6e08d9f80e0 to your computer and use it in GitHub Desktop.
Script for backing up processed photos

My method for storing and backing up photos is as follows:

  1. All photos get downloaded into the Archive folder, in a folder for the current year, with the batch folder name starting with the date.
  2. The photos get reviewed, the ones which get picked are edited, exported as JPEGs, and the edit metadata saved alongside.
  3. Once editing is complete, the processed JPEGs are uploaded to wherever they're being shared (e.g. Google Photos).
  4. Run the script below which copies only files which have been picked and edited into the Best folder using the same folder names, but not grouped into years.
  5. The Best folder is backed up to a second local disk, and an offsite location (AWS Glacier).
  6. The Archive folder is only backed up onto a second local replicated disk.
open System
open System.IO
let cd = __SOURCE_DIRECTORY__
let path parts = Path.Combine(parts)
let archive = path [|cd; "Archive"|]
let best = path [|cd; "Best"|]
let isJpg filename =
let ext = Path.GetExtension(filename)
match ext.ToLowerInvariant() with
| ".jpg" | ".jpeg" -> true
| _ -> false
let isMeta filename =
let ext = Path.GetExtension(filename)
match ext.ToLowerInvariant() with
| ".xmp" | ".bib" -> true
| _ -> false
let isPhotoRelated filename =
let ext = Path.GetExtension(filename)
match ext.ToLowerInvariant() with
| ".jpg" | ".jpeg"
| ".nef" | ".xmp" | ".bib" -> true
| _ -> false
let getPhotosToMove folder =
Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories)
|> Seq.groupBy(Path.GetFileNameWithoutExtension)
|> Seq.map(fun (key, files) ->
key, (files |> Seq.filter isPhotoRelated |> Seq.toList)
)
|> Seq.filter(fun (withoutExt, files) ->
(not (files |> List.forall isJpg)) &&
(files |> List.exists isJpg)
)
|> Seq.collect snd
|> Seq.toArray
type PhotoFolder =
{ FolderName: string
Files: string[] }
let describePhotoFolder path =
{ FolderName = Path.GetFileName(path)
Files = getPhotosToMove path }
let describeArchive () =
Directory.GetDirectories(archive)
|> Array.collect (
Directory.GetDirectories
>> Array.map describePhotoFolder
)
type FileCopy =
{ Source: string
Destination: string }
type Duplicate =
{ Filename: string
Paths: string[] }
type Operation =
| NewDirectory of folderName:string * path:string * files:FileCopy[]
| NewFilesInDirectory of folderName:string * newFiles:FileCopy[] * skippedFiles:FileCopy[]
| NoChangeDirectory of folderName:string
| UnprocessedDirectory of folderName:string
| DuplicateFiles of folderName:string * duplicates:Duplicate[]
let planFolderOperation destination folder =
let fileCount = folder.Files.Length
let duplicates =
folder.Files
|> Seq.groupBy Path.GetFileName
|> Seq.choose (fun (key, files) ->
let filesArray = Seq.toArray files
if filesArray.Length > 1 then
Some { Filename = key; Paths = filesArray }
else None
)
|> Seq.toArray
if fileCount = 0 then
UnprocessedDirectory folder.FolderName
elif duplicates.Length > 0 then
DuplicateFiles(folder.FolderName, duplicates)
else
let destFolder = path [| destination; folder.FolderName |]
let potentialFileCopies =
folder.Files
|> Array.map (fun file ->
{ Source = file
Destination = path [| destFolder; Path.GetFileName file |] }
)
if not <| Directory.Exists destFolder then
NewDirectory(folder.FolderName, destFolder, potentialFileCopies)
else
let existing, newFileCopies =
potentialFileCopies
|> Array.partition (fun fileCopy -> File.Exists(fileCopy.Destination))
if newFileCopies.Length = 0 then
NoChangeDirectory folder.FolderName
else
NewFilesInDirectory(folder.FolderName, newFileCopies, existing)
let planOperations destination folders =
folders |> Array.map (planFolderOperation destination)
let copyFiles files =
for file in files do
printfn "%s\t-> %s" file.Source file.Destination
File.Copy (file.Source, file.Destination)
let run () =
let archive = describeArchive ()
let operations = planOperations best archive
for operation in operations do
match operation with
| NewDirectory(folderName, path,fileCopies) ->
printfn "+ %s" folderName
Directory.CreateDirectory path |> ignore
copyFiles fileCopies
| NewFilesInDirectory(folderName, newFiles, existing) ->
printfn "~ %s (%i existing files)" folderName existing.Length
copyFiles newFiles
| NoChangeDirectory(_) -> ()
| UnprocessedDirectory(folderName) ->
printfn "TODO: %s" folderName
| DuplicateFiles(folder, files) ->
printfn "Duplicates in %s" folder
// Not covered:
// Folders with only .NEF files
// Folders with renamed .jpg files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment