Skip to content

Instantly share code, notes, and snippets.

@danielrbradley
Created August 3, 2018 22:43

Revisions

  1. danielrbradley created this gist Aug 3, 2018.
    8 changes: 8 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    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.
    135 changes: 135 additions & 0 deletions photos-backup.fsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,135 @@
    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