Copy-only mode archives media files to a destination without any conversion, using checksums to verify integrity and detect duplicates.
Overview
Perfect for creating verified backups or organizing existing media without modifying formats:
media-converter --copy-only /source /dest
Key features:
- Checksum verification (XXHash64)
- Duplicate detection
- Atomic copy operations
- Same date-based organization
- Parallel processing
Basic Usage
Simple Archive Copy
media-converter --copy-only /Volumes/Camera/DCIM /Volumes/Backup/Photos
Copies all media files with verification, skipping duplicates.
With Date Organization
media-converter --copy-only --organize-by-date /source /dest
Organizes copied files by date (same structure as conversion mode):
dest/
├── 2024/
│ ├── 03-March/
│ │ ├── 2024-03-15/
│ │ │ ├── images/
│ │ │ │ ├── IMG_1234.CR2
│ │ │ │ └── IMG_1235.JPG
│ │ │ └── videos/
│ │ │ └── VID_5678.MOV
Dry Run Mode
media-converter --copy-only --dry-run /source /dest
Previews what would be copied without actually copying files.
Checksum Verification
How It Works
Every file is verified using XXHash64 checksums:
Calculate source checksum
srcChecksum, err := c.hasher.Calculate(srcPath)
// Result: 0x1a2b3c4d5e6f7890
Copy to temporary file
tmpPath := destPath + ".tmp"
copyFileData(srcPath, tmpPath)
Verify copied checksum
copiedChecksum, err := c.hasher.Calculate(tmpPath)
if copiedChecksum != srcChecksum {
return fmt.Errorf("checksum mismatch")
}
Atomic rename
os.Rename(tmpPath, destPath)
XXHash64 is used for its speed (~GB/s throughput) while maintaining reliability for duplicate detection.
Enable Verification
media-converter --copy-only --verify-checksum /source /dest
With --verify-checksum, existing files in the destination are indexed before copying to enable duplicate detection.
Duplicate Detection
Session Duplicates
Detects duplicates within the same copy session:
✅ Copied: IMG_1234.JPG → /dest/images/IMG_1234.JPG (checksum: 1a2b3c4d)
⏭️ Skipped duplicate: IMG_1234_copy.JPG (same as IMG_1234.JPG, checksum: 1a2b3c4d)
Existing File Duplicates
With --verify-checksum, detects duplicates against existing files:
media-converter --copy-only --verify-checksum /source /dest
Indexed 1,234 existing files for duplicate detection
...
⏭️ Skipped duplicate: IMG_5678.JPG (matches existing /dest/2024/03-March/2024-03-15/images/IMG_5678.JPG, checksum: 9e8d7c6b)
Duplicate Detection Logic
if c.tracker.IsDuplicate(srcChecksum) {
if original, ok := c.tracker.FirstPath(srcChecksum); ok {
// Skip duplicate
c.logger.Info(fmt.Sprintf("⏭️ Skipped duplicate: %s (same as %s)",
filepath.Base(srcPath),
filepath.Base(original)))
return result, nil
}
}
Atomic Copy Operations
Copy Process
All copies are atomic to prevent corruption:
Step 1: Copy to temp
Step 2: Verify
Step 3: Atomic rename
tmpPath := destPath + ".tmp"
if err := copyFileData(srcPath, tmpPath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("copy data: %w", err)
}
Files are copied with .tmp suffix.copiedChecksum, err := c.hasher.Calculate(tmpPath)
if copiedChecksum != srcChecksum {
os.Remove(tmpPath)
return fmt.Errorf("checksum mismatch")
}
Checksum verification ensures perfect copy.if err := os.Rename(tmpPath, destPath); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("finalize copy: %w", err)
}
Atomic rename makes the copy visible.
If interrupted (Ctrl+C, crash), .tmp files are automatically cleaned up on next run.
Parallel Processing
Worker Configuration
Same as conversion mode, control concurrent copies:
# Auto-detect workers (CPU cores - 1)
media-converter --copy-only /source /dest
# Explicit worker count
media-converter --copy-only --jobs 8 /source /dest
# Maximum parallelism
media-converter --copy-only --jobs 16 /source /dest
For disk-to-disk copies, too many workers can hurt performance due to disk I/O contention. Start with 4-8 workers.
# Recommended for most scenarios
media-converter --copy-only --jobs 4 /source /dest
Progress & Statistics
Real-time Progress
📈 Progress: [████████████████████░░░░░] 187/234 (79.9%) | ETA: 2m45s
✅ Copied: IMG_1234.CR2 → /dest/2024/03-March/2024-03-15/images/IMG_1234.CR2 (checksum: a1b2c3d4)
⏭️ Skipped duplicate: IMG_1234_copy.CR2 (same as IMG_1234.CR2, checksum: a1b2c3d4)
Final Summary
╔══════════════════════════════════════════════════════════════╗
║ Copy-Only Mode Complete ║
╚══════════════════════════════════════════════════════════════╝
✅ Files processed: 234/234
📦 Files copied: 198
⏭️ Duplicates skipped: 36
💾 Data copied: 45.2 GB
⏱️ Total time: 8m34s
🔐 Time spent on checksum verification: 1m12s
📁 Archived files in: /Volumes/Backup/Photos
📄 Detailed logs: /Volumes/Backup/Photos/conversion.log
Incompatible Flags
Copy-only mode disables conversion-specific flags:
The following flags cannot be used with --copy-only:
# ❌ These will cause an error:
--photo-format
--photo-quality-avif
--photo-quality-webp
--video-codec
--video-crf
--video-acceleration
--adaptive-workers
--adaptive-workers-*
Validation happens before copying starts:
if cfg.CopyOnly {
incompatible := []string{"photo-format", "video-codec", ...}
for _, name := range incompatible {
if cmd.Flags().Changed(name) {
return fmt.Errorf("--%s cannot be used with --copy-only mode", name)
}
}
}
Use Cases
Camera Card Backup
Create verified backup from camera card:
media-converter \
--copy-only \
--verify-checksum \
--organize-by-date \
--jobs 4 \
/Volumes/SDCARD /Volumes/Backup/Camera-2024-03-15
Archive Consolidation
Merge multiple sources with duplicate detection:
# Copy first source
media-converter --copy-only --verify-checksum /source1 /archive
# Add second source (skips duplicates)
media-converter --copy-only --verify-checksum /source2 /archive
# Add third source (skips duplicates)
media-converter --copy-only --verify-checksum /source3 /archive
Pre-flight Verification
Test copy before conversion:
Dry run copy
media-converter --copy-only --dry-run /source /dest
Actual copy
media-converter --copy-only /source /dest
Convert in place
media-converter --photo-format avif /dest /dest
Configuration File
Store copy-only preferences in ~/.media-converter.yaml:
# Copy-only mode settings
copy_only: true
verify_checksum: true
organize_by_date: true
max_jobs: 4
# Organization
language: en
Then run:
media-converter /source /dest
Checksum Storage
Checksums are tracked in memory during operation:
type Tracker struct {
mu sync.RWMutex
checksums map[uint64][]string // checksum -> file paths
}
func (t *Tracker) Register(checksum uint64, path string) {
t.mu.Lock()
defer t.mu.Unlock()
t.checksums[checksum] = append(t.checksums[checksum], path)
}
func (t *Tracker) IsDuplicate(checksum uint64) bool {
t.mu.RLock()
defer t.mu.RUnlock()
return len(t.checksums[checksum]) > 0
}
Checksums are not persisted to disk after completion. Re-run with --verify-checksum to detect duplicates against existing files.
Error Handling
Checksum Mismatch
If a copied file doesn’t match the source checksum:
❌ Failed to copy IMG_1234.JPG: checksum mismatch: source=a1b2c3d4 copied=e5f6g7h8
The .tmp file is automatically deleted and the error is logged.
Disk Space
Copy-only mode respects disk space checks:
# Skip disk space check (not recommended)
media-converter --copy-only --skip-disk-check /source /dest
Permission Errors
Permission errors are logged and skipped:
⚠️ Failed to copy IMG_9999.JPG: permission denied
Comparison with rsync
| Feature | Camera Workflow --copy-only | rsync |
|---|
| Checksum verification | ✅ XXHash64 (fast) | ✅ MD5/XXHash (slower) |
| Duplicate detection | ✅ Within and across runs | ❌ No cross-run detection |
| Date organization | ✅ EXIF/metadata-based | ❌ Filesystem only |
| Atomic operations | ✅ .tmp + rename | ✅ .tmp + rename |
| Progress tracking | ✅ ETA and statistics | ✅ Basic progress |
| Parallel processing | ✅ Configurable workers | ❌ Single-threaded |
| Media-specific | ✅ Photo/video formats | ❌ Generic files |
Use rsync for general file syncing. Use Camera Workflow --copy-only for media-specific workflows with duplicate detection.