Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

fscatalog

Fast file cataloging with fd, xxhash (XXH3_64), and DuckDB.

Scans directory trees at high speed using fd for file discovery, computes content hashes with xxhash, and stores everything in a DuckDB database for instant querying, deduplication, and change detection.

Requirements

  • Python ≥ 3.10
  • fd installed and on PATH
  • uv for project management

Installation

# install as library
uv pip install .

# install with CLI progress bars (tqdm)
uv pip install ".[cli]"

# or in a project
uv add fscatalog

CLI Usage

# Scan a directory (all files)
fscatalog scan /home/tim/Bilder

# Scan with pattern matching
fscatalog scan /home/tim/Bilder -p patterns/whatsapp-video.toml -p patterns/bsc-camera.toml

# Scan a whole directory of patterns
fscatalog scan /mnt/backup -p patterns/

# Quick metadata scan (skip hashing)
fscatalog scan /mnt/nas --no-hash

# Follow symlinks
fscatalog scan /home -L

# Custom database path
fscatalog scan /data -d /tmp/my_catalog.duckdb

# Show scan metadata
fscatalog info catalog.duckdb

# Find duplicate files
fscatalog dupes catalog.duckdb
fscatalog dupes catalog.duckdb --min-size 1048576  # only files ≥ 1 MiB

# Run raw SQL
fscatalog query "SELECT extension, count(*), sum(size_bytes) FROM files GROUP BY extension ORDER BY 3 DESC"

# Verbose logging
fscatalog -vv scan /home/tim

-vv enables debug logs with phase timings for pattern compilation, disk-info collection, file discovery, hashing, and database inserts. If installed with the cli extra, the scan command shows a spinner during fd discovery and a tqdm bar for file processing.

Library Usage

from fscatalog import CatalogDB, FilePattern, run_scan

# Define patterns in code
patterns = [
    FilePattern(
        name="whatsapp-video",
        description="WhatsApp videos",
        regex=r"VID-(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})-WA(?P<sequence>\d+)",
        extensions=(".mp4", ".3gp"),
    ),
]

# Run a scan
with CatalogDB("my_catalog.duckdb") as db:
    meta = run_scan("/home/tim/Bilder", db, patterns=patterns)
    print(f"Scanned {db.file_count(scan_id=meta.scan_id)} files")

    # Find duplicates
    for group in db.find_duplicates():
        print(f"Hash {group.xxhash}: {len(group.files)} copies, {group.size_bytes:,} bytes each")
        for f in group.files:
            print(f"  {f.absolute_path}")

    # Iterate with filters
    for entry in db.iter_files(extension=".mp4", pattern_name="whatsapp-video"):
        groups = entry.decoded_groups()
        print(f"{entry.filename} -> {groups}")

    # Raw SQL via DuckDB
    result = db.execute("""
        SELECT extension, count(*) as cnt, sum(size_bytes) as total
        FROM files
        GROUP BY extension
        ORDER BY total DESC
        LIMIT 10
    """)
    for row in result.fetchall():
        print(row)

Typical post-scan deduplication workflow:

from __future__ import annotations

from pathlib import Path

from fscatalog import CatalogDB, run_scan


def iter_duplicates_to_delete(
    db: CatalogDB,
    *,
    scan_id: str,
):
    """Yield (duplicate, keeper) for duplicate files.

    Strategy:
    - group by content hash (`find_duplicates`)
    - sort each group by oldest mtime first
    - keep the oldest file
    - delete the rest
    """
    for group in db.find_duplicates(scan_id=scan_id, min_size=1):
        # Oldest file wins. Add `absolute_path` as a stable tie-breaker.
        ordered = sorted(group.files, key=lambda f: (f.mtime_epoch, f.absolute_path))
        keeper = ordered[0]

        for duplicate in ordered[1:]:
            yield Path(duplicate.absolute_path), Path(keeper.absolute_path)


with CatalogDB("my_catalog.duckdb") as db:
    meta = run_scan("/srv/photos", db)

    for duplicate, keeper in iter_duplicates_to_delete(
        db,
        scan_id=meta.scan_id,
    ):
        print(f"KEEP {keeper}")
        print(f"DELETE {duplicate}")
        duplicate.unlink()

If you want a safer first pass, remove duplicate.unlink() and keep the print(...). If you prefer creation time sorting, replace mtime_epoch with ctime_epoch.

Pattern TOML Format

[pattern]
name = "whatsapp-video"
description = "WhatsApp videos"
regex = "VID-(?P<year>\\d{4})(?P<month>\\d{2})(?P<day>\\d{2})-WA(?P<sequence>\\d+)"
extensions = [".mp4", ".3gp"]

DuckDB Schema

scans — one row per scan run: scan_id, scan_epoch, root_path, follow_symlinks, disk_uuid, disk_model, disk_serial, disk_device, disk_label, disk_fstype, username, library_version, patterns_json

files — one row per catalogued file: scan_id, absolute_path, filename, extension, xxhash, size_bytes, mtime_epoch, ctime_epoch, is_symlink, pattern_name, pattern_groups

Indexes on xxhash, scan_id, and extension.

Metadata

Release files for fscatalog 0.2.0b1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fscatalog 0.2.0b1
File Size Uploaded
fscatalog-0.2.0b1.tar.gz 34.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fscatalog 0.2.0b1
File Interpreter ABI Platform
fscatalog-0.2.0b1-py3-none-any.whl Python 3 none any Details

Total release size: 53.0 kB

Release files / fscatalog-0.2.0b1.tar.gz

Download URL fscatalog-0.2.0b1.tar.gz
Size 34.0 kB
Tags Source
SHA-256 checksum
How to use checksums
b9b54d845dce82465ccc1487c6d21f8da00ca5c266e5390fac768bd3c7dd6fb2
BLAKE2b-256 checksum
How to use checksums
bdfbc72afa645c9dae024e553a5069c56af13228c17f3ee529b8f5245a900ac0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 7, 2026.

Transparency log

Release files / fscatalog-0.2.0b1-py3-none-any.whl

Download URL fscatalog-0.2.0b1-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b965000ab1143261d8b258d74bc9210d51f165ef6de4b22604a1be350c2efbf6
BLAKE2b-256 checksum
How to use checksums
8ffdf0a31e919bb02b6b41c5ad92ac1ed74d64a21976cb63317bc2aeaf7eda75
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0b1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page