@@ -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 |