tmpfiles: reject extra argument fields and make 'r'/'R' support age field (#43195)

This updates systemd-tmpfiles in two small areas:

- Reject non-empty argument fields for tmpfiles.d line types that do not
consume the argument field, instead of warning and silently ignoring
them.
- Let `r` and `R` tmpfiles.d entries honor the `Age` field when
`systemd-tmpfiles --clean` is used. The existing `--remove` behavior
remains unconditional.

The completed TODO entries are removed, and NEWS/man page documentation
is updated for the visible behavior changes.
This commit is contained in:
Yu Watanabe
2026-08-01 01:34:08 +09:00
committed by GitHub
6 changed files with 640 additions and 31 deletions
+11
View File
@@ -79,6 +79,17 @@ CHANGES WITH 262:
* bootctl, systemd-oomd, systemd-repart, systemd-sysusers, and
systemd-tmpfiles now accept -n as a short option for --dry-run.
Changes in systemd-tmpfiles:
* systemd-tmpfiles now rejects non-empty argument fields for tmpfiles.d
line types that do not use the argument field. This is a compatibility
change: fields that were previously ignored after a warning will now
cause the tmpfiles.d configuration to be rejected, so downstreams
should audit shipped tmpfiles.d snippets before upgrading.
* The r and R tmpfiles.d line types now honor the age field when
systemd-tmpfiles is invoked with --clean.
Changes in the TPM Subsystem:
* The way that NvPCRs are anchored has been improved. They are now
-2
View File
@@ -2729,8 +2729,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
DHCP/HTTP base EFI boot.
- **tmpfiles:**
- allow time-based cleanup in r and R too
- instead of ignoring unknown fields, reject them.
- creating new directories/subvolumes/fifos/device nodes
should not follow symlinks. None of the other adjustment or creation
calls follow symlinks.
+14 -8
View File
@@ -164,7 +164,9 @@ L /tmp/foobar - - - - /dev/null</programlisting>
<para>Fields may contain C-style escapes. With the exception of the seventh field (the "argument") all
fields may be enclosed in quotes. Note that any whitespace found in the line after the beginning of the
argument field will be considered part of the argument field. To begin the argument field with a
whitespace character, use C-style escapes (e.g. <literal>\x20</literal>).</para>
whitespace character, use C-style escapes (e.g. <literal>\x20</literal>). Line types which do not
document use of the argument field reject a non-empty argument field; use <literal>-</literal> to leave
the argument field empty.</para>
<refsect2>
<title>Type</title>
@@ -380,7 +382,8 @@ L /tmp/foobar - - - - /dev/null</programlisting>
This may not be used to remove non-empty directories, use
<varname>R</varname> for that. Lines of this type accept
shell-style globs in place of normal path
names. Does not follow symlinks.</para></listitem>
names. Does not follow symlinks. When <option>--clean</option> is used, the file or directory is subject
to time-based cleanup if the age argument is specified. This age handling was added in v262.</para></listitem>
</varlistentry>
<varlistentry>
@@ -388,7 +391,8 @@ L /tmp/foobar - - - - /dev/null</programlisting>
<listitem><para>Recursively remove a path and all its
subdirectories (if it is a directory). Lines of this type
accept shell-style globs in place of normal path
names. Does not follow symlinks.</para></listitem>
names. Does not follow symlinks. When <option>--clean</option> is used, the path is subject to
time-based cleanup if the age argument is specified. This age handling was added in v262.</para></listitem>
</varlistentry>
<varlistentry>
@@ -674,13 +678,14 @@ w- /proc/sys/vm/swappiness - - - - 10</programlisting></para>
<para>The age field only applies to lines starting with
<varname>d</varname>, <varname>D</varname>, <varname>e</varname>,
<varname>v</varname>, <varname>q</varname>,
<varname>Q</varname>, <varname>C</varname>, <varname>x</varname>
and <varname>X</varname>. If omitted or set to
<literal>-</literal>, no automatic clean-up is done.</para>
<varname>Q</varname>, <varname>C</varname>, <varname>r</varname>,
<varname>R</varname>, <varname>x</varname> and <varname>X</varname>.
If omitted or set to <literal>-</literal>, no automatic clean-up is done.</para>
<para>If the age field starts with a tilde character <literal>~</literal>, clean-up is only applied to
files and directories one level inside the directory specified, but not the files and directories
immediately inside it.</para>
immediately inside it. For <varname>R</varname> lines, this also skips removing the specified
top-level directory itself. This modifier is not supported by <varname>r</varname> lines.</para>
<para>The age of a file system entry is determined from its last
modification timestamp (mtime), its last access timestamp (atime),
@@ -743,7 +748,8 @@ d /tmp/foo/bar - - - bmA:1h -</programlisting></para>
and <varname>T</varname>, determines extended attributes to be set. For <varname>a</varname> and
<varname>A</varname>, determines ACL attributes to be set. For <varname>h</varname> and <varname>H</varname>,
determines the file attributes to set. For <varname>k</varname> and <varname>K</varname>, determines
file capabilities to be set. Ignored for all other lines.</para>
file capabilities to be set. For line types that do not document use of this field, a non-empty
argument is rejected; use <literal>-</literal> to leave it unset.</para>
<para>This field can contain specifiers, see below.</para>
</refsect2>
+459 -20
View File
@@ -3390,6 +3390,352 @@ static int remove_item_instance(
}
}
static bool item_instance_needs_cleanup(
Item *i,
const char *instance,
const struct statx *sx) {
assert(i);
assert(instance);
assert(sx);
if (!i->age_set)
return false;
usec_t n = now(CLOCK_REALTIME);
if (n < i->age)
return false;
usec_t cutoff = n - i->age;
nsec_t atime_nsec = FLAGS_SET(sx->stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx->stx_atime) : NSEC_INFINITY;
nsec_t mtime_nsec = FLAGS_SET(sx->stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx->stx_mtime) : NSEC_INFINITY;
nsec_t ctime_nsec = FLAGS_SET(sx->stx_mask, STATX_CTIME) ? statx_timestamp_load_nsec(&sx->stx_ctime) : NSEC_INFINITY;
nsec_t btime_nsec = FLAGS_SET(sx->stx_mask, STATX_BTIME) ? statx_timestamp_load_nsec(&sx->stx_btime) : NSEC_INFINITY;
bool is_dir = S_ISDIR(sx->stx_mode);
return needs_cleanup(atime_nsec, btime_nsec, ctime_nsec, mtime_nsec,
cutoff * NSEC_PER_USEC, instance,
is_dir ? i->age_by_dir : i->age_by_file, is_dir);
}
static int item_instance_open_parent(
Item *i,
const char *instance,
char **ret_name,
bool *ret_must_be_directory) {
int r;
assert(i);
assert(instance);
assert(ret_name);
assert(ret_must_be_directory);
_cleanup_free_ char *name = NULL;
r = path_extract_filename(instance, &name);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", instance);
_cleanup_close_ int parent_fd = path_open_parent_safe(instance, i->allow_failure);
if (parent_fd < 0)
return parent_fd;
*ret_name = TAKE_PTR(name);
*ret_must_be_directory = r == O_DIRECTORY;
return TAKE_FD(parent_fd);
}
static int item_instance_open_opath(
int parent_fd,
const char *name,
const char *instance,
int *ret_fd,
struct statx *ret_sx) {
int r;
assert(parent_fd >= 0);
assert(name);
assert(instance);
assert(ret_fd);
assert(ret_sx);
_cleanup_close_ int fd = RET_NERRNO(openat(parent_fd, name, O_PATH|O_CLOEXEC|O_NOFOLLOW));
if (IN_SET(fd, -ENOENT, -ENOTDIR))
return 0;
if (fd < 0)
return log_error_errno(fd, "Failed to open \"%s\": %m", instance);
struct statx sx;
r = xstatx_full(fd,
/* path= */ NULL,
AT_EMPTY_PATH,
/* xstatx_flags= */ 0,
STATX_TYPE|STATX_MODE,
STATX_ATIME|STATX_MTIME|STATX_CTIME|STATX_BTIME,
/* mandatory_attributes= */ 0,
&sx);
if (r < 0)
return log_error_errno(r, "statx(%s) failed: %m", instance);
*ret_fd = TAKE_FD(fd);
*ret_sx = sx;
return 1;
}
static int item_instance_still_current(
int fd,
int parent_fd,
const char *name,
const char *instance) {
int r;
assert(fd >= 0);
assert(parent_fd >= 0);
assert(name);
assert(instance);
r = inode_same_at(fd, /* filea= */ NULL,
parent_fd, name,
AT_EMPTY_PATH|AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT);
if (IN_SET(r, -ENOENT, -ENOTDIR))
return 0;
if (r < 0) {
log_debug_errno(r, "Failed to verify that \"%s\" still refers to the same inode, skipping: %m", instance);
return 0;
}
if (r == 0) {
log_debug("Skipping \"%s\": inode changed.", instance);
return 0;
}
return 1;
}
static int item_instance_fd_still_current(
int fd,
int other_fd,
const char *instance) {
int r;
assert(fd >= 0);
assert(other_fd >= 0);
assert(instance);
r = fd_inode_same(fd, other_fd);
if (r < 0) {
log_debug_errno(r, "Failed to verify that \"%s\" still refers to the same inode, skipping: %m", instance);
return 0;
}
if (r == 0) {
log_debug("Skipping \"%s\": inode changed.", instance);
return 0;
}
return 1;
}
static int item_instance_open_directory_and_lock(
int parent_fd,
const char *name,
int fd,
const char *instance,
DIR **ret) {
_cleanup_closedir_ DIR *d = NULL;
int r;
assert(parent_fd >= 0);
assert(name);
assert(fd >= 0);
assert(instance);
assert(ret);
d = xopendirat_nomod(parent_fd, name);
if (!d) {
if (IN_SET(errno, ENOENT, ENOTDIR, ELOOP))
return 0;
return log_error_errno(errno, "Failed to open directory \"%s\": %m", instance);
}
r = item_instance_fd_still_current(fd, dirfd(d), instance);
if (r <= 0)
return r;
if (!arg_dry_run &&
flock(dirfd(d), LOCK_EX|LOCK_NB) < 0) {
log_debug_errno(errno, "Couldn't acquire shared BSD lock on directory \"%s\", skipping: %m", instance);
return 0;
}
*ret = TAKE_PTR(d);
return 1;
}
static int item_instance_remove_empty_directory(
int parent_fd,
const char *name,
const char *instance) {
int r;
assert(parent_fd >= 0);
assert(name);
assert(instance);
if (arg_dry_run)
return 0;
r = RET_NERRNO(unlinkat(parent_fd, name, AT_REMOVEDIR));
if (r < 0) {
bool fatal = !IN_SET(r, -ENOENT, -ENOTEMPTY, -EBUSY);
log_full_errno(fatal ? LOG_ERR : LOG_DEBUG, r, "Failed to remove %s: %m", instance);
if (fatal)
return r;
}
return 0;
}
static int item_instance_remove_directory_if_empty(
int parent_fd,
const char *name,
DIR *d,
const char *instance,
bool assume_empty) {
int r;
assert(parent_fd >= 0);
assert(name);
assert(d);
assert(instance);
if (!assume_empty) {
r = dir_is_empty_at(dirfd(d), /* path= */ NULL, /* ignore_hidden_or_backup= */ false);
if (IN_SET(r, -ENOENT, -ENOTDIR))
return 0;
if (r < 0)
return log_error_errno(r, "Failed to determine whether \"%s\" is empty: %m", instance);
if (r == 0)
return 0;
}
r = item_instance_still_current(dirfd(d), parent_fd, name, instance);
if (r <= 0)
return r;
log_action("Would remove", "Removing", "%s directory \"%s\".", instance);
r = item_instance_remove_empty_directory(parent_fd, name, instance);
if (r < 0)
return r;
return 0;
}
static int clean_remove_item_instance_at(
Item *i,
int parent_fd,
const char *name,
int fd,
const char *instance,
const struct statx *sx,
bool must_be_directory) {
int r;
assert(i);
assert(parent_fd >= 0);
assert(name);
assert(fd >= 0);
assert(instance);
assert(sx);
if (must_be_directory && !S_ISDIR(sx->stx_mode))
return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "\"%s\" is not a directory.", instance);
if (!item_instance_needs_cleanup(i, instance, sx))
return 0;
if (S_ISDIR(sx->stx_mode)) {
_cleanup_closedir_ DIR *d = NULL;
r = item_instance_open_directory_and_lock(parent_fd, name, fd, instance, &d);
if (r <= 0)
return r;
return item_instance_remove_directory_if_empty(parent_fd, name, d, instance, /* assume_empty= */ false);
}
_cleanup_close_ int lock_fd = -EBADF;
if (!arg_dry_run) {
lock_fd = xopenat(parent_fd, name, O_RDONLY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME|O_NONBLOCK|O_NOCTTY);
if (lock_fd < 0 && !IN_SET(lock_fd, -ENOENT, -ELOOP))
log_warning_errno(lock_fd, "Opening file \"%s\" failed, proceeding without lock: %m", instance);
if (lock_fd >= 0) {
r = item_instance_fd_still_current(fd, lock_fd, instance);
if (r <= 0)
return r;
if (flock(lock_fd, LOCK_EX|LOCK_NB) < 0 && errno == EAGAIN) {
log_debug_errno(errno, "Couldn't acquire shared BSD lock on file \"%s\", skipping: %m", instance);
return 0;
}
}
r = item_instance_still_current(fd, parent_fd, name, instance);
if (r <= 0)
return r;
}
if (i->type == RECURSIVE_REMOVE_PATH)
log_action("Would remove", "Removing", "%s file \"%s\".", instance);
else
log_action("Would remove", "Removing", "%s \"%s\".", instance);
if (!arg_dry_run &&
unlinkat(parent_fd, name, 0) < 0 &&
errno != ENOENT)
log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", instance);
return 0;
}
static int clean_remove_item_instance(
Context *c,
Item *i,
const char *instance,
CreationMode creation) {
int r;
assert(c);
assert(i);
assert(instance);
if (!i->age_set)
return 0;
_cleanup_free_ char *name = NULL;
bool must_be_directory;
_cleanup_close_ int parent_fd = item_instance_open_parent(i, instance, &name, &must_be_directory);
if (parent_fd < 0)
return parent_fd;
_cleanup_close_ int fd = -EBADF;
struct statx sx;
r = item_instance_open_opath(parent_fd, name, instance, &fd, &sx);
if (r <= 0)
return r;
return clean_remove_item_instance_at(i, parent_fd, name, fd, instance, &sx, must_be_directory);
}
static int remove_item(Context *c, Item *i) {
assert(c);
assert(i);
@@ -3427,13 +3773,19 @@ static char *age_by_to_string(AgeBy ab, bool is_dir) {
return ret;
}
static int clean_item_instance(
static int clean_item_instance_from_dir(
Context *c,
Item *i,
const char* instance,
CreationMode creation) {
const char *instance,
DIR *d,
const struct statx *sx,
bool mountpoint) {
assert(c);
assert(i);
assert(instance);
assert(d);
assert(sx);
if (!i->age_set)
return 0;
@@ -3443,19 +3795,8 @@ static int clean_item_instance(
return 0;
usec_t cutoff = n - i->age;
nsec_t atime_nsec, mtime_nsec;
_cleanup_closedir_ DIR *d = NULL;
struct statx sx;
bool mountpoint;
int r;
r = opendir_and_stat(instance, &d, &sx, &mountpoint);
if (r <= 0)
return r;
atime_nsec = FLAGS_SET(sx.stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx.stx_atime) : NSEC_INFINITY;
mtime_nsec = FLAGS_SET(sx.stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx.stx_mtime) : NSEC_INFINITY;
nsec_t atime_nsec = FLAGS_SET(sx->stx_mask, STATX_ATIME) ? statx_timestamp_load_nsec(&sx->stx_atime) : NSEC_INFINITY;
nsec_t mtime_nsec = FLAGS_SET(sx->stx_mask, STATX_MTIME) ? statx_timestamp_load_nsec(&sx->stx_mtime) : NSEC_INFINITY;
if (DEBUG_LOGGING) {
_cleanup_free_ char *ab_f = NULL, *ab_d = NULL;
@@ -3479,12 +3820,96 @@ static int clean_item_instance(
atime_nsec,
mtime_nsec,
cutoff * NSEC_PER_USEC,
sx.stx_dev_major, sx.stx_dev_minor,
sx->stx_dev_major, sx->stx_dev_minor,
mountpoint,
MAX_DEPTH, i->keep_first_level,
i->age_by_file, i->age_by_dir);
}
static int clean_item_instance(
Context *c,
Item *i,
const char *instance,
CreationMode creation) {
int r;
assert(c);
assert(i);
assert(instance);
_cleanup_closedir_ DIR *d = NULL;
struct statx sx;
bool mountpoint;
r = opendir_and_stat(instance, &d, &sx, &mountpoint);
if (r <= 0)
return r;
return clean_item_instance_from_dir(c, i, instance, d, &sx, mountpoint);
}
static int clean_recursive_remove_item_instance(
Context *c,
Item *i,
const char *instance,
CreationMode creation) {
int r;
assert(c);
assert(i);
assert(instance);
if (!i->age_set)
return 0;
_cleanup_free_ char *name = NULL;
bool must_be_directory;
_cleanup_close_ int parent_fd = item_instance_open_parent(i, instance, &name, &must_be_directory);
if (parent_fd < 0)
return parent_fd;
_cleanup_close_ int fd = -EBADF;
struct statx sx;
r = item_instance_open_opath(parent_fd, name, instance, &fd, &sx);
if (r <= 0)
return r;
if (!S_ISDIR(sx.stx_mode))
return clean_remove_item_instance_at(i, parent_fd, name, fd, instance, &sx, must_be_directory);
_cleanup_closedir_ DIR *d = NULL;
r = item_instance_open_directory_and_lock(parent_fd, name, fd, instance, &d);
if (r <= 0)
return r;
struct statx dir_sx;
r = xstatx_full(dirfd(d),
/* path= */ NULL,
AT_EMPTY_PATH,
/* xstatx_flags= */ 0,
STATX_TYPE|STATX_MODE|STATX_INO,
STATX_ATIME|STATX_MTIME|STATX_CTIME|STATX_BTIME,
STATX_ATTR_MOUNT_ROOT,
&dir_sx);
if (r < 0)
return log_error_errno(r, "statx(%s) failed: %m", instance);
bool cleanup_needed = item_instance_needs_cleanup(i, instance, &dir_sx);
r = clean_item_instance_from_dir(c, i, instance, d, &dir_sx, FLAGS_SET(dir_sx.stx_attributes, STATX_ATTR_MOUNT_ROOT));
if (r < 0)
return r;
if (i->keep_first_level)
return 0;
if (!cleanup_needed)
return 0;
return item_instance_remove_directory_if_empty(parent_fd, name, d, instance, /* assume_empty= */ arg_dry_run);
}
static int clean_item(Context *c, Item *i) {
assert(c);
assert(i);
@@ -3506,6 +3931,12 @@ static int clean_item(Context *c, Item *i) {
case IGNORE_DIRECTORY_PATH:
return glob_item(c, i, clean_item_instance);
case REMOVE_PATH:
return glob_item(c, i, clean_remove_item_instance);
case RECURSIVE_REMOVE_PATH:
return glob_item(c, i, clean_recursive_remove_item_instance);
default:
return 0;
}
@@ -4043,9 +4474,11 @@ static int parse_line(
case ADJUST_MODE:
case RELABEL_PATH:
case RECURSIVE_RELABEL_PATH:
if (i.argument)
log_syntax(NULL, LOG_WARNING, fname, line, 0,
"%c lines don't take argument fields, ignoring.", (char) i.type);
if (i.argument) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"%c lines don't take argument fields.", (char) i.type);
}
break;
case CREATE_FILE:
@@ -4376,6 +4809,12 @@ static int parse_line(
_cleanup_free_ char *seconds = NULL, *age_by = NULL;
if (*a == '~') {
if (i.type == REMOVE_PATH) {
*invalid_config = true;
return log_syntax(NULL, LOG_ERR, fname, line, SYNTHETIC_ERRNO(EBADMSG),
"Age modifier '~' is not supported for %c lines.", (char) i.type);
}
i.keep_first_level = true;
a++;
}
+7
View File
@@ -21,12 +21,19 @@ test ! -e /tmp/fifo
test ! -e /tmp/test
# Test invalid config
ret=0
systemd-tmpfiles --inline --remove 'garbage' || ret=$?
test "$ret" -eq 65 # EX_DATAERR
ret=0
systemd-tmpfiles --inline --create 'd /tmp/test - - - - unexpected' || ret=$?
test "$ret" -eq 65 # EX_DATAERR
echo 'garbage' >/tmp/config.conf
ret=0
systemd-tmpfiles --remove /tmp/config.conf || ret=$?
test "$ret" -eq 65 # EX_DATAERR
ret=0
systemd-tmpfiles --remove /tmp/config-missing.conf || ret=$?
test "$ret" -eq 1
+149 -1
View File
@@ -84,7 +84,7 @@ else
fi
# Check for an invalid "age" and "age-by" arguments.
for a in ':' ':1s' '2:1h' 'nope:42h' '" :7m"' 'm:' '::' '"+r^w-x:2/h"' 'b ar::64'; do
for a in ':' ':1s' '2:1h' 'nope:42h' '" :7m"' 'm:' '::' '"+r^w-x:2/h"' '"b ar::64"'; do
systemd-tmpfiles --clean - <<EOF 2>&1 | grep -F 'Invalid age' >/dev/null
d /tmp/ageby - - - ${a} -
EOF
@@ -220,6 +220,154 @@ test "$before" = "$after"
test ! -e /tmp/ageby-mtime/old-child
rmdir /tmp/ageby-mtime
# r/R entries honor age when cleaning.
rm -rf /tmp/ageby-remove
mkdir -p /tmp/ageby-remove/old-dir/child /tmp/ageby-remove/new-dir/child \
/tmp/ageby-remove/old-empty-dir /tmp/ageby-remove/new-empty-dir
touch /tmp/ageby-remove/old-file /tmp/ageby-remove/new-file \
/tmp/ageby-remove/old-recursive-file /tmp/ageby-remove/new-recursive-file
touch --date "3 minutes ago" \
/tmp/ageby-remove/old-file \
/tmp/ageby-remove/old-recursive-file \
/tmp/ageby-remove/old-dir/child \
/tmp/ageby-remove/old-dir \
/tmp/ageby-remove/old-empty-dir
output="$(systemd-tmpfiles --dry-run --clean - 2>&1 <<-EOF
r /tmp/ageby-remove/old-file - - - m:1m -
r /tmp/ageby-remove/new-file - - - m:1m -
R /tmp/ageby-remove/old-recursive-file - - - m:1m -
R /tmp/ageby-remove/new-recursive-file - - - m:1m -
R /tmp/ageby-remove/old-dir - - - M:1m -
R /tmp/ageby-remove/new-dir - - - M:1m -
R /tmp/ageby-remove/old-empty-dir - - - M:1m -
R /tmp/ageby-remove/new-empty-dir - - - M:1m -
EOF
)"
[[ "$output" == *"Would remove directory \"/tmp/ageby-remove/old-dir\""* ]]
[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/new-dir\""* ]]
[[ "$output" == *"Would remove directory \"/tmp/ageby-remove/old-empty-dir\""* ]]
[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/new-empty-dir\""* ]]
test -e /tmp/ageby-remove/old-file
test -e /tmp/ageby-remove/new-file
test -e /tmp/ageby-remove/old-recursive-file
test -e /tmp/ageby-remove/new-recursive-file
test -d /tmp/ageby-remove/old-dir
test -d /tmp/ageby-remove/old-dir/child
test -d /tmp/ageby-remove/new-dir
test -d /tmp/ageby-remove/new-dir/child
test -d /tmp/ageby-remove/old-empty-dir
test -d /tmp/ageby-remove/new-empty-dir
systemd-tmpfiles --clean - <<-EOF
r /tmp/ageby-remove/old-file - - - m:1m -
r /tmp/ageby-remove/new-file - - - m:1m -
R /tmp/ageby-remove/old-recursive-file - - - m:1m -
R /tmp/ageby-remove/new-recursive-file - - - m:1m -
R /tmp/ageby-remove/old-dir - - - M:1m -
R /tmp/ageby-remove/new-dir - - - M:1m -
R /tmp/ageby-remove/old-empty-dir - - - M:1m -
R /tmp/ageby-remove/new-empty-dir - - - M:1m -
EOF
test ! -e /tmp/ageby-remove/old-file
test -e /tmp/ageby-remove/new-file
test ! -e /tmp/ageby-remove/old-recursive-file
test -e /tmp/ageby-remove/new-recursive-file
test ! -e /tmp/ageby-remove/old-dir
test -d /tmp/ageby-remove/new-dir
test -d /tmp/ageby-remove/new-dir/child
test ! -e /tmp/ageby-remove/old-empty-dir
test -d /tmp/ageby-remove/new-empty-dir
# Aged r cleanup removes empty directories but not non-empty or too-new directories.
mkdir -p /tmp/ageby-remove/old-r-empty-dir \
/tmp/ageby-remove/new-r-empty-dir \
/tmp/ageby-remove/old-r-nonempty-dir/child
touch --date "3 minutes ago" \
/tmp/ageby-remove/old-r-empty-dir \
/tmp/ageby-remove/old-r-nonempty-dir
output="$(systemd-tmpfiles --dry-run --clean - 2>&1 <<-EOF
r /tmp/ageby-remove/old-r-empty-dir - - - M:1m -
r /tmp/ageby-remove/new-r-empty-dir - - - M:1m -
r /tmp/ageby-remove/old-r-nonempty-dir - - - M:1m -
EOF
)"
[[ "$output" == *"Would remove directory \"/tmp/ageby-remove/old-r-empty-dir\""* ]]
[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/new-r-empty-dir\""* ]]
[[ "$output" != *"Would remove directory \"/tmp/ageby-remove/old-r-nonempty-dir\""* ]]
systemd-tmpfiles --clean - <<-EOF
r /tmp/ageby-remove/old-r-empty-dir - - - M:1m -
r /tmp/ageby-remove/new-r-empty-dir - - - M:1m -
r /tmp/ageby-remove/old-r-nonempty-dir - - - M:1m -
EOF
test ! -e /tmp/ageby-remove/old-r-empty-dir
test -d /tmp/ageby-remove/new-r-empty-dir
test -d /tmp/ageby-remove/old-r-nonempty-dir/child
# Aged R cleanup keeps the top-level directory if a child is too new.
mkdir -p /tmp/ageby-remove/old-dir-new-child/new-child
touch --date "3 minutes ago" /tmp/ageby-remove/old-dir-new-child
systemd-tmpfiles --clean --inline "R /tmp/ageby-remove/old-dir-new-child - - - M:1m -"
test -d /tmp/ageby-remove/old-dir-new-child/new-child
(! systemd-tmpfiles --clean --inline "r /tmp/ageby-remove/new-file - - - ~1m -")
# The ~ age modifier on R keeps the target and its immediate children.
mkdir -p /tmp/ageby-remove/keep-root/old-child/grandchild
touch --date "3 minutes ago" \
/tmp/ageby-remove/keep-root \
/tmp/ageby-remove/keep-root/old-child \
/tmp/ageby-remove/keep-root/old-child/grandchild
systemd-tmpfiles --clean --inline "R /tmp/ageby-remove/keep-root - - - ~M:1m -"
test -d /tmp/ageby-remove/keep-root
test -d /tmp/ageby-remove/keep-root/old-child
test ! -e /tmp/ageby-remove/keep-root/old-child/grandchild
# Aged r/R cleanup skips locked files.
touch /tmp/ageby-remove/locked-file /tmp/ageby-remove/locked-recursive-file
touch --date "3 minutes ago" /tmp/ageby-remove/locked-file /tmp/ageby-remove/locked-recursive-file
exec {lock_fd}<>/tmp/ageby-remove/locked-file
exec {recursive_lock_fd}<>/tmp/ageby-remove/locked-recursive-file
flock --exclusive "$lock_fd"
flock --exclusive "$recursive_lock_fd"
systemd-tmpfiles --clean - <<-EOF
r /tmp/ageby-remove/locked-file - - - m:1m -
R /tmp/ageby-remove/locked-recursive-file - - - m:1m -
EOF
test -e /tmp/ageby-remove/locked-file
test -e /tmp/ageby-remove/locked-recursive-file
exec {lock_fd}>&-
exec {recursive_lock_fd}>&-
systemd-tmpfiles --clean - <<-EOF
r /tmp/ageby-remove/locked-file - - - m:1m -
R /tmp/ageby-remove/locked-recursive-file - - - m:1m -
EOF
test ! -e /tmp/ageby-remove/locked-file
test ! -e /tmp/ageby-remove/locked-recursive-file
# Aged r cleanup skips locked directories.
mkdir -p /tmp/ageby-remove/locked-r-dir
touch --date "3 minutes ago" /tmp/ageby-remove/locked-r-dir
flock --exclusive /tmp/ageby-remove/locked-r-dir systemd-tmpfiles --clean --inline \
"r /tmp/ageby-remove/locked-r-dir - - - M:1m -"
test -d /tmp/ageby-remove/locked-r-dir
systemd-tmpfiles --clean --inline "r /tmp/ageby-remove/locked-r-dir - - - M:1m -"
test ! -e /tmp/ageby-remove/locked-r-dir
# Aged R cleanup skips locked directories and everything below them.
mkdir -p /tmp/ageby-remove/locked-dir/child
touch --date "3 minutes ago" /tmp/ageby-remove/locked-dir /tmp/ageby-remove/locked-dir/child
flock --exclusive /tmp/ageby-remove/locked-dir systemd-tmpfiles --clean --inline \
"R /tmp/ageby-remove/locked-dir - - - M:1m -"
test -d /tmp/ageby-remove/locked-dir/child
systemd-tmpfiles --clean --inline "R /tmp/ageby-remove/locked-dir - - - M:1m -"
test ! -e /tmp/ageby-remove/locked-dir
rm -rf /tmp/ageby-remove
# X entries inherit the parent cleanup age and its age-by fields.
rm -rf /tmp/ageby-x-inherit
mkdir -p /tmp/ageby-x-inherit/parent/child /tmp/ageby-x-inherit/parent/other