Showing 1 changed files with 262 additions and 144 deletions
+262 -144
media-importer.sh
@@ -9,8 +9,9 @@ VERSION="1.0"
9 9
 SCRIPT_NAME="Standalone Media Importer"
10 10
 
11 11
 # Default values
12
-ORGANIZATION=""
13
-FORCE_FULL_DATE=0
12
+# Default organization: 'ymd' (single folder per day yyyy-mm-dd). Override with -o/--organization.
13
+ORGANIZATION="ymd"
14
+FILENAME_MODE="full"  # options: auto, full, orig
14 15
 COLLECT_UNSORTABLE=0
15 16
 SOURCE_PATTERNS=()
16 17
 DESTINATION=""
@@ -31,7 +32,14 @@ START_TIME=$(date +%s)
31 32
 RED='\033[0;31m'
32 33
 GREEN='\033[0;32m'
33 34
 YELLOW='\033[1;33m'
34
-BLUE='\033[0;34m'
35
+# BLUE is used for informational/verbose messages. Dark terminal themes may render the
36
+# default blue hard to read, so use a brighter cyan by default and allow users to
37
+# override via the VERBOSE_COLOR environment variable (must be a terminal escape seq).
38
+if [[ -n "${VERBOSE_COLOR:-}" ]]; then
39
+    BLUE="$VERBOSE_COLOR"
40
+else
41
+    BLUE=$'\033[1;36m'  # bright cyan
42
+fi
35 43
 NC='\033[0m' # No Color
36 44
 
37 45
 # Function to print colored output
