jpenguin icon

fat32_copy.py

jpenguin | PRO | 12/26/25 08:43:26 PM UTC (Edited) | 0 ⭐ | 11812 👁️ | Never ⏰ | []
Python |

4.42 KB

|

None

|

0 👍

/

0 👎

#!/usr/bin/env python3
import os
import shutil
import unicodedata
import re
from pathlib import Path
import sys
 
# Copy a directory (~/Masic/RythmBox) to a FAT32 formated
# flash drive, named MUSIC, converting all filenames
# This was written with GPT-5 Mini via duck.ai as a bash script,
# Some manual changes and it was fed into Google Gemini
# Gemini then made further optimizations and rewrote it in python
# --- CONFIGURATION ---
HOME = Path.home()
SOURCE_DIR = HOME / "Music/RythmBox"
DEST_DIR = Path("/media") / HOME.name / "MUSIC"
MAX_BYTES = 255
 
# --- PROGRESS BAR SETUP ---
# Try to import tqdm. If missing, create a dummy fallback.
try:
    from tqdm import tqdm
    HAS_TQDM = True
except ImportError:
    HAS_TQDM = False
    print("Tip: Run 'pip3 install tqdm' for a nicer progress bar with ETA.")
 
    # Fallback class to mimic tqdm if library is missing
    class tqdm:
        def __init__(self, total=0, unit='files', **kwargs):
            self.total = total
            self.n = 0
        def update(self, n=1):
            self.n += n
            sys.stdout.write(f"\rProcessed: {self.n}/{self.total}")
            sys.stdout.flush()
        def close(self):
            print("")
        def write(self, msg):
            sys.stdout.write(f"\r{msg}\n")
        def __enter__(self): return self
        def __exit__(self, *args): self.close()
 
def sanitize_component(name):
    """
    Makes a filename FAT32 safe.
    """
    # 1. Transliterate Unicode to ASCII
    norm = unicodedata.normalize('NFKD', name)
    name = norm.encode('ASCII', 'ignore').decode('ASCII')
 
    # 2. Replace forbidden characters and spaces
    name = re.sub(r'[<>:"/\\|?*]', '-', name)
    name = re.sub(r'[\000-\037]', '', name)
    name = re.sub(r'\s+', '_', name)
 
    # 3. Trim leading/trailing dots and spaces
    name = name.strip('. ')
 
    # 4. Ensure not empty
    if not name:
        name = "_"
 
    # 5. Truncate to MAX_BYTES
    encoded = name.encode('utf-8')
    if len(encoded) > MAX_BYTES:
        name = encoded[:MAX_BYTES].decode('utf-8', 'ignore')
 
    return name
 
def main():
    src_path = Path(SOURCE_DIR)
    dst_path = Path(DEST_DIR)
 
    if not src_path.exists():
        print(f"Error: Source directory not found: {src_path}")
        return
 
    print(f"Source: {src_path}")
    print(f"Dest:   {dst_path}")
 
    # 1. Count total files for the progress bar
    print("Scanning source directory to count files...")
    total_files = sum(len(files) for _, _, files in os.walk(src_path))
    print(f"Total files found: {total_files}")
    print("-" * 40)
 
    # 2. Start the copy process
    with tqdm(total=total_files, unit='file') as pbar:
        for root, dirs, files in os.walk(src_path):
            rel_path = Path(root).relative_to(src_path)
 
            # Sanitize directory path
            safe_parts = [sanitize_component(p) for p in rel_path.parts if p != '.']
            current_dst_dir = dst_path.joinpath(*safe_parts)
 
            if not current_dst_dir.exists():
                try:
                    current_dst_dir.mkdir(parents=True, exist_ok=True)
                except OSError as e:
                    pbar.write(f"Error creating dir {rel_path}: {e}")
                    continue
 
            for filename in files:
                safe_filename = sanitize_component(filename)
                src_file = Path(root) / filename
                dst_file = current_dst_dir / safe_filename
 
                # Logic: Copy if dest missing OR src is newer
                should_copy = False
                if not dst_file.exists():
                    should_copy = True
                elif src_file.stat().st_mtime > dst_file.stat().st_mtime:
                    should_copy = True
 
                if should_copy:
                    try:
                        # Update description to show current file being copied
                        # (Only do this if using real tqdm to avoid clutter in fallback)
                        if HAS_TQDM:
                            pbar.set_description(f"Copying {safe_filename[:20]}...")
 
                        shutil.copy2(src_file, dst_file)
                    except Exception as e:
                        pbar.write(f"Failed to copy {filename}: {e}")
 
                # Advance progress bar
                pbar.update(1)
 
    print("Operation complete.")
 
if __name__ == "__main__":
    main()
 

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