#!/bin/bash # AutoNAS Media Importer # Advanced media import engine that processes, organizes and imports media files from cameras # Usage: autonas-media-importer.sh [options] # Features: organization patterns (ymd/ym/d/h/m/y), GoPro handling, metadata sync LOG_TAG="autonas-import" VERSION="2.0" # Default values (compatible with original) ORGANIZATION="ymd" FILENAME_MODE="full" VERIFY_MODE="size" DATE_SOURCE="auto" SYNC_METADATA=0 UNATTENDED=1 COLLECT_UNSORTABLE=0 KEEP_EMPTY_DIRS=1 DRY_RUN=0 VERBOSE=0 KEEP_ORIGINALS=0 FILE_LIMIT=0 CONFLICT_APPLY_ALL="" RESOLVED_DESTINATION_PATH="" RESERVED_DESTINATION_PATHS=() WORK_BASE="${AUTONAS_IMPORT_WORKDIR:-/dev/shm/autonas-media-importer}" TOTAL_FILES=0 PROCESSED_FILES=0 SKIPPED_FILES=0 ERROR_FILES=0 TOTAL_SIZE=0 PROCESSED_SIZE=0 START_TIME=$(date +%s) RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE=$'\033[1;36m' NC='\033[0m' MEDIA_EXTENSIONS=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v") print_color() { local color="$1" local message="$2" echo -e "${color}${message}${NC}" } log_message() { local message="$1" local priority="${2:-info}" logger -p "local0.$priority" -t "$LOG_TAG" "$message" if [ -t 1 ]; then case "$priority" in "err") print_color "$RED" "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: $message" >&2 ;; "warning") print_color "$YELLOW" "$(date '+%Y-%m-%d %H:%M:%S') - WARNING: $message" ;; "info") if [[ $VERBOSE -eq 1 ]]; then print_color "$BLUE" "$(date '+%Y-%m-%d %H:%M:%S') - INFO: $message" fi ;; *) echo "$(date '+%Y-%m-%d %H:%M:%S') - $message" ;; esac fi } show_help() { cat << EOF AutoNAS Media Importer v$VERSION Advanced camera import engine Usage: $0 [OPTIONS] Arguments: source_mount Mount point of camera destination_path Destination directory for imported files Options: -o, --organization y|m|d|h|ym|ymd (default: ymd) -F, --filename-mode auto|full|orig (default: full) --date-source auto|exif|filesystem (default: auto) --sync-metadata Write date into destination metadata --unattended Never prompt; resolve destination conflicts with numeric suffixes --collect-unsortable Put undated files into DEST/unsortable --keep-empty-dirs Keep empty directories after processing --dry-run Show actions without changing files --keep-originals Copy files instead of moving --verbose Enable verbose output --limit N Process only N files -h, --help Show this help EOF } check_dependencies() { if ! command -v exiftool &> /dev/null; then print_color "$RED" "ERROR: exiftool is required but not installed" exit 1 fi } # Utility functions get_file_size() { local file="$1" if [[ -f "$file" ]]; then if stat -c%s "$file" >/dev/null 2>&1; then stat -c%s "$file" 2>/dev/null elif stat -f%z "$file" >/dev/null 2>&1; then stat -f%z "$file" 2>/dev/null else ls -ln "$file" | awk '{print $5}' fi else echo "0" fi } is_gopro_media_file() { local filename filename=$(basename "$1") [[ "$filename" =~ ^G[HPX][0-9]{6}\.[Mm][Pp]4$ ]] } should_prefer_gopro_filesystem_date() { is_gopro_media_file "$1" } date_to_exiftool_format() { local date_str="$1" if [[ "$date_str" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})[[:space:]]+([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]]; then echo "${BASH_REMATCH[1]}:${BASH_REMATCH[2]}:${BASH_REMATCH[3]} ${BASH_REMATCH[4]}:${BASH_REMATCH[5]}:${BASH_REMATCH[6]}" return 0 fi return 1 } sync_destination_metadata_to_date() { local file="$1" local date_str="$2" local exif_date exif_date=$(date_to_exiftool_format "$date_str") || return 1 # GoPro filesystem timestamps are local camera time. QuickTime integer # timestamps should be stored as UTC, so let ExifTool convert from local # time on write instead of storing the local clock value as if it were UTC. exiftool -api QuickTimeUTC=1 -overwrite_original \ "-CreateDate=$exif_date" \ "-DateTimeOriginal=$exif_date" \ "-DateTime=$exif_date" \ "-ModifyDate=$exif_date" \ "-MediaCreateDate=$exif_date" \ "-TrackCreateDate=$exif_date" \ "-QuickTime:CreateDate=$exif_date" \ "-QuickTime:ModifyDate=$exif_date" \ "$file" >/dev/null 2>&1 } set_filesystem_mtime_to_date() { local file="$1" local date_str="$2" if [[ "$OSTYPE" == "darwin"* ]]; then local touch_date touch_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%Y%m%d%H%M.%S" 2>/dev/null) || return 1 touch -t "$touch_date" "$file" else touch -d "$date_str" "$file" fi } should_sync_imported_metadata() { local original_filename="$1" local date_source="$2" if [[ $SYNC_METADATA -eq 1 ]]; then return 0 fi [[ "$date_source" == Filesystem* ]] && is_gopro_media_file "$original_filename" } filesystem_date_reference() { local file="$1" # GoPro MP4 files carry the capture time reliably in the file's own # filesystem timestamp. THM/LRV sidecars can drift to chapter/end times on # some cards, so they must not override the MP4 timestamp. echo "$file" } extract_filesystem_date() { local file="$1" if [[ ! -e "$file" ]]; then return 2 fi local epoch="" if [[ "$OSTYPE" == "darwin"* ]]; then epoch=$(stat -f %m "$file" 2>/dev/null || echo "") [[ -n "$epoch" ]] || return 2 date -j -r "$epoch" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || return 2 else epoch=$(stat -c %Y "$file" 2>/dev/null || echo "") [[ -n "$epoch" ]] || return 2 date -d "@$epoch" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || return 2 fi } destination_path_unavailable() { local candidate="$1" [[ -e "$candidate" ]] && return 0 for reserved_path in "${RESERVED_DESTINATION_PATHS[@]}"; do [[ "$reserved_path" == "$candidate" ]] && return 0 done return 1 } reserve_destination_path() { local candidate="$1" if [[ -n "$candidate" ]]; then RESERVED_DESTINATION_PATHS+=("$candidate") fi } extract_file_date() { local file="$1" local create_date="" local date_source="" if [[ "$DATE_SOURCE" == "filesystem" ]] || { [[ "$DATE_SOURCE" == "auto" ]] && should_prefer_gopro_filesystem_date "$file"; }; then local filesystem_reference filesystem_reference=$(filesystem_date_reference "$file") create_date=$(extract_filesystem_date "$filesystem_reference") || return 2 if is_gopro_media_file "$file"; then echo "$create_date|Filesystem:GoPro" else echo "$create_date|Filesystem" fi return 0 fi # Try EXIF data local exif_output=$(exiftool -G1 -s -CreateDate -DateTimeOriginal -DateTime "$file" 2>/dev/null) if [[ -n "$exif_output" ]]; then while IFS= read -r line; do if [[ "$line" =~ ^\[([^\]]+)\][[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$ ]]; then local group="${BASH_REMATCH[1]}" local tag="${BASH_REMATCH[2]}" local value="${BASH_REMATCH[3]}" tag=$(echo "$tag" | sed 's/[[:space:]]*$//') if [[ "$tag" == "DateTimeOriginal" && -z "$create_date" ]]; then create_date="$value" date_source="$group:$tag" elif [[ "$tag" == "CreateDate" && "$date_source" != *"DateTimeOriginal"* ]]; then create_date="$value" date_source="$group:$tag" elif [[ "$tag" == "DateTime" && -z "$create_date" ]]; then create_date="$value" date_source="$group:$tag" fi fi done <<< "$exif_output" fi # Fallback to filesystem in auto mode if [[ -z "$create_date" && "$DATE_SOURCE" == "auto" ]]; then local filesystem_reference filesystem_reference=$(filesystem_date_reference "$file") create_date=$(extract_filesystem_date "$filesystem_reference") || return 2 date_source="Filesystem" fi [[ -z "$create_date" ]] && return 2 # Convert EXIF format (YYYY:MM:DD HH:MM:SS) to standard if [[ "$create_date" =~ ^([0-9]{4}):([0-9]{1,2}):([0-9]{1,2})[[:space:]]*([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$ ]]; then local year="${BASH_REMATCH[1]}" local month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") local day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") local hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))") local minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))") local second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))") create_date="$year-$month-$day $hour:$minute:$second" fi # QuickTime UTC conversion if [[ "$date_source" == *"QuickTime"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then local utc_timestamp=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$create_date" "+%s" 2>/dev/null) [[ -n "$utc_timestamp" ]] && create_date=$(date -j -r "$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) else local utc_timestamp=$(date -d "$create_date UTC" "+%s" 2>/dev/null) [[ -n "$utc_timestamp" ]] && create_date=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) fi fi echo "$create_date|$date_source" return 0 } generate_destination_path() { local date_str="$1" local original_filename="$2" local base_destination="$3" local year month day hour minute second if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$date_str" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})[[:space:]]*([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$ ]]; then year="${BASH_REMATCH[1]}" month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))") day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))") hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))") minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))") second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))") else return 1 fi else year=$(date -d "$date_str" "+%Y" 2>/dev/null) month=$(date -d "$date_str" "+%m" 2>/dev/null) day=$(date -d "$date_str" "+%d" 2>/dev/null) hour=$(date -d "$date_str" "+%H" 2>/dev/null) minute=$(date -d "$date_str" "+%M" 2>/dev/null) second=$(date -d "$date_str" "+%S" 2>/dev/null) fi [[ -z "$year" || -z "$month" || -z "$day" ]] && return 1 local extension="${original_filename##*.}" local lowercase_ext=$(echo "$extension" | tr '[:upper:]' '[:lower:]') local dir_path="" local filename="" case "$ORGANIZATION" in "y") dir_path="$base_destination/$year" filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}" ;; "m") dir_path="$base_destination/$year/$month" filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}" ;; "d") dir_path="$base_destination/$year/$month/$day" filename="${hour}-${minute}-${second}.${lowercase_ext}" ;; "h") dir_path="$base_destination/$year/$month/$day/$hour" filename="${minute}-${second}.${lowercase_ext}" ;; "ym") dir_path="$base_destination/${year}-${month}" filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}" ;; "ymd") dir_path="$base_destination/${year}-${month}-${day}" filename="${hour}-${minute}-${second}.${lowercase_ext}" ;; *) return 1 ;; esac case "$FILENAME_MODE" in orig) filename="$original_filename" ;; full) filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}" ;; auto) ;; *) filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}" ;; esac echo "$dir_path/$filename" return 0 } ensure_unique_destination_path() { local desired_path="$1" local counter=1 local resolved_path="$desired_path" while destination_path_unavailable "$resolved_path"; do local dir=$(dirname "$desired_path") local base=$(basename "$desired_path") local name_without_ext="${base%.*}" local ext="${base##*.}" if [[ "$ext" == "$base" ]]; then resolved_path="$dir/${name_without_ext}_${counter}" else resolved_path="$dir/${name_without_ext}_${counter}.$ext" fi counter=$((counter + 1)) [[ $counter -gt 1000 ]] && return 1 done echo "$resolved_path" return 0 } resolve_destination_conflict() { local desired_path="$1" RESOLVED_DESTINATION_PATH="" [[ -z "$desired_path" ]] && return 1 if ! destination_path_unavailable "$desired_path"; then RESOLVED_DESTINATION_PATH="$desired_path" reserve_destination_path "$RESOLVED_DESTINATION_PATH" return 0 fi local resolved_path resolved_path=$(ensure_unique_destination_path "$desired_path") [[ -z "$resolved_path" ]] && return 1 RESOLVED_DESTINATION_PATH="$resolved_path" reserve_destination_path "$RESOLVED_DESTINATION_PATH" return 0 } safe_mv() { local src="$1" dst="$2" mv "$src" "$dst" 2> >(grep -v "set flags (was:" >&2) } safe_cp() { local src="$1" dst="$2" cp -p "$src" "$dst" 2> >(grep -v "set flags (was:" >&2) } verify_copied_file() { local src="$1" local dst="$2" if [[ ! -f "$dst" ]]; then log_message "Verified copy missing at destination: $dst" "err" return 1 fi if [[ "$VERIFY_MODE" == "none" ]]; then return 0 fi local src_size dst_size src_size=$(get_file_size "$src") dst_size=$(get_file_size "$dst") if [[ "$src_size" != "$dst_size" ]]; then log_message "Size mismatch after copy: $src ($src_size) != $dst ($dst_size)" "err" return 1 fi if [[ "$VERIFY_MODE" == "strict" ]] && ! cmp -s "$src" "$dst"; then log_message "Content mismatch after copy: $src -> $dst" "err" return 1 fi return 0 } copy_with_verification() { local src="$1" local dst="$2" if [[ -e "$dst" ]]; then log_message "Refusing to overwrite existing destination: $dst" "err" return 1 fi local dst_dir tmp dst_dir=$(dirname "$dst") tmp=$(mktemp "$dst_dir/.media-importer.$(basename "$dst").tmp.XXXXXX") || return 1 rm -f "$tmp" if ! safe_cp "$src" "$tmp"; then rm -f "$tmp" return 1 fi if ! verify_copied_file "$src" "$tmp"; then rm -f "$tmp" return 1 fi if [[ -e "$dst" ]]; then log_message "Destination appeared during copy, refusing to overwrite: $dst" "err" rm -f "$tmp" return 1 fi if ! safe_mv "$tmp" "$dst"; then rm -f "$tmp" return 1 fi } available_bytes_for_path() { local path="$1" df -PB1 "$path" 2>/dev/null | awk 'NR==2 {print $4}' } staging_directory_for_file() { local file_size="$1" local needs_metadata_sync="$2" local dest_dir="$3" local required_bytes available_bytes required_bytes=$((file_size + 67108864)) if [[ "$needs_metadata_sync" -eq 1 ]]; then required_bytes=$((file_size * 2 + 67108864)) fi if mkdir -p "$WORK_BASE" 2>/dev/null; then available_bytes=$(available_bytes_for_path "$WORK_BASE") if [[ "$available_bytes" =~ ^[0-9]+$ && "$available_bytes" -gt "$required_bytes" ]]; then echo "$WORK_BASE" return 0 fi log_message "Not enough space in $WORK_BASE for staging; falling back to destination staging" "warning" fi echo "$dest_dir" } copy_with_staging() { local src="$1" local dst="$2" local date_str="$3" local sync_metadata="$4" if [[ -e "$dst" ]]; then log_message "Refusing to overwrite existing destination: $dst" "err" return 1 fi local dst_dir src_size stage_dir stage_tmp dst_dir=$(dirname "$dst") src_size=$(get_file_size "$src") stage_dir=$(staging_directory_for_file "$src_size" "$sync_metadata" "$dst_dir") mkdir -p "$stage_dir" || return 1 stage_tmp=$(mktemp "$stage_dir/.media-importer.$(basename "$dst").stage.XXXXXX") || return 1 rm -f "$stage_tmp" if ! safe_cp "$src" "$stage_tmp"; then rm -f "$stage_tmp" return 1 fi if ! verify_copied_file "$src" "$stage_tmp"; then rm -f "$stage_tmp" return 1 fi if [[ "$sync_metadata" -eq 1 ]]; then if ! sync_destination_metadata_to_date "$stage_tmp" "$date_str"; then log_message "Failed to sync metadata date for staged file: $stage_tmp" "warning" rm -f "$stage_tmp" return 1 fi fi set_filesystem_mtime_to_date "$stage_tmp" "$date_str" || log_message "Failed to set filesystem timestamp for staged file: $stage_tmp" "warning" if ! copy_with_verification "$stage_tmp" "$dst"; then rm -f "$stage_tmp" return 1 fi set_filesystem_mtime_to_date "$dst" "$date_str" || log_message "Failed to set filesystem timestamp for $dst" "warning" rm -f "$stage_tmp" } verified_move_file() { local src="$1" local dst="$2" if ! copy_with_verification "$src" "$dst"; then return 1 fi rm -f "$src" } # Process a single file process_file() { local file="$1" local relative_path="${file#$SOURCE_MOUNT/}" local file_size=$(get_file_size "$file") TOTAL_SIZE=$((TOTAL_SIZE + file_size)) # Check mount point if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then log_message "Error: Source mount point is no longer available" "err" return 1 fi # Check if file still exists if [[ ! -f "$file" ]]; then log_message "Error: File no longer exists: $relative_path" "err" log_message "Camera appears to be disconnected, stopping import" "warning" exit 1 fi [[ $VERBOSE -eq 1 ]] && log_message "Processing: $relative_path" "info" # Extract date local date_info=$(extract_file_date "$file") local extract_status=$? if [[ $extract_status -eq 2 ]]; then if [[ $COLLECT_UNSORTABLE -eq 1 ]]; then local unsortable_dir="$DESTINATION/unsortable" mkdir -p "$unsortable_dir" local unsortable_path="$unsortable_dir/$(basename "$file")" resolve_destination_conflict "$unsortable_path" [[ $? -eq 0 ]] && unsortable_path="$RESOLVED_DESTINATION_PATH" if [[ $DRY_RUN -eq 0 ]]; then safe_mv "$file" "$unsortable_path" log_message "Moved to unsortable: $relative_path" "info" fi else log_message "No date found for $relative_path - skipping" "warning" fi SKIPPED_FILES=$((SKIPPED_FILES + 1)) return 1 elif [[ $extract_status -ne 0 ]]; then log_message "Failed to extract date from $relative_path" "warning" SKIPPED_FILES=$((SKIPPED_FILES + 1)) return 1 fi local date_str="${date_info%|*}" local date_source="${date_info#*|}" [[ $VERBOSE -eq 1 ]] && log_message "Date: $date_str (from $date_source)" "info" # Generate destination path local original_basename=$(basename "$file") local dest_path=$(generate_destination_path "$date_str" "$original_basename" "$DESTINATION") [[ $? -ne 0 ]] && { log_message "Could not generate destination path for $relative_path" "err"; ERROR_FILES=$((ERROR_FILES + 1)); return 1; } local desired_dest_path="$dest_path" resolve_destination_conflict "$dest_path" [[ $? -eq 0 ]] && dest_path="$RESOLVED_DESTINATION_PATH" || { ERROR_FILES=$((ERROR_FILES + 1)); return 1; } [[ "$dest_path" != "$desired_dest_path" ]] && log_message "Destination conflict resolved" "info" local dest_dir=$(dirname "$dest_path") if [[ $DRY_RUN -eq 1 ]]; then if [[ $KEEP_ORIGINALS -eq 1 ]]; then echo "Would copy: $relative_path -> ${dest_path#$DESTINATION/}" else echo "Would move: $relative_path -> ${dest_path#$DESTINATION/}" fi if should_sync_imported_metadata "$original_basename" "$date_source"; then echo "Would sync metadata date: ${dest_path#$DESTINATION/} -> $date_str" fi PROCESSED_FILES=$((PROCESSED_FILES + 1)) PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) return 0 fi mkdir -p "$dest_dir" || { log_message "Could not create directory: $dest_dir" "err"; ERROR_FILES=$((ERROR_FILES + 1)); return 1; } local sync_metadata_after_copy=0 if should_sync_imported_metadata "$original_basename" "$date_source"; then sync_metadata_after_copy=1 fi if [[ $KEEP_ORIGINALS -eq 1 ]]; then if copy_with_staging "$file" "$dest_path" "$date_str" "$sync_metadata_after_copy"; then log_message "Copied: $relative_path" "info" [[ $VERBOSE -eq 1 ]] && echo "✓ Copied" PROCESSED_FILES=$((PROCESSED_FILES + 1)) PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) return 0 else log_message "Failed to copy $relative_path" "err" ERROR_FILES=$((ERROR_FILES + 1)) return 1 fi else if copy_with_staging "$file" "$dest_path" "$date_str" "$sync_metadata_after_copy" && rm -f "$file"; then log_message "Moved: $relative_path" "info" [[ $VERBOSE -eq 1 ]] && echo "✓ Moved" PROCESSED_FILES=$((PROCESSED_FILES + 1)) PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) return 0 else log_message "Failed to move $relative_path" "err" ERROR_FILES=$((ERROR_FILES + 1)) return 1 fi fi } # Parse command line arguments SOURCE_MOUNT="" DESTINATION="" while [[ $# -gt 0 ]]; do case $1 in -o|--organization) ORGANIZATION="$2" [[ ! "$ORGANIZATION" =~ ^(y|m|d|h|ym|ymd)$ ]] && { echo "Invalid organization pattern"; exit 1; } shift 2 ;; -F|--filename-mode) FILENAME_MODE="$2" [[ ! "$FILENAME_MODE" =~ ^(auto|full|orig)$ ]] && { echo "Invalid filename mode"; exit 1; } shift 2 ;; --date-source) DATE_SOURCE="$2" [[ ! "$DATE_SOURCE" =~ ^(auto|exif|filesystem)$ ]] && { echo "Invalid date source"; exit 1; } shift 2 ;; --sync-metadata) SYNC_METADATA=1 shift ;; --unattended) UNATTENDED=1 shift ;; --collect-unsortable) COLLECT_UNSORTABLE=1 shift ;; --keep-empty-dirs) KEEP_EMPTY_DIRS=0 shift ;; --dry-run) DRY_RUN=1 shift ;; --keep-originals) KEEP_ORIGINALS=1 shift ;; -v|--verbose) VERBOSE=1 shift ;; --limit) FILE_LIMIT="$2" [[ ! "$FILE_LIMIT" =~ ^[0-9]+$ ]] && { echo "Error: --limit requires a number"; exit 1; } shift 2 ;; -h|--help) show_help exit 0 ;; -*) echo "Unknown option: $1" exit 1 ;; *) if [[ -z "$SOURCE_MOUNT" ]]; then SOURCE_MOUNT="$1" elif [[ -z "$DESTINATION" ]]; then DESTINATION="$1" else echo "Too many arguments" exit 1 fi shift ;; esac done [[ -z "$SOURCE_MOUNT" || -z "$DESTINATION" ]] && { echo "Error: Both source_mount and destination_path are required"; show_help; exit 1; } # Validate paths [[ ! -d "$SOURCE_MOUNT" ]] && { log_message "Error: Source mount point does not exist: $SOURCE_MOUNT" "err"; exit 1; } ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null && log_message "Warning: Source path is not a mount point: $SOURCE_MOUNT" "warning" if [[ $DRY_RUN -eq 0 ]]; then mkdir -p "$DESTINATION" || { log_message "Error: Cannot create destination directory: $DESTINATION" "err"; exit 1; } fi check_dependencies # Find camera directories find_camera_directories() { local search_patterns=("DCIM" "PRIVATE" "MP_ROOT" "AVCHD" "Photos" "Videos") local found_dirs=() ! timeout 3 ls "$SOURCE_MOUNT" >/dev/null 2>&1 && { log_message "Error: Mount point is not accessible" "err"; exit 1; } for pattern in "${search_patterns[@]}"; do while IFS= read -r -d '' dir; do found_dirs+=("$dir") done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type d -iname "$pattern" -print0 2>/dev/null) done if [[ ${#found_dirs[@]} -eq 0 ]]; then log_message "No camera directories found, searching for media files..." "info" for ext in "${MEDIA_EXTENSIONS[@]}"; do while IFS= read -r -d '' file; do local dir=$(dirname "$file") local already_added=0 for found_dir in "${found_dirs[@]}"; do [[ "$found_dir" == "$dir" ]] && { already_added=1; break; } done [[ $already_added -eq 0 ]] && found_dirs+=("$dir") done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type f -iname "$ext" -print0 2>/dev/null) done fi printf '%s\n' "${found_dirs[@]}" | sort -u } log_message "Starting camera import from $SOURCE_MOUNT to $DESTINATION" "info" [[ $DRY_RUN -eq 1 ]] && log_message "DRY RUN MODE - No files will be moved/copied" "info" [[ $KEEP_ORIGINALS -eq 1 ]] && log_message "KEEP ORIGINALS MODE - Files will be copied instead of moved" "info" log_message "Scanning for camera directories..." "info" camera_dirs=$(find_camera_directories) if [[ -z "$camera_dirs" ]]; then log_message "No camera directories or media files found in $SOURCE_MOUNT" "warning" exit 0 fi echo "Found camera directories:" echo "$camera_dirs" | while IFS= read -r dir; do echo " $dir" done # Clean up GLV files (preview files - usually not needed) log_message "Cleaning up GLV preview files..." "info" glv_count=0 while IFS= read -r dir; do if [[ $DRY_RUN -eq 1 ]]; then glv_files=$(find "$dir" -type f -iname "*.glv" 2>/dev/null | wc -l) [[ $glv_files -gt 0 ]] && echo "Would delete $glv_files GLV files from $dir" && glv_count=$((glv_count + glv_files)) else while IFS= read -r -d '' glv_file; do rm "$glv_file" 2>/dev/null && glv_count=$((glv_count + 1)) done < <(find "$dir" -type f -iname "*.glv" -print0 2>/dev/null) fi done <<< "$camera_dirs" [[ $glv_count -gt 0 ]] && log_message "GLV files cleaned up: $glv_count" "info" # Process media files log_message "Processing media files..." "info" max_files_to_show=20 files_shown=0 files_processed_count=0 while IFS= read -r dir; do ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null && { log_message "Camera disconnected, stopping import" "warning"; break; } for ext in "${MEDIA_EXTENSIONS[@]}"; do while IFS= read -r -d '' file; do TOTAL_FILES=$((TOTAL_FILES + 1)) files_processed_count=$((files_processed_count + 1)) ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null && { log_message "Camera disconnected during processing" "warning"; exit 1; } [[ $FILE_LIMIT -gt 0 && $files_processed_count -gt $FILE_LIMIT ]] && { echo "Reached file limit"; break 3; } if [[ $DRY_RUN -eq 1 && $files_shown -ge $max_files_to_show ]]; then [[ $files_shown -eq $max_files_to_show ]] && echo "... (limiting output in dry-run mode)" ((files_shown++)) else [[ $DRY_RUN -eq 1 ]] && ((files_shown++)) process_file "$file" fi done < <(find "$dir" -type f -iname "$ext" -print0 2>/dev/null) done done <<< "$camera_dirs" # Summary log_message "Import completed: $PROCESSED_FILES/$TOTAL_FILES files processed successfully" "info" [[ $ERROR_FILES -gt 0 ]] && log_message "Import had errors: $ERROR_FILES files failed" "warning" echo "" echo "=== Import Summary ===" echo "Total files found: $TOTAL_FILES" echo "Successfully processed: $PROCESSED_FILES" echo "Skipped: $SKIPPED_FILES" echo "Errors: $ERROR_FILES" [[ $glv_count -gt 0 ]] && echo "GLV files cleaned up: $glv_count" # Cleanup empty directories if [[ $KEEP_EMPTY_DIRS -eq 1 && $DRY_RUN -eq 0 ]]; then log_message "Cleaning up empty directories..." "info" find "$DESTINATION" -type d -empty -not -path "$DESTINATION" -delete 2>/dev/null || true fi [[ $ERROR_FILES -gt 0 ]] && exit 1 || exit 0