@@ -70,73 +78,41 @@ log_message() {
70 78
 
71 79
 # Function to display help
72 80
 show_help() {
73
-    cat << EOF
74
-$SCRIPT_NAME v$VERSION
81
+        cat << EOF
82
+    $SCRIPT_NAME v$VERSION
75 83
 
76
-USAGE:
84
+Usage:
77 85
     $0 [OPTIONS]
78 86
 
79
-DESCRIPTION:
80
-    Organizes media files (photos and videos) by date with various naming patterns.
81
-    if [[ $exif_found -eq 0 ]]; then
82
-        log_message "Warning: No EXIF date found for $file. Using filesystem modification time."
83
-    -o, --organization PATTERN
84
-        Organization pattern:
85
-        y -> target/yyyy/mm-dd_hh-mm-ss.orig_ext
86
-        m -> target/yyyy/mm/dd_hh-mm-ss.orig_ext  
87
-        d -> target/yyyy/mm/dd/mm-dd_hh-mm-ss.orig_ext
88
-        h -> target/yyyy/mm/dd/hh/mm-ss.orig_ext
89
-
90
-    --full-date
91
-        Force all files to be named with full date (yyyy-mm-dd_hh-mm-ss.ext) in the destination folder, regardless of organization pattern.
92
-
93
-    -s, --source PATTERN
94
-        Source folder pattern(s) with simple regex support (*^$)
95
-        Can be specified multiple times
96
-        Examples:
97
-            -s "/DCIM/*Video$"
98
-            -s "/path/to/photos"
99
-            -s "*.jpg"
100
-        Default: all subfolders in current directory except destination
101
-
102
-    -d, --destination PATH
103
-        Destination folder (default: ./sorted)
104
-
105
-    -k, --keep-originals
106
-        Keep original files (copy instead of move)
107
-
108
-    --dry-run
109
-        Show what would be done without actually doing it
110
-
111
-    -v, --verbose
112
-        Enable verbose output
113
-
114
-    -h, --help
115
-        Show this help message
116
-
117
-    --version
118
-        Show version information
119
-
120
-EXAMPLES:
121
-    # Basic usage - organize all media in current directory
122
-    $0
123
-
124
-    # Organize with monthly folders, keep originals
125
-    $0 -o m -k
126
-
127
-    # Process specific folders with hourly organization
128
-    # Return date and source (no warnings or debug output)
129
-    echo "$create_date|$date_source"
130
-    # Dry run with verbose output
131
-    $0 --dry-run -v -s "*.mov" -d "/tmp/test"
132
-
133
-DEPENDENCIES:
134
-    Required: exiftool
135
-    Optional: mediainfo, file (for enhanced metadata detection)
136
-
87
+What it does:
88
+    Sorts photos and videos into dated folders (by year/month/day/hour) and
89
+    generates filenames from the file's creation timestamp or preserves the
90
+    original name.
91
+
92
+Options:
93
+    -o, --organization PATTERN   y|m|d|h|ym|ymd   (default: ymd)
94
+    -F, --filename-mode MODE     auto|full|orig    (default: full)
95
+    -s, --source PATH            File or directory to process (repeatable). Default: cwd
96
+    -d, --destination PATH       Destination folder. Required when multiple -s are given.
97
+        -k, --keep-originals         Copy files instead of moving
98
+    --collect-unsortable         Put files without dates into DEST/unsortable
99
+    --dry-run                   Show actions without changing files
100
+    -v, --verbose                Verbose output
101
+    -h, --help                   Show this help
102
+    --version                    Show version
103
+
104
+Examples:
105
+    $0 -s /path/to/photos
106
+    $0 -s /path/to/DCIM -d /mnt/sorted --dry-run
107
+
108
+Dependencies:
109
+    exiftool (required). mediainfo and file are optional.
137 110
 EOF
138 111
 }
139 112
 
113
+# Central media extensions list (used by find functions)
114
+MEDIA_EXTENSIONS=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v")
115
+
140 116
 # Function to show version
141 117
 show_version() {
142 118
     echo "$SCRIPT_NAME v$VERSION"
@@ -183,22 +159,85 @@ check_dependencies() {
183 159
     log_message "All required dependencies found" "SUCCESS"
184 160
 }
185 161
 
186
-# Function to get file size in bytes
162
+# Determine filesystem/device ID for a path (portable between Linux and macOS)
163
+get_dev() {
164
+    # Return a device identifier for the supplied path (portable between GNU stat and BSD stat)
165
+    local path="$1"
166
+    if [[ -z "$path" ]]; then
167
+        path="."
168
+    fi
169
+
170
+    # Prefer GNU stat if available
171
+    if stat --version >/dev/null 2>&1; then
172
+        stat -c %d "$path" 2>/dev/null || stat -c %i "$path" 2>/dev/null || echo ""
173
+    else
174
+        # BSD/macOS stat
175
+        stat -f %d "$path" 2>/dev/null || stat -f %i "$path" 2>/dev/null || echo ""
176
+    fi
177
+}
178
+
179
+# Determine the mount root for a path by walking up until device changes
180
+get_mountpoint() {
181
+    local path="$1"
182
+    if [[ -z "$path" ]]; then path="."; fi
183
+    # Resolve to absolute
184
+    local abs
185
+    abs=$(cd "$path" 2>/dev/null && pwd) || abs="$path"
186
+    # Walk up until device differs or we reach /
187
+    local parent="$abs"
188
+    local root_dev
189
+    root_dev=$(get_dev "$parent")
190
+    while [[ "$parent" != "/" ]]; do
191
+        local next_parent
192
+        next_parent=$(dirname "$parent")
193
+        local next_dev
194
+        next_dev=$(get_dev "$next_parent")
195
+        if [[ "$next_dev" != "$root_dev" ]]; then
196
+            break
197
+        fi
198
+        parent="$next_parent"
199
+    done
200
+    echo "$parent"
201
+}
202
+
203
+# Function to get file size in bytes (portable between Linux and macOS)
187 204
 get_file_size() {
188 205
     local file="$1"
189 206
     if [[ -f "$file" ]]; then
190
-        if command -v stat &> /dev/null; then
191
-            # Try GNU stat first (Linux)
207
+        # Try GNU stat
208
+        if stat -c%s "$file" >/dev/null 2>&1; then
192 209
             stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null
210
+        elif stat -f%z "$file" >/dev/null 2>&1; then
211
+            stat -f%z "$file" 2>/dev/null
193 212
         else
194 213
             # Fallback to ls
195
-            ls -l "$file" | awk '{print $5}'
214
+            ls -ln "$file" | awk '{print $5}'
196 215
         fi
197 216
     else
198 217
         echo "0"
199 218
     fi
200 219
 }
201 220
 
221
+# (Removed checksum/prefix/conflict helper functions to revert to pre-conflict-resolution behavior)
222
+
223
+# Safe move/copy helpers: filter out benign "set flags (was: ...): Operation not supported"
224
+# which appears when moving files onto filesystems that don't support BSD file flags
225
+# (macOS mv may try to preserve flags and print this warning while still succeeding).
226
+safe_mv() {
227
+    local src="$1" dst="$2"
228
+    # Redirect stderr through a filter that removes the known benign message
229
+    mv "$src" "$dst" 2> >(grep -v "set flags (was:" >&2)
230
+    return $?
231
+}
232
+
233
+# Use simple safe_mv/safe_cp for moving/copying files. Removed atomic installer to let exiftool or filesystem handle renames.
234
+
235
+safe_cp() {
236
+    local src="$1" dst="$2"
237
+    cp "$src" "$dst" 2> >(grep -v "set flags (was:" >&2)
238
+    return $?
239
+}
240
+
202 241
 # Function to format file size
203 242
 format_size() {
204 243
     local size=$1
@@ -345,11 +384,21 @@ generate_destination_path() {
345 384
     # Generate path and filename based on organization pattern
346 385
     local dir_path=""
347 386
     local filename=""
348
-    if [[ $FORCE_FULL_DATE -eq 1 ]]; then
387
+
388
+    # If no organization specified, use flat destination (base) and choose filename per mode
389
+    if [[ -z "$ORGANIZATION" ]]; then
349 390
         dir_path="$base_destination"
350
-        filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
351
-    else
352
-        case "$ORGANIZATION" in
391
+        if [[ "$FILENAME_MODE" == "orig" ]]; then
392
+            filename="$original_filename"
393
+        else
394
+            # full or auto both map to full date for flat layout
395
+            filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
396
+        fi
397
+        echo "$dir_path/$filename"
398
+        return 0
399
+    fi
400
+
401
+    case "$ORGANIZATION" in
353 402
             "y")
354 403
                 dir_path="$base_destination/$year"
355 404
                 filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
@@ -366,68 +415,98 @@ generate_destination_path() {
366 415
                 dir_path="$base_destination/$year/$month/$day/$hour"
367 416
                 filename="${minute}-${second}.${lowercase_ext}"
368 417
                 ;;
418
+            "ym")
419
+                # Single folder per month named yyyy-mm; filename includes day and time
420
+                dir_path="$base_destination/${year}-${month}"
421
+                filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}"
422
+                ;;
423
+            "ymd"|"ymd-")
424
+                # Single folder per day named yyyy-mm-dd; filename is time
425
+                dir_path="$base_destination/${year}-${month}-${day}"
426
+                filename="${hour}-${minute}-${second}.${lowercase_ext}"
427
+                ;;
369 428
             *)
