#!/usr/bin/env bash
set -Eeu -o pipefail

# Look through the local filesystem and exclude development dependencies
# from Apple Time Machine backups.
#
# Since these files can be restored easily via their respective installation
# tools, there's no reason to waste time/bandwidth on backing them up.
#
# To retrieve a full list of excluded files, you may run:
#
#   sudo mdfind "com_apple_backup_excludeItem = 'com.apple.backupd'"
#
# For a full explanation, please see https://apple.stackexchange.com/a/25833/206772
#
# @version 0.3.0
# @author  Steve Grunwell
# @license MIT

readonly ASIMOV_ROOT=~

# Paths to unconditionally skip over. This prevents Asimov from modifying the
# Time Machine exclusions for these paths (and decendents). It has an important
# side-effect of speeding up the search.
readonly ASIMOV_SKIP_PATHS=(
    ~/.Trash
    ~/Library
)

# A list of "directory"/"sentinel" pairs.
#
# Directories will only be excluded if the dependency ("sentinel") file exists.
#
# For example, 'node_modules package.json' means "exclude node_modules/ from the
# Time Machine backups if there is a package.json file next to it."
readonly ASIMOV_VENDOR_DIR_SENTINELS=(
    '.build Package.swift'             # Swift
    '.gradle build.gradle'             # Gradle
    '.gradle build.gradle.kts'         # Gradle Kotlin Script
    'build build.gradle'               # Gradle build files
    'build build.gradle.kts'           # Gradle Kotlin Script build files
    '.dart_tool pubspec.yaml'          # Flutter (Dart)
    '.packages pubspec.yaml'           # Pub (Dart)
    '.stack-work stack.yaml'           # Stack (Haskell)
    '.tox tox.ini'                     # Tox (Python)
    '.nox noxfile.py'                  # Nox (Python)
    '.vagrant Vagrantfile'             # Vagrant
    '.venv requirements.txt'           # virtualenv (Python)
    '.venv pyproject.toml'             # virtualenv (Python)
    'Carthage Cartfile'                # Carthage
    'Pods Podfile'                     # CocoaPods
    'bower_components bower.json'      # Bower (JavaScript)
    'build build.gradle'               # Gradle
    'build build.gradle.kts'           # Gradle Kotlin Script
    'build pubspec.yaml'               # Flutter (Dart)
    'build setup.py'                   # Python
    'dist setup.py'                    # PyPI Publishing (Python)
    'node_modules package.json'        # npm, Yarn (NodeJS)
    '.parcel-cache package.json'       # Parcel v2 cache (JavaScript)
    'target Cargo.toml'                # Cargo (Rust)
    'target pom.xml'                   # Maven
    'target build.sbt'                 # Sbt (Scala)
    'target plugins.sbt'               # Sbt plugins (Scala)
    'vendor composer.json'             # Composer (PHP)
    'vendor Gemfile'                   # Bundler (Ruby)
    'vendor go.mod'                    # Go Modules (Golang)
    'venv requirements.txt'            # virtualenv (Python)
    'deps mix.exs'                     # Mix dependencies (Elixir)
    '.build mix.exs'                   # Mix build files (Elixir)
    '.terraform.d .terraformrc'        # Terraform plugin cache
    '.terragrunt-cache terragrunt.hcl' # Terragrunt
    'cdk.out cdk.json'                 # AWS CDK
)

# Exclude the given paths from Time Machine backups.
# Reads the newline-separated list of paths from stdin.
exclude_file() {
    local path
    while IFS=$'\n' read -r path; do
        if tmutil isexcluded "${path}" | grep -Fq '[Excluded]'; then
            echo "- ${path} is already excluded, skipping."
            continue
        fi

        tmutil addexclusion "${path}"

        sizeondisk=$(du -hs "${path}" | cut -f1)
        echo "- ${path} has been excluded from Time Machine backups (${sizeondisk})."
    done
}

# Iterate over the skip directories to construct the `find` expression.
declare -a find_parameters_skip=()
for d in "${ASIMOV_SKIP_PATHS[@]}"; do
    find_parameters_skip+=( -not \( -path "${d}" -prune \) )
done

# Iterate over the directory/sentinel pairs to construct the `find` expression.
declare -a find_parameters_vendor=()
for i in "${ASIMOV_VENDOR_DIR_SENTINELS[@]}"; do
    read -ra parts <<< "${i}"

    # Add this folder to the `find` list, allowing a single `find` command to find all
    _exclude_name="${parts[0]}"
    _sibling_sentinel_name="${parts[1]}"

    # Given a directory path, determine if the corresponding file (relative
    # to that directory) is available.
    #
    # For example, when looking at a /vendor directory, we may choose to
    # ensure a composer.json file is available.
    find_parameters_vendor+=( -or \( \
        -type d \
        -name "${_exclude_name}" \
        -execdir test -e "${_sibling_sentinel_name}" \; \
        -prune \
        -print \
    \) )
done

printf '\n\033[0;36mFinding dependency directories with corresponding definition files…\033[0m\n'

find "${ASIMOV_ROOT}" \( "${find_parameters_skip[@]}" \) \( -false "${find_parameters_vendor[@]}" \) \
    | exclude_file \
    ;
