diff --git a/doc/manual/paths.md b/doc/manual/paths.md new file mode 100644 index 00000000..0c4d88cb --- /dev/null +++ b/doc/manual/paths.md @@ -0,0 +1,60 @@ +# Resolving paths + +Commands that return a path have `.realpath` variants that resolve it to a +canonical one, with symlinks followed and any `.` or `..` components removed. + +The intent is to make paths safer to hand to an external script. +A script called through `execute` receives whatever path rtorrent gives it, and +many scripts do no sanity checking of their own, so resolving the path before it +leaves rtorrent removes a class of surprises: a download directory that is a +symlink into somewhere unexpected, or a torrent whose name walks upwards out of +the directory it is supposed to live in. + + # Instead of this + execute = ~/bin/on-finished, (d.base_path) + + # Pass the resolved path + execute = ~/bin/on-finished, (d.base_path.realpath.or_throw) + +## Available variants + +Each command below comes in an `.or_empty` and an `.or_throw` form. + +| Command | Resolves | +| --- | --- | +| `d.base_path.realpath.*` | `d.base_path` | +| `d.directory.realpath.*` | `d.directory` | +| `d.tied_to_file.realpath.*` | `d.tied_to_file` | +| `d.loaded_file.realpath.*` | `d.loaded_file` | +| `f.frozen_path.realpath.*` | `f.frozen_path` | +| `session.path.realpath.*` | `session.path` | +| `directory.default.realpath.*` | `directory.default` | + +A leading `~` is expanded first, exactly as it is for `execute`, so +`~/downloads` resolves the same way it would on the command line. + +## Paths that do not exist + +Resolving requires the path to name an existing file or directory, which is +often not the case for a download whose data has not been written yet. +The two forms differ only in what they do about it. + +`.or_empty` returns an empty string: + + print = (d.base_path.realpath.or_empty) # "" + +This keeps `d.multicall` usable over a view that mixes started and unstarted +downloads, since a single unresolvable path does not abort the whole call. +A script receiving one of these paths should still check that it is not empty +before acting on it. + +`.or_throw` raises an error instead: + + print = (d.base_path.realpath.or_throw) # Could not resolve path: '...' + +Prefer this one wherever an unresolvable path means the command should not run +at all, such as a single `execute` on `event.download.finished`. + +Note that a download only has a file list once it has been opened, so +`d.base_path` and `f.frozen_path` are empty at `event.download.inserted` time +and their `.realpath` variants resolve nothing.