Skip to main content

Overview

Camera Workflow is designed to be 100% safe and idempotent. The recovery system ensures you can safely resume conversions after interruptions (crashes, Ctrl+C, power loss) without data loss or duplicate work.
Idempotent: Running the same conversion command multiple times produces identical results, safely skipping already-converted files.

Core Safety Guarantees

1. Original Preservation

Originals are preserved by default (--keep-originals=true):
  • Source files never modified during conversion
  • Deletion requires explicit opt-out (--keep-originals=false)
  • Triple verification before any deletion occurs

2. Atomic Conversions

All conversions use temporary files with atomic rename:
// Convert to .tmp file first
tempPath := outputPath + ".tmp"
ffmpeg -i input.mov -c:v hevc output.mp4.tmp

// Verify integrity
VerifyOutputFile(tempPath)

// Atomic rename (never overwrites partially-written files)
os.Rename(tempPath, outputPath)
Interruptions at any point leave either:
  • No output file (conversion never started)
  • .tmp file (conversion incomplete, auto-cleaned on next run)
  • Complete valid file (conversion succeeded)

3. Processing Markers

Track in-flight conversions with .processing files (security.go:211-231):
PID:12345
Started:2024-08-15T14:30:22Z
File:/dest/2024/08-August/2024-08-15/videos/vacation.mp4
Purpose:
  • Identify abandoned conversions from crashed processes
  • Enable cleanup of orphaned .tmp files
  • Prevent concurrent conversions of the same file

Recovery Mechanisms

Corruption Detection (security.go:187-209)

Before skipping existing files, verify integrity:
func (s *SecurityChecker) IsFileCorrupted(filePath, fileType string) bool {
    // Check file exists and is not empty
    info, err := os.Stat(filePath)
    if err != nil || info.Size() == 0 {
        return true
    }
    
    // Verify file integrity based on type
    switch fileType {
    case "photo":
        return s.verifyImageIntegrity(filePath) != nil  // Uses ImageMagick identify
    case "video":
        return s.verifyVideoIntegrity(filePath) != nil  // Uses ffprobe
    }
}
Per-File Type Verification:
  • Images (security.go:107-114): magick identify <file> - detects truncated/corrupted images
  • Videos (security.go:116-125): ffprobe <file> - validates container and streams

Abandoned File Cleanup (security.go:282-319)

Run automatically before each conversion batch:
func (s *SecurityChecker) CleanupAbandonedFiles(dir string) error {
    // Remove orphaned .tmp files from interrupted conversions
    // Remove .processing markers from dead processes
    // Log cleanup actions
}
Detection Logic (security.go:256-280):
func (s *SecurityChecker) isMarkerAbandoned(markerPath string) bool {
    // Read PID from .processing file
    content, _ := os.ReadFile(markerPath)
    pid := extractPID(content)
    
    // Check if process still exists
    return !processExists(pid)
}

Idempotent Conversion Flow

Every conversion follows this recovery-aware sequence (video.go:174-191):
// 1. Check if already converted and valid
if _, err := os.Stat(outputPath); err == nil {
    if !c.security.IsFileCorrupted(outputPath, "video") {
        // Valid file exists, skip
        c.logger.Info("already exists and valid, skipping")
        c.stats.skippedFiles++
        return nil
    } else {
        // Corrupted file detected, remove and re-convert
        c.logger.Warn("corrupted file detected, re-converting")
        os.Remove(outputPath)
        c.stats.recoveredFiles++
    }
}

// 2. Create processing marker
c.security.CreateProcessingMarker(outputPath)
defer c.security.RemoveProcessingMarker(outputPath)

// 3. Convert to .tmp file
// 4. Verify integrity
// 5. Atomic rename

Recovery Workflow

When resuming after interruption:

Step 1: Startup Cleanup

media-converter /source /dest
Before processing files:
  1. Scans destination for .processing markers
  2. Checks if PIDs are still running
  3. Removes abandoned markers and .tmp files
  4. Logs cleanup actions
Example Output:
🔒 Security Check: Found 2 abandoned conversions from previous run
🔒 Cleanup: Removed vacation.mp4.tmp (incomplete conversion)
🔒 Cleanup: Removed beach.mp4.processing (dead process 12345)

Step 2: Integrity Verification

For each file in source:
  1. Check if output exists in destination
  2. If exists, verify integrity with external tools
  3. If valid → skip
  4. If corrupted → remove and re-convert
  5. If missing → convert
Example Output:
📹 vacation.mov → 2024-08-15_vacation.mp4 (already exists and valid, skipping)
📹 beach.mov → 2024-08-15_beach.mp4 (corrupted file detected, re-converting)
📹 sunset.mov → 2024-08-15_sunset.mp4 (converting...)

Step 3: Safe Re-conversion

Corrupted files are automatically re-converted:
  • Original source file remains untouched
  • Corrupted output removed before re-conversion
  • recoveredFiles counter incremented
  • Standard conversion flow proceeds

Processing Marker Format

Created at conversion start (security.go:211-222): File: <output_path>.processing Content:
PID:12345
Started:2024-08-15T14:30:22Z
File:/dest/2024/08-August/2024-08-15/videos/vacation.mp4
Lifecycle:
  • Created: When conversion starts (before FFmpeg/ImageMagick)
  • Removed: When conversion completes successfully
  • Orphaned: If process crashes/killed before completion
  • Cleaned: On next run by CleanupAbandonedFiles()
Do not manually delete .processing files while conversions are running. This can lead to duplicate concurrent conversions of the same file.

Integrity Verification

Output Verification (security.go:51-105)

