Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fileorg

A small, safe, data-driven utility that keeps messy directories tidy. You describe what goes where in a YAML file; fileorg computes a plan and (only when you let it) carries it out. It does three things: organizes files into folders by extension, name, age, or size; finds duplicate files by content and quarantines the redundant copies; and takes incremental backups into timestamped snapshots. It is built to run unattended on a schedule with a full audit trail, and to be boring and predictable: it never overwrites, never deletes, and shows you exactly what it will do before it does anything. (The one exception: backup prunes its own old snapshots — never your originals.)

$ fileorg organize --config ~/.fileorg/config.yaml --dry-run
2026-06-15 21:31:29 INFO === fileorg run: mode=organize dry_run=True ===
2026-06-15 21:31:29 INFO organize: 2 move(s) planned.
2026-06-15 21:31:29 INFO DRY-RUN move ~/Downloads/book.pdf  -> ~/Documents/Books/book.pdf (ext=pdf)
2026-06-15 21:31:29 INFO DRY-RUN move ~/Downloads/photo.jpg -> ~/Pictures/Downloads/photo.jpg (ext=jpg/png)

Quickstart

Requires Python 3.10+.

# 1. Install (editable, so the `fileorg` command tracks your checkout)
pip install -e .

# 2. Copy the example config and edit it for your machine
mkdir -p ~/.fileorg
cp config.example.yaml ~/.fileorg/config.yaml

# 3. Always look before you leap — dry-run prints the plan, touches nothing
fileorg organize --config ~/.fileorg/config.yaml --dry-run

# 4. Happy with the plan? Run it for real
fileorg organize --config ~/.fileorg/config.yaml

--config defaults to ~/.fileorg/config.yaml, so once it's in place you can just run fileorg organize.

Configuration

Behavior lives entirely in the config file — changing rules never means editing Python. See config.example.yaml for a complete, commented example.

organize:
  - source: ~/Downloads
    rules:
      - match: { extension: [pdf, epub] }
        dest: ~/Documents/Books
      - match: { name_pattern: "invoice_*" }
        dest: ~/Documents/Invoices
      - match: { older_than_days: 90 }
        dest: ~/Downloads/Archive

dedup:
  - path: ~/Pictures
    action: flag # flag | report-only (default: report-only)

backup:
  - source: ~/Documents/Projects
    dest: ~/Backups/Projects
    keep_snapshots: 7 # default: 7

logging:
  file: ~/.fileorg/fileorg.log
  level: INFO
  max_bytes: 1048576
  backup_count: 5

Rule semantics

A rule fires when every key in its match block matches (logical AND).

Match key Matches when…
extension file extension is in the list (case-insensitive)
name_pattern filename matches the glob (e.g. invoice_*)
older_than_days file's mtime is at least N days ago
min_size_mb file is at least this many MB
max_size_mb file is at most this many MB
  • Rules are evaluated in order; the first match wins. Put your most specific rules first — this order-dependence is the one footgun to keep in mind.
  • dest directories are created automatically if missing.
  • Only the top level of each source is scanned; subfolders are left alone (which is also what keeps repeat runs idempotent).
  • ~ and $VARS are expanded in every path.

Invalid config fails fast with a located message, e.g. organize[0].rules[2]: unknown match key 'flavour'.

Editing & checking the config

Since all behavior lives in the config file, fileorg ships small helpers for working with it. None of them rewrite the YAML — your comments and key order are left intact:

fileorg config edit       # open in $VISUAL/$EDITOR, then re-validate on save
fileorg config validate   # load + validate; print a per-section summary (or the error)
fileorg config path       # print the resolved config path (~ and $VARS expanded)

config edit is the easiest way to change a destination or add a rule: it's a plain text edit that re-checks the file the moment you save, so a typo surfaces right then instead of at the next scheduled run. config validate does the same check on demand (exit 0 if valid, 1 if not) — handy in a pre-flight script.

