diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 65ab7ff12ff..ff9e8edbd47 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -7674,6 +7674,71 @@ static FnCallResult FnCallStrToTime(ARG_UNUSED EvalContext *ctx, ARG_UNUSED cons /*********************************************************************/ +static FnCallResult FnCallFileOlderThan(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) +{ + assert(fp != NULL); + + if (finalargs == NULL) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires path as first argument", + fp->name); + return FnFailure(); + } + const char *filename = RlistScalarValue(finalargs); + + if (finalargs->next == NULL) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires date or date offset as second argument", + fp->name); + return FnFailure(); + } + const char *date = RlistScalarValue(finalargs->next); + const char *option = (finalargs->next->next != NULL) ? RlistScalarValue(finalargs->next->next) : "modification"; + + struct stat statbuf; + + if (stat(filename, &statbuf) != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Couldn't stat '%s'", fp->name, filename); + return FnFailure(); + } + + time_t file_ts; + + if (StringEqual_IgnoreCase(option, "modification") || StringEqual_IgnoreCase(option, "modif") ) + { + file_ts = statbuf.st_mtime; + } + else if (StringEqual_IgnoreCase(option, "access")) + { + file_ts = statbuf.st_atime; + } + else if (StringEqual_IgnoreCase(option, "change")) + { + file_ts = statbuf.st_ctime; + } + else + { + ProgrammingError("Unknown option for %s\n", fp->name); + } + + time_t input_time; + int ret = ParseDate(date, &input_time); + + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Couldn't parse '%s'", fp->name, date); + return FnFailure(); + } + + time_t now = time(NULL); + time_t offset = input_time - now; // convert date to an offset + + return FnReturnContext(file_ts + offset <= now); +} + +/*********************************************************************/ + static FnCallResult FnCallEval(EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) { if (finalargs == NULL) @@ -11738,6 +11803,14 @@ static const FnCallArg ISREADABLE_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg FILE_OLDER_THAN_ARGS[] = +{ + {CF_ABSPATHRANGE, CF_DATA_TYPE_STRING, "Path to file"}, + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Date string"}, + {"modification,modif,access,change", CF_DATA_TYPE_OPTION, "file timespec"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg DATATYPE_ARGS[] = { {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Variable identifier"}, @@ -12188,6 +12261,9 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(2, 3)), FnCallTypeNew("isreadable", CF_DATA_TYPE_CONTEXT, ISREADABLE_ARGS, &FnCallIsReadable, "Check if file is readable. Timeout immediately or after optional timeout interval", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(1, 2)), + FnCallTypeNew("file_older_than", CF_DATA_TYPE_CONTEXT, FILE_OLDER_THAN_ARGS, &FnCallFileOlderThan, "Check if file is older than a time offset or date.", + FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(2, 3)), + // Datatype functions FnCallTypeNew("type", CF_DATA_TYPE_STRING, DATATYPE_ARGS, &FnCallDatatype, "Get type description as string", diff --git a/tests/acceptance/01_vars/02_functions/file_older_than.cf b/tests/acceptance/01_vars/02_functions/file_older_than.cf new file mode 100644 index 00000000000..4f0a24041ad --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/file_older_than.cf @@ -0,0 +1,64 @@ +####################################################### +# +# Test file_older_than function +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent init +{ + files: + "$(G.testdir)/somefile.txt" + create => "true", + content => "Hello world!"; +} + +####################################################### +bundle agent test +{ + classes: + "long_time_past" + expression => file_older_than( + "$(G.testdir)/somefile.txt", "100 years ago" + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "long_time_future" + expression => not( + file_older_than("$(G.testdir)/somefile.txt", "100 years") + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "newer_than_a_week" + expression => file_older_than( + "$(G.testdir)/somefile.txt", "-1 week", "modif" + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "ok" + expression => and( + "long_time_past", "long_time_future", "newer_than_a_week" + ), + if => fileexists("$(G.testdir)/somefile.txt"), + scope => "namespace"; +} + +####################################################### +bundle agent check +{ + files: + "$(G.testdir)/somefile.txt" delete => tidy; + + reports: + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; +}