Fast file cataloging with fd, xxhash and DuckDB
Project description
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
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fscatalog-0.2.0b1.tar.gz.
File metadata
- Download URL: fscatalog-0.2.0b1.tar.gz
- Upload date:
- Size: 34.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9b54d845dce82465ccc1487c6d21f8da00ca5c266e5390fac768bd3c7dd6fb2
|
|
| MD5 |
df21ec2ad351243909ee7375f37b8301
|
|
| BLAKE2b-256 |
bdfbc72afa645c9dae024e553a5069c56af13228c17f3ee529b8f5245a900ac0
|
Provenance
The following attestation bundles were made for fscatalog-0.2.0b1.tar.gz:
Publisher:
release.yml on TKaluza/fscatalog
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fscatalog-0.2.0b1.tar.gz -
Subject digest:
b9b54d845dce82465ccc1487c6d21f8da00ca5c266e5390fac768bd3c7dd6fb2 - Sigstore transparency entry: 1246025877
- Sigstore integration time:
-
Permalink:
TKaluza/fscatalog@2be9e1f7f0d3f11c61ea97e0a8a2b95ad030530c -
Branch / Tag:
refs/tags/0.2.0b1 - Owner: https://github.com/TKaluza
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2be9e1f7f0d3f11c61ea97e0a8a2b95ad030530c -
Trigger Event:
release
-
Statement type:
File details
Details for the file fscatalog-0.2.0b1-py3-none-any.whl.
File metadata
- Download URL: fscatalog-0.2.0b1-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b965000ab1143261d8b258d74bc9210d51f165ef6de4b22604a1be350c2efbf6
|
|
| MD5 |
4de5b416096f3a3d4cacdf1c95273cf9
|
|
| BLAKE2b-256 |
8ffdf0a31e919bb02b6b41c5ad92ac1ed74d64a21976cb63317bc2aeaf7eda75
|
Provenance
The following attestation bundles were made for fscatalog-0.2.0b1-py3-none-any.whl:
Publisher:
release.yml on TKaluza/fscatalog
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fscatalog-0.2.0b1-py3-none-any.whl -
Subject digest:
b965000ab1143261d8b258d74bc9210d51f165ef6de4b22604a1be350c2efbf6 - Sigstore transparency entry: 1246025921
- Sigstore integration time:
-
Permalink:
TKaluza/fscatalog@2be9e1f7f0d3f11c61ea97e0a8a2b95ad030530c -
Branch / Tag:
refs/tags/0.2.0b1 - Owner: https://github.com/TKaluza
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2be9e1f7f0d3f11c61ea97e0a8a2b95ad030530c -
Trigger Event:
release
-
Statement type: