#!/bin/bash # ─── Config ─────────────────────────────────────────────────────────────────── CONFIG_DIR="$HOME/.config/backup_project" INSTALL_PATH="/usr/local/bin/backup_project" # ─── Help ───────────────────────────────────────────────────────────────────── if [[ "$1" == "--help" || "$1" == "-h" ]]; then cat <.cfg - To view all configs: ls ~/.config/backup_project/ - To uninstall: sudo rm /usr/local/bin/backup_project EOF exit 0 fi # ─── Self-install ───────────────────────────────────────────────────────────── if [[ "$1" == "--install" ]]; then echo "Installing backup_project to $INSTALL_PATH ..." sudo cp "$0" "$INSTALL_PATH" sudo chmod +x "$INSTALL_PATH" echo "Done! You can now run 'backup_project [folder]' from anywhere." exit 0 fi # ─── Set version override ───────────────────────────────────────────────────── # Usage: backup_project --set-version 2.0.15 [folder] if [[ "$1" == "--set-version" ]]; then NEW_VERSION="$2" SOURCE="$(realpath "${3:-$(pwd)}")" if ! echo "$NEW_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+ # If no argument given, use current directory if [ -z "$1" ]; then SOURCE="$(pwd)" else SOURCE="$1" fi # Strip trailing slash, resolve to absolute path SOURCE="$(realpath "${SOURCE%/}")" if [ ! -d "$SOURCE" ]; then echo "Error: '$SOURCE' is not a directory." exit 1 fi # ─── Load or create config for this source folder ──────────────────────────── mkdir -p "$CONFIG_DIR" CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "First time backing up '$(basename "$SOURCE")'." read -rp "Enter starting version (e.g. 2.0.12): " FULL_VERSION # Validate format: must be X.Y.Z (digits and dots, at least one dot, ends in digits) if ! echo "$FULL_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+$'; then echo "Error: version must be in format like 2.0.12 or 1.0.0" exit 1 fi # Split into prefix (everything before last dot) and starting minor (last number) BASE_VERSION="${FULL_VERSION%.*}" # e.g. 2.0 START_MINOR="${FULL_VERSION##*.}" # e.g. 12 echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "Saved: prefix='$BASE_VERSION', starting minor='$START_MINOR' for '$(basename "$SOURCE")'." else source "$FOLDER_CONFIG" fi # ─── Find next version number ───────────────────────────────────────────────── BASE_NAME=$(basename "$SOURCE") PARENT_DIR=$(dirname "$SOURCE") if [ -n "$FORCE_MINOR" ]; then # --force: use exactly this number, ignore existing folders, then clear the force NEXT="$FORCE_MINOR" echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$FORCE_MINOR" >> "$FOLDER_CONFIG" echo "FORCE_MINOR=" >> "$FOLDER_CONFIG" else # Find highest existing minor version >= START_MINOR LAST=$(ls -d "${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}".* 2>/dev/null \ | grep -oP '\d+ DEST="${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}.${NEXT}" # ─── Copy ───────────────────────────────────────────────────────────────────── cp -r "$SOURCE" "$DEST" echo "✓ Backed up '$(basename "$SOURCE")' → '$DEST'" ; then echo "Error: version must be in format like 2.0.15" exit 1 fi CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "Error: no config found for '$SOURCE'. Run a backup first." exit 1 fi BASE_VERSION="${NEW_VERSION%.*}" START_MINOR="${NEW_VERSION##*.}" echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "FORCE_MINOR=" >> "$FOLDER_CONFIG" echo "✓ Next backup of '$(basename "$SOURCE")' will be '$NEW_VERSION'" exit 0 fi # ─── Force version override ─────────────────────────────────────────────────── # Usage: backup_project --force 2.0.15 [folder] if [[ "$1" == "--force" ]]; then NEW_VERSION="$2" SOURCE="$(realpath "${3:-$(pwd)}")" if ! echo "$NEW_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+ # ─── Resolve source folder ──────────────────────────────────────────────────── # If no argument given, use current directory if [ -z "$1" ]; then SOURCE="$(pwd)" else SOURCE="$1" fi # Strip trailing slash, resolve to absolute path SOURCE="$(realpath "${SOURCE%/}")" if [ ! -d "$SOURCE" ]; then echo "Error: '$SOURCE' is not a directory." exit 1 fi # ─── Load or create config for this source folder ──────────────────────────── mkdir -p "$CONFIG_DIR" CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "First time backing up '$(basename "$SOURCE")'." read -rp "Enter starting version (e.g. 2.0.12): " FULL_VERSION # Validate format: must be X.Y.Z (digits and dots, at least one dot, ends in digits) if ! echo "$FULL_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+$'; then echo "Error: version must be in format like 2.0.12 or 1.0.0" exit 1 fi # Split into prefix (everything before last dot) and starting minor (last number) BASE_VERSION="${FULL_VERSION%.*}" # e.g. 2.0 START_MINOR="${FULL_VERSION##*.}" # e.g. 12 echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "Saved: prefix='$BASE_VERSION', starting minor='$START_MINOR' for '$(basename "$SOURCE")'." else source "$FOLDER_CONFIG" fi # ─── Find next version number ───────────────────────────────────────────────── BASE_NAME=$(basename "$SOURCE") PARENT_DIR=$(dirname "$SOURCE") # Find highest existing minor version, but only consider numbers >= START_MINOR LAST=$(ls -d "${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}".* 2>/dev/null \ | grep -oP '\d+$' \ | awk -v start="$START_MINOR" '$1 >= start' \ | sort -n \ | tail -1) if [ -z "$LAST" ]; then NEXT="$START_MINOR" else NEXT=$((LAST + 1)) fi DEST="${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}.${NEXT}" # ─── Copy ───────────────────────────────────────────────────────────────────── cp -r "$SOURCE" "$DEST" echo "✓ Backed up '$(basename "$SOURCE")' → '$DEST'" ; then echo "Error: version must be in format like 2.0.15" exit 1 fi CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "Error: no config found for '$SOURCE'. Run a backup first." exit 1 fi BASE_VERSION="${NEW_VERSION%.*}" FORCE_MINOR="${NEW_VERSION##*.}" echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$FORCE_MINOR" >> "$FOLDER_CONFIG" echo "FORCE_MINOR=$FORCE_MINOR" >> "$FOLDER_CONFIG" echo "✓ Next backup of '$(basename "$SOURCE")' will be forced to '$NEW_VERSION' (ignoring existing folders)" exit 0 fi # ─── Resolve source folder ──────────────────────────────────────────────────── # If no argument given, use current directory if [ -z "$1" ]; then SOURCE="$(pwd)" else SOURCE="$1" fi # Strip trailing slash, resolve to absolute path SOURCE="$(realpath "${SOURCE%/}")" if [ ! -d "$SOURCE" ]; then echo "Error: '$SOURCE' is not a directory." exit 1 fi # ─── Load or create config for this source folder ──────────────────────────── mkdir -p "$CONFIG_DIR" CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "First time backing up '$(basename "$SOURCE")'." read -rp "Enter starting version (e.g. 2.0.12): " FULL_VERSION # Validate format: must be X.Y.Z (digits and dots, at least one dot, ends in digits) if ! echo "$FULL_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+$'; then echo "Error: version must be in format like 2.0.12 or 1.0.0" exit 1 fi # Split into prefix (everything before last dot) and starting minor (last number) BASE_VERSION="${FULL_VERSION%.*}" # e.g. 2.0 START_MINOR="${FULL_VERSION##*.}" # e.g. 12 echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "Saved: prefix='$BASE_VERSION', starting minor='$START_MINOR' for '$(basename "$SOURCE")'." else source "$FOLDER_CONFIG" fi # ─── Find next version number ───────────────────────────────────────────────── BASE_NAME=$(basename "$SOURCE") PARENT_DIR=$(dirname "$SOURCE") # Find highest existing minor version, but only consider numbers >= START_MINOR LAST=$(ls -d "${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}".* 2>/dev/null \ | grep -oP '\d+$' \ | awk -v start="$START_MINOR" '$1 >= start' \ | sort -n \ | tail -1) if [ -z "$LAST" ]; then NEXT="$START_MINOR" else NEXT=$((LAST + 1)) fi DEST="${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}.${NEXT}" # ─── Copy ───────────────────────────────────────────────────────────────────── cp -r "$SOURCE" "$DEST" echo "✓ Backed up '$(basename "$SOURCE")' → '$DEST'" \ | awk -v start="$START_MINOR" '$1 >= start' \ | sort -n \ | tail -1) if [ -z "$LAST" ]; then NEXT="$START_MINOR" else NEXT=$((LAST + 1)) fi fi DEST="${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}.${NEXT}" # ─── Copy ───────────────────────────────────────────────────────────────────── cp -r "$SOURCE" "$DEST" echo "✓ Backed up '$(basename "$SOURCE")' → '$DEST'" ; then echo "Error: version must be in format like 2.0.15" exit 1 fi CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "Error: no config found for '$SOURCE'. Run a backup first." exit 1 fi BASE_VERSION="${NEW_VERSION%.*}" START_MINOR="${NEW_VERSION##*.}" echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "FORCE_MINOR=" >> "$FOLDER_CONFIG" echo "✓ Next backup of '$(basename "$SOURCE")' will be '$NEW_VERSION'" exit 0 fi # ─── Force version override ─────────────────────────────────────────────────── # Usage: backup_project --force 2.0.15 [folder] if [[ "$1" == "--force" ]]; then NEW_VERSION="$2" SOURCE="$(realpath "${3:-$(pwd)}")" if ! echo "$NEW_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+ # ─── Resolve source folder ──────────────────────────────────────────────────── # If no argument given, use current directory if [ -z "$1" ]; then SOURCE="$(pwd)" else SOURCE="$1" fi # Strip trailing slash, resolve to absolute path SOURCE="$(realpath "${SOURCE%/}")" if [ ! -d "$SOURCE" ]; then echo "Error: '$SOURCE' is not a directory." exit 1 fi # ─── Load or create config for this source folder ──────────────────────────── mkdir -p "$CONFIG_DIR" CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "First time backing up '$(basename "$SOURCE")'." read -rp "Enter starting version (e.g. 2.0.12): " FULL_VERSION # Validate format: must be X.Y.Z (digits and dots, at least one dot, ends in digits) if ! echo "$FULL_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+$'; then echo "Error: version must be in format like 2.0.12 or 1.0.0" exit 1 fi # Split into prefix (everything before last dot) and starting minor (last number) BASE_VERSION="${FULL_VERSION%.*}" # e.g. 2.0 START_MINOR="${FULL_VERSION##*.}" # e.g. 12 echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "Saved: prefix='$BASE_VERSION', starting minor='$START_MINOR' for '$(basename "$SOURCE")'." else source "$FOLDER_CONFIG" fi # ─── Find next version number ───────────────────────────────────────────────── BASE_NAME=$(basename "$SOURCE") PARENT_DIR=$(dirname "$SOURCE") # Find highest existing minor version, but only consider numbers >= START_MINOR LAST=$(ls -d "${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}".* 2>/dev/null \ | grep -oP '\d+$' \ | awk -v start="$START_MINOR" '$1 >= start' \ | sort -n \ | tail -1) if [ -z "$LAST" ]; then NEXT="$START_MINOR" else NEXT=$((LAST + 1)) fi DEST="${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}.${NEXT}" # ─── Copy ───────────────────────────────────────────────────────────────────── cp -r "$SOURCE" "$DEST" echo "✓ Backed up '$(basename "$SOURCE")' → '$DEST'" ; then echo "Error: version must be in format like 2.0.15" exit 1 fi CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "Error: no config found for '$SOURCE'. Run a backup first." exit 1 fi BASE_VERSION="${NEW_VERSION%.*}" FORCE_MINOR="${NEW_VERSION##*.}" echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$FORCE_MINOR" >> "$FOLDER_CONFIG" echo "FORCE_MINOR=$FORCE_MINOR" >> "$FOLDER_CONFIG" echo "✓ Next backup of '$(basename "$SOURCE")' will be forced to '$NEW_VERSION' (ignoring existing folders)" exit 0 fi # ─── Resolve source folder ──────────────────────────────────────────────────── # If no argument given, use current directory if [ -z "$1" ]; then SOURCE="$(pwd)" else SOURCE="$1" fi # Strip trailing slash, resolve to absolute path SOURCE="$(realpath "${SOURCE%/}")" if [ ! -d "$SOURCE" ]; then echo "Error: '$SOURCE' is not a directory." exit 1 fi # ─── Load or create config for this source folder ──────────────────────────── mkdir -p "$CONFIG_DIR" CONFIG_KEY=$(echo "$SOURCE" | tr '/' '_' | tr ' ' '_') FOLDER_CONFIG="$CONFIG_DIR/${CONFIG_KEY}.cfg" if [ ! -f "$FOLDER_CONFIG" ]; then echo "First time backing up '$(basename "$SOURCE")'." read -rp "Enter starting version (e.g. 2.0.12): " FULL_VERSION # Validate format: must be X.Y.Z (digits and dots, at least one dot, ends in digits) if ! echo "$FULL_VERSION" | grep -qP '^\d+(\.\d+)*\.\d+$'; then echo "Error: version must be in format like 2.0.12 or 1.0.0" exit 1 fi # Split into prefix (everything before last dot) and starting minor (last number) BASE_VERSION="${FULL_VERSION%.*}" # e.g. 2.0 START_MINOR="${FULL_VERSION##*.}" # e.g. 12 echo "BASE_VERSION=$BASE_VERSION" > "$FOLDER_CONFIG" echo "START_MINOR=$START_MINOR" >> "$FOLDER_CONFIG" echo "Saved: prefix='$BASE_VERSION', starting minor='$START_MINOR' for '$(basename "$SOURCE")'." else source "$FOLDER_CONFIG" fi # ─── Find next version number ───────────────────────────────────────────────── BASE_NAME=$(basename "$SOURCE") PARENT_DIR=$(dirname "$SOURCE") # Find highest existing minor version, but only consider numbers >= START_MINOR LAST=$(ls -d "${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}".* 2>/dev/null \ | grep -oP '\d+$' \ | awk -v start="$START_MINOR" '$1 >= start' \ | sort -n \ | tail -1) if [ -z "$LAST" ]; then NEXT="$START_MINOR" else NEXT=$((LAST + 1)) fi DEST="${PARENT_DIR}/${BASE_NAME}-${BASE_VERSION}.${NEXT}" # ─── Copy ───────────────────────────────────────────────────────────────────── cp -r "$SOURCE" "$DEST" echo "✓ Backed up '$(basename "$SOURCE")' → '$DEST'"