Called after every conversion before finalizing:
func (s *SecurityChecker) VerifyOutputFile(
    inputPath, outputPath, fileType, outputFormat string,
) error {
    // 1. Check file exists
    // 2. Check file is not empty
    // 3. Verify minimum size ratio
    // 4. Run format-specific integrity check
}

Minimum Size Ratios

Prevents accepting files that are suspiciously small (config.go:82-84):
min_output_size_ratio
float
default:"0.005"
Default minimum size ratio (0.5% of original)
min_output_size_ratio_avif
float
default:"0.001"
AVIF minimum size ratio (0.1% of original) - highly compressed format
min_output_size_ratio_webp
float
default:"0.003"
WebP minimum size ratio (0.3% of original)
Validation (security.go:74-94):
minSize := int64(float64(inputSize) * ratio)
if outputSize < minSize {
    os.Remove(outputPath)  // Clean up suspicious file
    return fmt.Errorf("output file too small (%d < %d bytes, ratio %.3f for %s)",
        outputSize, minSize, ratio, outputFormat)
}

Format-Specific Checks

Images (security.go:107-114):
magick identify /path/to/output.avif
# Exit code 0 = valid, non-zero = corrupted
Videos (security.go:116-125):
ffprobe /path/to/output.mp4
# Exit code 0 = valid, non-zero = corrupted
Failures trigger automatic cleanup:
if err := cmd.Run(); err != nil {
    os.Remove(outputPath)  // Clean up corrupted file
    return fmt.Errorf("file is corrupted: %s", outputPath)
}

Safe Deletion

When --keep-originals=false, triple verification before deletion (security.go:127-145):
func (s *SecurityChecker) SafeDelete(filePath, outputPath string) error {
    // 1. Verify output file exists
    outputInfo, err := os.Stat(outputPath)
    if err != nil {
        return fmt.Errorf("cannot verify output file before deletion: %w", err)
    }
    
    // 2. Ensure output is not suspiciously small
    if outputInfo.Size() < 1000 {
        return fmt.Errorf("deletion cancelled for safety: output file too small")
    }
    
    // 3. Perform deletion
    if err := os.Remove(filePath); err != nil {
        return fmt.Errorf("failed to delete original file: %w", err)
    }
    
    return nil
}
Deletion failures are logged as warnings, not errors. The conversion is still considered successful if the output file is valid.

Configuration

Timeout Settings

Prevent hung conversions from blocking progress:
--timeout-photo
integer
default:"300"
Photo conversion timeout in seconds (5 minutes)
--timeout-video
integer
default:"1800"
Video conversion timeout in seconds (30 minutes)
Usage:
# Increase timeout for 4K videos
media-converter /source /dest --timeout-video 3600
Implementation (video.go:226-228):
ctx, cancel := context.WithTimeout(context.Background(), c.config.ConversionTimeoutVideo)
defer cancel()
cmd := exec.CommandContext(ctx, "ffmpeg", ...)

Skip Existing Files

Disable integrity verification for faster re-runs (not recommended):
# DANGER: Skips corruption detection
media-converter /source /dest --skip-integrity-check
There is no --skip-integrity-check flag by design. Integrity verification is mandatory to ensure recovery system reliability.

Monitoring Recovery

The converter tracks recovery statistics:
type ConversionStats struct {
    skippedFiles    int  // Already converted and valid
    recoveredFiles  int  // Re-converted due to corruption
    // ...
}
Final Report:
✅ Conversion complete!
   Total files processed: 150
   Successfully converted: 42
   Skipped (already converted): 105
   Recovered (re-converted corrupted): 3
   Failed: 0

Best Practices

1. Regular Backups

While the recovery system is robust, maintain source backups:
# Always keep originals during first conversion
media-converter /source /dest --keep-originals

# Verify destination integrity before deleting sources
media-converter /source /dest --verify-checksum

2. Test Recovery

Verify recovery behavior before production use:
# Start conversion
media-converter /source /dest

# Interrupt with Ctrl+C after a few files

# Resume - should skip completed files and cleanup .tmp files
media-converter /source /dest

3. Monitor Corruption

Watch for recovered file counts:
# High recovery count may indicate:
# - Storage issues (failing disk)
# - Insufficient resources (OOM during conversion)
# - Software bugs

4. Tune Timeouts

Adjust based on content characteristics:
~/.media-converter.yaml
# Large 4K video library
timeout_video: 3600  # 60 minutes for very large files

# RAW photos from high-resolution cameras
timeout_photo: 600   # 10 minutes for 100MP+ files

Troubleshooting

Files Repeatedly Re-converted

Symptom: Same file shows as “re-converting” on every run Causes:
  1. Output file consistently fails integrity check
  2. FFmpeg/ImageMagick not in PATH
  3. Corrupted external tool installation
Diagnosis:
# Test integrity check manually
ffprobe /dest/2024/08-August/2024-08-15/videos/vacation.mp4
magick identify /dest/2024/08-August/2024-08-15/images/photo.avif

Orphaned .processing Files

Symptom: .processing files remain after conversion completes Causes:
  1. Process killed with kill -9 (bypasses defer cleanup)
  2. System crash before defer runs
  3. Storage I/O errors during marker removal
Solution:
# Manual cleanup
find /dest -name "*.processing" -delete

# Or re-run converter (auto-cleans on startup)
media-converter /source /dest

High Skipped File Count

Symptom: Most files reported as “already exists and valid, skipping” Expected Behavior: This is correct idempotent operation! If Unintended:
  • Check if source/destination paths are correct
  • Verify date extraction is working (files organized by correct dates)
  • Use --dry-run to preview what would happen

Performance Tuning

Optimize conversion speed and resource usage

Adaptive Workers

Dynamic concurrency management

Build docs developers (and LLMs) love