370 429
                 log_message "Invalid organization pattern: $ORGANIZATION" "ERROR"
371 430
                 return 1
372 431
                 ;;
373 432
         esac
374
-    fi
433
+
434
+    # Apply filename mode overrides
435
+    case "$FILENAME_MODE" in
436
+        orig)
437
+            filename="$original_filename"
438
+            ;;
439
+        full)
440
+            filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
441
+            ;;
442
+        auto)
443
+            # keep the auto-generated filename from the organization case
444
+            ;;
445
+        *)
446
+            # fallback to full
447
+            filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
448
+            ;;
449
+    esac
450
+
375 451
     echo "$dir_path/$filename"
376 452
     return 0
377 453
 }
378 454
 
379 455
 # Function to find files matching patterns
380 456
 find_source_files() {
381
-    local files=()
382
-    
457
+    # Emit absolute file paths for all media files under sources, excluding DESTINATION when it is inside a source
458
+    local abs_dest=""
459
+    if [[ -n "$DESTINATION" ]]; then
460
+        abs_dest=$(cd "$DESTINATION" 2>/dev/null && pwd) || abs_dest="$DESTINATION"
461
+    fi
462
+
463
+    # Build -iname expression for find
464
+    local ext_expr=""
465
+    for ext in "${MEDIA_EXTENSIONS[@]}"; do
466
+        if [[ -n "$ext_expr" ]]; then
467
+            ext_expr="$ext_expr -o"
468
+        fi
469
+        ext_expr="$ext_expr -iname $ext"
470
+    done
471
+
383 472
     if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
384
-        # Default: find all media files in current directory and subdirectories
385
-        # Exclude destination directory if it's a subdirectory of current directory
386
-    local find_cmd="find -L . -type f"
387
-        
388
-        # Add exclusion for destination if it's relative to current directory
389
-        if [[ "$DESTINATION" =~ ^\./.*$ ]] || [[ "$DESTINATION" =~ ^[^/].*$ ]]; then
390
-            local abs_dest=$(cd "$DESTINATION" 2>/dev/null && pwd)
391
-            local abs_current=$(pwd)
392
-            if [[ "$abs_dest" == "$abs_current"* ]]; then
393
-                find_cmd="$find_cmd ! -path \"$DESTINATION/*\""
394
-            fi
473
+        # Default: scan current directory
474
+        local start_dot="."
475
+        local abs_current
476
+        abs_current=$(pwd)
477
+        local find_cmd=(find -L "$start_dot" -type f)
478
+        # If dest is inside cwd, add exclusion
479
+        if [[ -n "$abs_dest" && "$abs_dest" == "$abs_current"* ]]; then
480
+            find_cmd+=( ! -path "$abs_dest/*" ! -path "$abs_dest" )
395 481
         fi
396
-        
397
-        # Add media file extensions
398
-        local extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v")
399
-        local ext_pattern=""
400
-        for ext in "${extensions[@]}"; do
401
-            if [[ -n "$ext_pattern" ]]; then
402
-                ext_pattern="$ext_pattern -o"
403
-            fi
404
-            ext_pattern="$ext_pattern -iname $ext"
405
-        done
406
-        
407
-    eval "$find_cmd \\( $ext_pattern \\)" | while IFS= read -r file; do
408
-            echo "$file"
409
-        done
482
+        # Add expression
483
+        # shellcheck disable=SC2068
484
+        "${find_cmd[@]}" \( $ext_expr \) 2>/dev/null || true
410 485
     else
411
-        # Use specified patterns
412
-        for pattern in "${SOURCE_PATTERNS[@]}"; do
413
-            # Handle different pattern types
414
-            if [[ -d "$pattern" ]]; then
415
-                # Directory pattern
416
-                find -L "$pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) 2>/dev/null
417
-            elif [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then
418
-                # Glob pattern
419
-                for file in $pattern; do
420
-                    if [[ -f "$file" ]]; then
421
-                        echo "$file"
486
+        # Scan each provided source
487
+        for src in "${SOURCE_PATTERNS[@]}"; do
488
+            if [[ -f "$src" ]]; then
489
+                # single file - skip if it's inside dest
490
+                local abs_file
491
+                abs_file=$(cd "$(dirname "$src")" 2>/dev/null && pwd)/$(basename "$src")
492
+                if [[ -n "$abs_dest" && "$abs_file" == "$abs_dest"* ]]; then
493
+                    continue
494
+                fi
495
+                echo "$abs_file"
496
+            elif [[ -d "$src" ]]; then
497
+                local abs_src
498
+                abs_src=$(cd "$src" 2>/dev/null && pwd)
499
+                if [[ -n "$abs_src" ]]; then
500
+                    if [[ -n "$abs_dest" && "$abs_dest" == "$abs_src"* ]]; then
501
+                        find -L "$abs_src" -type f \( $ext_expr \) ! -path "$abs_dest/*" ! -path "$abs_dest" 2>/dev/null || true
502
+                    else
503
+                        find -L "$abs_src" -type f \( $ext_expr \) 2>/dev/null || true
422 504
                     fi
423
-                done
424
-            else
425
-                # Exact file or directory
426
-                if [[ -f "$pattern" ]]; then
427
-                    echo "$pattern"
428
-                elif [[ -d "$pattern" ]]; then
429
-                    find -L "$pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) 2>/dev/null
505
+                else
506
+                    print_color "$YELLOW" "Warning: Could not resolve source directory: $src"
430 507
                 fi
508
+            else
509
+                print_color "$YELLOW" "Warning: Source path does not exist or is not a file/directory: $src"
431 510
             fi
432 511
         done
433 512
     fi
@@ -481,17 +560,11 @@ process_file() {
481 560
             return 2
482 561
     fi
483 562
     
484
-    # Handle filename conflicts
485
-    local counter=1
486
-    local original_dest_path="$dest_path"
487
-    while [[ -f "$dest_path" ]]; do
488
-        local dir_path=$(dirname "$original_dest_path")
489
-        local filename=$(basename "$original_dest_path")
490
-        local name_without_ext="${filename%.*}"
491
-        local ext="${filename##*.}"
492
-        dest_path="$dir_path/${name_without_ext}_${counter}.${ext}"
493
-        counter=$((counter + 1))
494
-    done
563
+    # If destination exists, do not attempt complex conflict resolution here.
564
+    # Let external tools (exiftool) or filesystem semantics handle renames/overwrites.
565
+    if [[ -f "$dest_path" ]]; then
566
+        log_message "Destination already exists: $dest_path - proceeding to move/copy and letting external tools handle conflicts" "WARNING"
567
+    fi
495 568
     
496 569
     local dest_dir=$(dirname "$dest_path")
497 570
     
@@ -513,26 +586,26 @@ process_file() {
513 586
         return 1
514 587
     fi
515 588
     
516
-    # Copy or move file
589
+    # Copy or move file using safe helpers (filter benign stderr). Let external tools handle renaming conflicts.
517 590
     if [[ $KEEP_ORIGINALS -eq 1 ]]; then
518
-        if cp "$file" "$dest_path"; then
591
+        if safe_cp "$file" "$dest_path"; then
519 592
             log_message "Copied: $file -> $dest_path" "SUCCESS"
520 593
             PROCESSED_FILES=$((PROCESSED_FILES + 1))
521 594
             PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
522 595
             return 0
523 596
         else
524
-            log_message "Failed to copy: $file" "ERROR"
597
+            log_message "Failed to copy: $file -> $dest_path" "ERROR"
525 598
             ERROR_FILES=$((ERROR_FILES + 1))
526 599
             return 1
527 600
         fi
528 601
     else
529
-        if mv "$file" "$dest_path"; then
602
+        if safe_mv "$file" "$dest_path"; then
530 603
             log_message "Moved: $file -> $dest_path" "SUCCESS"
531 604
             PROCESSED_FILES=$((PROCESSED_FILES + 1))
532 605
             PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
533 606
             return 0
534 607
         else
535
-            log_message "Failed to move: $file" "ERROR"
608
+            log_message "Failed to move: $file -> $dest_path" "ERROR"
536 609
             ERROR_FILES=$((ERROR_FILES + 1))
537 610
             return 1
538 611
         fi
@@ -593,16 +666,23 @@ while [[ $# -gt 0 ]]; do
593 666
     case $1 in
594 667
         -o|--organization)
595 668
             ORGANIZATION="$2"
596
-            if [[ ! "$ORGANIZATION" =~ ^[ymdh]$ ]]; then
597
-                print_color "$RED" "Error: Invalid organization pattern. Must be one of: y, m, d, h"
669
+            # Accept new patterns: ym, ymd and ymd- as well as single-letter ones
670
+            if [[ ! "$ORGANIZATION" =~ ^(y|m|d|h|ym|ymd|ymd-)$ ]]; then
671
+                print_color "$RED" "Error: Invalid organization pattern. Must be one of: y, m, d, h, ym, ymd"
598 672
                 exit 1
599 673
             fi
600 674
             shift 2
601 675
             ;;
602
-        --full-date)
603
-            FORCE_FULL_DATE=1
604
-            shift
676
+        
677
+        -F|--filename-mode)
678
+            FILENAME_MODE="$2"
679
+            if [[ ! "$FILENAME_MODE" =~ ^(auto|full|orig)$ ]]; then
680
+                print_color "$RED" "Error: Invalid filename mode. Must be one of: auto, full, orig"
681
+                exit 1
682
+            fi
683
+            shift 2
605 684
             ;;
685
+    # (conflict resolution option removed; let exiftool/filesystem handle naming conflicts)
606 686
         --collect-unsortable)
607 687
             COLLECT_UNSORTABLE=1
608 688
             shift
@@ -643,14 +723,52 @@ while [[ $# -gt 0 ]]; do
643 723
     esac
644 724
 done
645 725
 
646
-# Set default destination if not specified
726
+# If no organization is provided, leave ORGANIZATION empty and filename mode will decide naming
727
+
728
+# Set default destination: if user didn't provide -d and a source was given, use first source's directory + /sorted
647 729
 if [[ -z "$DESTINATION" ]]; then
648
-    DESTINATION="./sorted"
730
+    if [[ ${#SOURCE_PATTERNS[@]} -gt 1 ]]; then
731
+        print_color "$RED" "Error: Multiple sources specified - destination (-d|--destination) is required when using multiple sources."
732
+        echo "Use -h for help."
733
+        exit 1
734
+    fi
735
+
736
+    if [[ ${#SOURCE_PATTERNS[@]} -gt 0 ]]; then
737
+        # If exactly one source provided, place 'sorted' in the source's mount root
738
+        first_source="${SOURCE_PATTERNS[0]}"
739
+        if [[ -e "$first_source" ]]; then
740
+            mount_root=$(get_mountpoint "$first_source")
741
+            if [[ -n "$mount_root" ]]; then
742
+                DESTINATION="$mount_root/sorted"
743
+            else
744
+                # Fallback to dirname of source
745
+                DESTINATION="$(dirname "$first_source")/sorted"
746
+            fi
747
+        else
748
+            # Source doesn't exist; fallback to ./sorted but warn
749
+            print_color "$YELLOW" "Warning: first source does not exist; defaulting destination to ./sorted"
750
+            DESTINATION="./sorted"
751
+        fi
752
+    else
753
+        DESTINATION="./sorted"
754
+    fi
649 755
 fi
650 756
 
651
-# If no organization is provided, default to flat full-date naming
652
-if [[ -z "$ORGANIZATION" ]]; then
653
-    FORCE_FULL_DATE=1
757
+# If no source specified, default to current directory. Refuse to run when cwd is unsafe.
758
+if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
759
+    cwd=$(pwd)
760
+    # Resolve home and root paths
761
+    home_dir="$HOME"
762
+    case "$cwd" in
763
+        "$home_dir"|"/"|"/etc"|"/var"|"/bin"|"/sbin"|"/dev"|"/proc"|"/sys"|"/run"|"/usr"|"/lib"|"/lib64"|"/boot"|"/snap")
764
+            print_color "$RED" "Refusing to run with default source in unsafe directory: $cwd"
765
+            print_color "$RED" "Please specify $cwd as source explicitly with -s or run from a safer directory."
766
+            exit 1
767
+            ;;
768
+        *)
769
+            SOURCE_PATTERNS+=("$cwd")
770
+            ;;
771
+    esac
654 772
 fi
655 773
 
656 774
 # Convert destination to absolute path