Skip to main content
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:
1

Calculate source checksum

srcChecksum, err := c.hasher.Calculate(srcPath)
// Result: 0x1a2b3c4d5e6f7890
2

Copy to temporary file

tmpPath := destPath + ".tmp"
copyFileData(srcPath, tmpPath)
3

Verify copied checksum

copiedChecksum, err := c.hasher.Calculate(tmpPath)
if copiedChecksum != srcChecksum {
    return fmt.Errorf("checksum mismatch")
}
4

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:
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.
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

Performance Considerations

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:
1

Dry run copy

media-converter --copy-only --dry-run /source /dest
2

Actual copy

media-converter --copy-only /source /dest
3

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

FeatureCamera Workflow --copy-onlyrsync
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.

Build docs developers (and LLMs) love