Deduplication

fileorg dedup walks each dedup path recursively and groups files by content (SHA-256, with a size + first/last-chunk pre-filter so large sets stay cheap). Within each group the oldest file is kept as the canonical original.

fileorg dedup --config ~/.fileorg/config.yaml --dry-run   # look first
fileorg dedup --config ~/.fileorg/config.yaml             # then act
  • action: report-only (default) just logs the duplicate groups.
  • action: flag moves the redundant copies into a _duplicates/ folder at the root of that path (preserving subpaths, never overwriting). The original stays put, and nothing is ever deleted. The _duplicates/ folder is skipped on later runs, so dedup is idempotent.

Backup

fileorg backup walks each backup source recursively and writes a timestamped snapshot under its dest — a full, self-contained mirror plus a .fileorg-manifest.json of per-file size/mtime/sha256.

fileorg backup --config ~/.fileorg/config.yaml --dry-run   # look first
fileorg backup --config ~/.fileorg/config.yaml             # then snapshot
  • Incremental reads. A file is re-read from the source only when it's new or its mtime/size changed; unchanged files are carried forward from the previous snapshot (hash reused), so nothing on the source is re-read needlessly.
  • Idempotent. If nothing changed, no new snapshot is written.
  • Pruned. Snapshots beyond keep_snapshots (default 7) are removed. This is the only deletion fileorg performs, and it only ever touches its own snapshot folders — never your source files.

Trade-off: each snapshot is a full copy rather than a hardlink tree, so an unchanged file uses disk in every snapshot it survives into. We chose this for v1 because each snapshot stands alone — pruning one can never corrupt another.

Safety

This tool moves your files unattended, so it is conservative by design:

  • Dry-run everywhere. --dry-run computes and logs the full plan without touching a single file. It's the recommended first step every time.
  • Never overwrites. A move into an occupied name becomes name (1).ext, name (2).ext, … — the existing file is never clobbered.
  • Never deletes your files. Organize moves, dedup at most quarantines, and backup only reads from sources. The sole removal is backup pruning its own snapshots beyond keep_snapshots — nothing under a source is ever deleted.
  • Idempotent. Running twice in a row is safe: the second run sees nothing to do (and backup writes no duplicate snapshot).
  • Observable. Every action is written to a rotating log file.

Scheduling

fileorg is a plain CLI, so let the OS schedule it — more robust than a long-running Python daemon. Point the scheduler at a single fileorg run --once, which performs organize → dedup → backup in one pass with one aggregated exit code.

Ready-to-copy templates live in scheduling/, one per platform, with a step-by-step guide in scheduling/README.md:

Each template runs run --once daily. To run organize / dedup / backup on separate cadences, copy a template and swap the command — see the "Different cadences" note in the guide.

Development

pip install -e ".[dev]"   # installs pytest
pytest                    # run the suite

Tests use tmp_path fixtures and never touch real directories. The focus is the engine — rule matching, collision handling, dry-run, and idempotency — rather than glue code.

Architecture

The backbone of the whole tool is a plan → execute split:

  1. Plan — a pure function inspects the filesystem and returns a list of intended operations (Move(src, dest, rule)), touching nothing.
  2. Execute — the only side-effecting half carries the plan out (unless --dry-run), logging each step through a single safe_move helper.

This one decision is what makes dry-run honest, idempotency automatic, and the logic trivial to unit-test.

fileorg/
├── config.py      # YAML -> typed dataclasses, strict fail-fast validation
├── organizer.py   # rule matching (plan) + moves (execute)
├── dedup.py       # content hashing (plan) + flag/report (execute)
├── backup.py      # snapshot diffing (plan) + copy/prune (execute)
├── utils.py       # safe_move / safe_copy — never overwrite
├── logsetup.py    # rotating logger with a per-run header line
└── cli.py         # argument parsing + command dispatch

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages