190 lines
5.8 KiB
Nix
190 lines
5.8 KiB
Nix
{
|
|
den.aspects.janitor-backend = {
|
|
nixos = { pkgs, lib, config, ... }:
|
|
let
|
|
cfg = config.services.janitor;
|
|
|
|
janitorScript = pkgs.writeText "janitor.py" ''
|
|
import os
|
|
import sys
|
|
import json
|
|
import shutil
|
|
import time
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="[janitor] %(message)s",
|
|
stream=sys.stdout,
|
|
)
|
|
log = logging.getLogger("janitor")
|
|
|
|
CONFIG_PATH = os.environ.get("JANITOR_CONFIG")
|
|
|
|
|
|
def load_config():
|
|
if not CONFIG_PATH or not os.path.exists(CONFIG_PATH):
|
|
raise FileNotFoundError(f"JANITOR_CONFIG not set or missing: {CONFIG_PATH}")
|
|
with open(CONFIG_PATH) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def get_destination(extension, rules):
|
|
ext = extension.lower().lstrip(".")
|
|
for folder, extensions in rules.items():
|
|
if ext in extensions:
|
|
return folder
|
|
return None
|
|
|
|
|
|
def resolve_dest(dest_key, watch_dir):
|
|
if os.path.isabs(dest_key):
|
|
return Path(dest_key)
|
|
return watch_dir.parent / dest_key
|
|
|
|
|
|
def unique_path(target):
|
|
if not target.exists():
|
|
return target
|
|
stem, suffix = target.stem, target.suffix
|
|
counter = 1
|
|
while True:
|
|
candidate = target.parent / f"{stem}_{counter}{suffix}"
|
|
if not candidate.exists():
|
|
return candidate
|
|
counter += 1
|
|
|
|
|
|
def main():
|
|
try:
|
|
config = load_config()
|
|
except Exception as e:
|
|
log.error("Failed to load config: %s", e)
|
|
sys.exit(1)
|
|
|
|
grace_period = config.get("grace_period", 60)
|
|
rules = config.get("rules", {})
|
|
now = time.time()
|
|
|
|
for watch_dir_str in config.get("watched_dirs", []):
|
|
watch_dir = Path(os.path.expanduser(watch_dir_str))
|
|
|
|
if not watch_dir.exists():
|
|
log.warning("Watched dir does not exist: %s", watch_dir)
|
|
continue
|
|
|
|
for item in watch_dir.iterdir():
|
|
if not item.is_file() or item.name.startswith("."):
|
|
continue
|
|
|
|
age = now - item.stat().st_mtime
|
|
if age < grace_period:
|
|
continue
|
|
|
|
dest_key = get_destination(item.suffix, rules)
|
|
if dest_key is None:
|
|
continue
|
|
|
|
target_dir = resolve_dest(dest_key, watch_dir)
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
target = unique_path(target_dir / item.name)
|
|
|
|
try:
|
|
shutil.move(str(item), str(target))
|
|
log.info("Moved %s -> %s", item.name, target)
|
|
except Exception as e:
|
|
log.error("Failed to move %s: %s", item.name, e)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
'';
|
|
|
|
janitorConfig = pkgs.writeText "janitor_config.json" (builtins.toJSON {
|
|
grace_period = cfg.gracePeriod;
|
|
watched_dirs = cfg.watchedDirs;
|
|
rules = cfg.rules;
|
|
});
|
|
|
|
in {
|
|
options.services.janitor = {
|
|
enable = lib.mkEnableOption "file sorting janitor";
|
|
|
|
interval = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "5min";
|
|
description = "How often to run the janitor (systemd time span, e.g. \"5min\", \"1h\").";
|
|
};
|
|
|
|
gracePeriod = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 60;
|
|
description = "Seconds a file must remain unmodified before it is eligible to be moved.";
|
|
};
|
|
|
|
watchedDirs = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ "~/Downloads" ];
|
|
description = "Directories to scan and sort. Supports ~ expansion.";
|
|
};
|
|
|
|
rules = lib.mkOption {
|
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
default = { };
|
|
description = ''
|
|
Mapping of destination folder path to a list of file extensions (without leading dot).
|
|
Destinations are relative to the parent of the watched directory, or absolute if they
|
|
start with /.
|
|
|
|
Example:
|
|
rules = {
|
|
"Pictures/Downloads" = [ "jpg" "png" "gif" "webp" ];
|
|
"Videos/Downloads" = [ "mp4" "mkv" "webm" ];
|
|
"/mnt/archive" = [ "zip" "tar" ];
|
|
};
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
"Pictures/Downloads" = [ "jpg" "jpeg" "png" "gif" "webp" "avif" ];
|
|
"Videos/Downloads" = [ "mp4" "mkv" "mov" "webm" "avi" ];
|
|
"Music/Downloads" = [ "mp3" "flac" "wav" "ogg" "opus" "m4a" ];
|
|
"Documents/Downloads" = [ "pdf" "doc" "docx" "odt" "txt" "md" "epub" ];
|
|
}
|
|
'';
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "bug";
|
|
description = "User account the janitor service runs as.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
systemd.services.janitor = {
|
|
description = "File sorting janitor";
|
|
|
|
environment.JANITOR_CONFIG = "${janitorConfig}";
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
ExecStart = "${pkgs.python3}/bin/python3 ${janitorScript}";
|
|
};
|
|
};
|
|
|
|
systemd.timers.janitor = {
|
|
wantedBy = [ "timers.target" ];
|
|
|
|
timerConfig = {
|
|
OnBootSec = "10m";
|
|
OnUnitActiveSec = cfg.interval;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|