diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 3a54abde..8e410aaf 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -34,6 +34,7 @@ #include #include +#include #ifdef _LIBITE_LITE # include #else @@ -46,6 +47,7 @@ int debug; int create_flag = 0; +int clean_flag = 0; int remove_flag = 0; static int is_dir_empty(const char *path) @@ -261,6 +263,82 @@ static void write_arg(FILE *fp, char *arg) fputs(arg, fp); } +/** + * Parse age string and return age in seconds. + * Returns 0 on invalid/empty age ("-" or NULL). + * Examples: "10d" = 864000, "1w" = 604800, "2h" = 7200 + */ +static time_t parse_age(const char *age) +{ + time_t val; + char *end; + + if (!age || !strcmp(age, "-") || !strcmp(age, "0")) + return 0; + + errno = 0; + val = strtoul(age, &end, 10); + if (errno || end == age) + return 0; + + switch (*end) { + case 'w': + val *= 7; + /* fallthrough */ + case 'd': + val *= 24; + /* fallthrough */ + case 'h': + val *= 60; + /* fallthrough */ + case 'm': + val *= 60; + /* fallthrough */ + case 's': + case '\0': + break; + default: + return 0; /* unknown suffix */ + } + + return val; +} + +/* Globals for do_clean() callback - nftw doesn't support user data */ +static time_t clean_age; +static time_t clean_now; + +static int do_clean(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw) +{ + int is_dir; + + /* Skip the root directory itself */ + if (ftw->level == 0) + return 0; + + is_dir = (tflag == FTW_D || tflag == FTW_DP); + + /* + * Conservative cleanup matching systemd-tmpfiles behavior: + * - Files: keep if ANY of atime, ctime, mtime is recent + * - Directories: keep if ANY of atime, mtime is recent (ctime + * excluded because cleanup itself updates directory ctime) + */ + if (clean_now - sb->st_mtime < clean_age) + return 0; /* mtime is too new, keep it */ + + if (clean_now - sb->st_atime < clean_age) + return 0; /* atime is too new, keep it */ + + if (!is_dir && clean_now - sb->st_ctime < clean_age) + return 0; /* ctime is too new, keep it (files only) */ + + if (remove(fpath) && errno != ENOENT && errno != EBUSY) + warn("Failed cleaning %s", fpath); + + return 0; +} + /* * The configuration format is one line per path, containing type, path, * mode, ownership, age, and argument fields. The lines are separated by @@ -312,7 +390,6 @@ static void tmpfiles(char *line) group = "root"; age = strtok(NULL, "\t "); - (void)age; /* unused atm. */ arg = strtok(NULL, "\n"); strc = stat(path, &st); @@ -349,7 +426,7 @@ static void tmpfiles(char *line) break; case 'X': case 'x': - dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path); + /* handled by clean_flag (--clean) */ break; case 'Z': case 'z': @@ -360,6 +437,32 @@ static void tmpfiles(char *line) } } + /* age-based cleanup logic */ + if (clean_flag) { + time_t max_age = parse_age(age); + + /* Only process if age is specified */ + if (max_age > 0) { + switch (type[0]) { + case 'd': + case 'D': + case 'e': + if (!fisdir(path)) + break; + clean_age = max_age; + clean_now = time(NULL); + nftw(path, do_clean, 20, FTW_DEPTH | FTW_PHYS); + break; + case 'x': + case 'X': + /* exclusion patterns - not yet implemented */ + break; + default: + break; + } + } + } + // file & directory creation logic if (create_flag) { switch (type[0]) { @@ -505,7 +608,7 @@ static void tmpfiles(char *line) break; case 'X': case 'x': - dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path); + /* handled by clean_flag (--clean) */ break; case 'Z': opts = "-R"; @@ -538,6 +641,7 @@ static int usage(int rc) "Usage: tmpfiles [COMMAND...]\n" "\n" "Commands:\n" + " -C, --clean Clean files and directories based on age\n" " -c, --create Create files and directories\n" " -d, --debug Show developer debug messages\n" " -r, --remove Remove files and directories marked for removal\n" @@ -550,6 +654,7 @@ static int usage(int rc) int main(int argc, char *argv[]) { struct option long_options[] = { + { "clean", 0, NULL, 'C' }, { "create", 0, NULL, 'c' }, { "debug", 0, NULL, 'd' }, { "remove", 0, NULL, 'r' }, @@ -559,8 +664,12 @@ int main(int argc, char *argv[]) int c; - while ((c = getopt_long(argc, argv, "cdrh?", long_options, NULL)) != EOF) { + while ((c = getopt_long(argc, argv, "Ccdrh?", long_options, NULL)) != EOF) { switch(c) { + case 'C': + clean_flag = 1; + break; + case 'c': create_flag = 1; break; @@ -582,8 +691,8 @@ int main(int argc, char *argv[]) } } - if (create_flag + remove_flag == 0) { - fprintf(stderr, "You need to specify at least one of --create or --remove.\n"); + if (create_flag + clean_flag + remove_flag == 0) { + fprintf(stderr, "You need to specify at least one of --clean, --create, or --remove.\n"); return 1; }