home/modules/downloads-sorter: extend

This commit is contained in:
2025-05-05 21:00:26 +02:00
parent 1b206b70e8
commit 9bad62502d

View File

@@ -10,7 +10,7 @@ in
options.services.downloads-sorter = { options.services.downloads-sorter = {
enable = lib.mkEnableOption "downloads sorter units, path activated units to keep the download dir clean"; enable = lib.mkEnableOption "downloads sorter units, path activated units to keep the download dir clean";
downloadsDir = lib.mkOption { downloadsDirectory = lib.mkOption {
type = lib.types.path; type = lib.types.path;
description = "Which directory to keep clean"; description = "Which directory to keep clean";
default = if config.xdg.userDirs.enable then config.xdg.userDirs.download else "${config.home.homeDirectory}/Downloads"; default = if config.xdg.userDirs.enable then config.xdg.userDirs.download else "${config.home.homeDirectory}/Downloads";
@@ -22,9 +22,57 @@ in
''; '';
}; };
# TODO: allow specifying a dynamic filter together with a system path trigger in an attrset.
mappings = lib.mkOption { mappings = lib.mkOption {
type = with lib.types; attrsOf (listOf str); type = let
mappingType = lib.types.submodule ({ name, ... }: {
options = {
unitName = lib.mkOption {
type = lib.types.str;
description = ''
The basename of the path/service unit responsible for this mapping
'';
default = "downloads-sorter@${name}";
example = "downloads-sorter@asdf";
};
dir = lib.mkOption {
type = lib.types.path;
description = ''
Absolute path to the directory where matching files should be moved.
'';
default = if builtins.substring 0 1 name == "/" then name else "${cfg.downloadsDirectory}/${name}";
defaultText = ''
if builtins.substring 0 1 name == "/" then name else "''${config.services.downloads-sorter.downloadsDirectory}/''${name}"
'';
};
globs = lib.mkOption {
type = with lib.types; listOf str;
description = ''
A list of globs that match the files that should be moved.
'';
example = [
"*.jpg"
"IMG_*_2020_*.png"
];
apply = map (g: "${cfg.downloadsDirectory}/${g}");
};
createDirIfNotExists = lib.mkOption {
type = lib.types.bool;
description = ''
Whether to create the target directory if it does not exist yet.
Turn this off if you'd like the target directory to be a symlink or similar.
'';
default = true;
example = false;
};
# TODO: allow specifying a dynamic filter together with a system path trigger in an attrset.
};
});
in with lib.types; attrsOf (coercedTo (listOf str) (globs: { inherit globs; }) mappingType);
description = '' description = ''
A mapping from a file pattern to the location where it should be moved. A mapping from a file pattern to the location where it should be moved.
@@ -37,22 +85,23 @@ in
"*.png" "*.png"
"*.jpg" "*.jpg"
]; ];
"documents" = [ "*.pdf" ]; "documents" = {
createDirIfNotExists = false;
globs = [ "*.pdf" ];
};
"/home/<user>/archives" = [ "*.rar" ]; "/home/<user>/archives" = [ "*.rar" ];
}; };
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
systemd.user.paths = lib.mapAttrs' (dir: globs: let systemd.user.paths = lib.mapAttrs' (dir: mapping: {
unitName = "downloads-sorter@${dir}"; name = mapping.unitName;
in {
name = unitName;
value = { value = {
Install.WantedBy = [ "paths.target" ]; Install.WantedBy = [ "paths.target" ];
Path = { Path = {
PathExistsGlob = map (g: "${cfg.downloadsDir}/${g}") globs; PathExistsGlob = mapping.globs;
Unit = "${unitName}.service"; Unit = "${mapping.unitName}.service";
TriggerLimitIntervalSec = "1s"; TriggerLimitIntervalSec = "1s";
TriggerLimitBurst = "1"; TriggerLimitBurst = "1";
}; };
@@ -60,29 +109,25 @@ in
}) cfg.mappings; }) cfg.mappings;
# TODO: deduplicate # TODO: deduplicate
systemd.user.services = lib.mapAttrs' (dir: globs: let systemd.user.services = lib.mapAttrs' (dir: mapping: {
unitName = "downloads-sorter@${dir}"; name = mapping.unitName;
in {
name = unitName;
value = { value = {
Unit.Description = "Downloads directory watchdog, sorts the downloads directory"; Unit.Description = "Downloads directory watchdog, sorts the downloads directory";
Service = { Service = {
Type = "oneshot"; Type = "oneshot";
SyslogIdentifier = unitName; SyslogIdentifier = mapping.unitName;
ExecStart = let ExecStart = let
absolutePath = if (builtins.substring 0 1 dir) == "/" then dir else "${cfg.downloadsDir}/${dir}";
script = pkgs.writeShellApplication { script = pkgs.writeShellApplication {
name = "downloads-sorter-${dir}.sh"; name = "downloads-sorter-${dir}.sh";
runtimeInputs = [ pkgs.coreutils ]; runtimeInputs = [ pkgs.coreutils ];
text = '' text = ''
shopt -s nullglob shopt -s nullglob
FILES=(${lib.concatMapStringsSep " " (g: "'${cfg.downloadsDir}'/${g}") globs}) FILES=(${builtins.concatStringsSep " " mapping.globs})
for file in "''${FILES[@]}"; do for file in "''${FILES[@]}"; do
echo "$file -> ${absolutePath}" echo "$file -> ${mapping.dir}"
mv "$file" '${absolutePath}' mv "$file" '${mapping.dir}'
done done
''; '';
}; };
@@ -100,7 +145,11 @@ in
}) cfg.mappings; }) cfg.mappings;
systemd.user.tmpfiles.settings."10-downloads-sorter-service" = let systemd.user.tmpfiles.settings."10-downloads-sorter-service" = let
absolutePaths = map (p: if (builtins.substring 0 1 p) == "/" then p else "${cfg.downloadsDir}/${p}") (builtins.attrNames cfg.mappings); absolutePaths = lib.pipe cfg.mappings [
builtins.attrValues
(builtins.filter (m: m.createDirIfNotExists))
(map (m: m.dir))
];
in lib.genAttrs absolutePaths (_: { in lib.genAttrs absolutePaths (_: {
d = { d = {
user = config.home.username; user = config.home.username;