Add the --stop-after & --stop-at options.
authorWayne Davison <wayne@opencoder.net>
Mon, 13 Jul 2020 01:32:41 +0000 (18:32 -0700)
committerWayne Davison <wayne@opencoder.net>
Mon, 13 Jul 2020 01:32:41 +0000 (18:32 -0700)
NEWS.md
configure.ac
io.c
options.c
rsync.1.md

diff --git a/NEWS.md b/NEWS.md
index 9bd98baedb468dab671e81ac5f3e24b26af64b47..30a741288b5fc4839c9990ed242905177f709bb7 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
    other user/group names in the transfer (instead of assuming that both sides
    have the same id-0 names).
 
+ - Added the `--stop-after=MINS` and `--stop-at=DATE_TIME` options (with the
+   `--time-limit=MINS` option accepted as an alias for `--stop-after`).  This
+   is an enhanced version of the time-limit patch from the patches repo.
+
  - Added some compatibility code for HPE NonStop platforms.
 
 ### INTERNAL:
index 69c8f933a59411a35f3865bb99b5a6acebbd7e31..fac166c814771508955b2451e408280affe4040d 100644 (file)
@@ -822,7 +822,7 @@ AC_FUNC_UTIME_NULL
 AC_FUNC_ALLOCA
 AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \
     fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \
-    chflags getattrlist \
+    chflags getattrlist mktime \
     memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \
     strlcat strlcpy strtol mallinfo getgroups setgroups geteuid getegid \
     setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \
diff --git a/io.c b/io.c
index ddd20fa8d12b54975205d5e4bcb98081a868e93d..1785f832678856ee6d31faa22800e2b015697859 100644 (file)
--- a/io.c
+++ b/io.c
@@ -60,6 +60,7 @@ extern int preserve_hard_links;
 extern BOOL extra_flist_sending_enabled;
 extern BOOL flush_ok_after_signal;
 extern struct stats stats;
+extern time_t stop_at_utime;
 extern struct file_list *cur_flist;
 #ifdef ICONV_OPTION
 extern int filesfrom_convert;
@@ -785,9 +786,13 @@ static char *perform_io(size_t needed, int flags)
                        if (msgs2stderr && DEBUG_GTE(IO, 2))
                                rprintf(FINFO, "[%s] recv=%ld\n", who_am_i(), (long)n);
 
-                       if (io_timeout) {
+                       if (io_timeout || stop_at_utime) {
                                last_io_in = time(NULL);
-                               if (flags & PIO_NEED_INPUT)
+                               if (stop_at_utime && last_io_in >= stop_at_utime) {
+                                       rprintf(FERROR, "stopping at requested limit\n");
+                                       exit_cleanup(RERR_TIMEOUT);
+                               }
+                               if (io_timeout && flags & PIO_NEED_INPUT)
                                        maybe_send_keepalive(last_io_in, 0);
                        }
                        stats.total_read += n;
index 1e438fb10fc7781ae2201f2cbe477c50acd89a9b..9ed164058f797a690955098bb643e624dce10b12 100644 (file)
--- a/options.c
+++ b/options.c
@@ -119,6 +119,7 @@ size_t bwlimit_writemax = 0;
 int ignore_existing = 0;
 int ignore_non_existing = 0;
 int need_messages_from_generator = 0;
+time_t stop_at_utime = 0;
 int max_delete = INT_MIN;
 OFF_T max_size = -1;
 OFF_T min_size = -1;
@@ -664,6 +665,11 @@ static void print_info_flags(enum logcode f)
 #endif
                        "prealloc",
 
+#ifndef HAVE_MKTIME
+               "no "
+#endif
+                       "stop-at",
+
        "*Optimizations",
 
 #ifndef HAVE_SIMD
@@ -779,6 +785,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM,
       OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE,
       OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT,
       OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS,
+      OPT_STOP_AFTER, OPT_STOP_AT,
       OPT_REFUSED_BASE = 9000};
 
 static struct poptOption long_options[] = {
@@ -988,6 +995,9 @@ static struct poptOption long_options[] = {
   {"no-timeout",       0,  POPT_ARG_VAL,    &io_timeout, 0, 0, 0 },
   {"contimeout",       0,  POPT_ARG_INT,    &connect_timeout, 0, 0, 0 },
   {"no-contimeout",    0,  POPT_ARG_VAL,    &connect_timeout, 0, 0, 0 },
+  {"stop-after",       0,  POPT_ARG_STRING, 0, OPT_STOP_AFTER, 0, 0 },
+  {"time-limit",       0,  POPT_ARG_STRING, 0, OPT_STOP_AFTER, 0, 0 }, /* earlier stop-after name */
+  {"stop-at",          0,  POPT_ARG_STRING, 0, OPT_STOP_AT, 0, 0 },
   {"rsh",             'e', POPT_ARG_STRING, &shell_cmd, 0, 0, 0 },
   {"rsync-path",       0,  POPT_ARG_STRING, &rsync_path, 0, 0, 0 },
   {"temp-dir",        'T', POPT_ARG_STRING, &tmpdir, 0, 0, 0 },
@@ -1192,6 +1202,9 @@ static void set_refuse_options(void)
 #ifndef SUPPORT_HARD_LINKS
        parse_one_refuse_match(0, "link-dest", list_end);
 #endif
+#ifndef HAVE_MKTIME
+       parse_one_refuse_match(0, "stop-at", list_end);
+#endif
 #ifndef ICONV_OPTION
        parse_one_refuse_match(0, "iconv", list_end);
 #endif
@@ -1326,6 +1339,148 @@ failure:
        return -1;
 }
 
+#ifdef HAVE_MKTIME
+/* Allow the user to specify a time in the format yyyy-mm-ddThh:mm while
+ * also allowing abbreviated data.  For instance, if the time is omitted,
+ * it defaults to midnight.  If the date is omitted, it defaults to the
+ * next possible date in the future with the specified time.  Even the
+ * year or year-month can be omitted, again defaulting to the next date
+ * in the future that matches the specified information.  A 2-digit year
+ * is also OK, as is using '/' instead of '-'. */
+static time_t parse_time(const char *arg)
+{
+       const char *cp;
+       time_t val, now = time(NULL);
+       struct tm t, *today = localtime(&now);
+       int in_date, old_mday, n;
+
+       memset(&t, 0, sizeof t);
+       t.tm_year = t.tm_mon = t.tm_mday = -1;
+       t.tm_hour = t.tm_min = t.tm_isdst = -1;
+       cp = arg;
+       if (*cp == 'T' || *cp == 't' || *cp == ':') {
+               in_date = *cp == ':' ? 0 : -1;
+               cp++;
+       } else
+               in_date = 1;
+       for ( ; ; cp++) {
+               if (!isDigit(cp))
+                       return (time_t)-1;
+               n = 0;
+               do {
+                       n = n * 10 + *cp++ - '0';
+               } while (isDigit(cp));
+               if (*cp == ':')
+                       in_date = 0;
+               if (in_date > 0) {
+                       if (t.tm_year != -1)
+                               return (time_t)-1;
+                       t.tm_year = t.tm_mon;
+                       t.tm_mon = t.tm_mday;
+                       t.tm_mday = n;
+                       if (!*cp)
+                               break;
+                       if (*cp == 'T' || *cp == 't') {
+                               if (!cp[1])
+                                       break;
+                               in_date = -1;
+                       } else if (*cp != '-' && *cp != '/')
+                               return (time_t)-1;
+                       continue;
+               }
+               if (t.tm_hour != -1)
+                       return (time_t)-1;
+               t.tm_hour = t.tm_min;
+               t.tm_min = n;
+               if (!*cp) {
+                       if (in_date < 0)
+                               return (time_t)-1;
+                       break;
+               }
+               if (*cp != ':')
+                       return (time_t)-1;
+               in_date = 0;
+       }
+
+       in_date = 0;
+       if (t.tm_year < 0) {
+               t.tm_year = today->tm_year;
+               in_date = 1;
+       } else if (t.tm_year < 100) {
+               while (t.tm_year < today->tm_year)
+                       t.tm_year += 100;
+       } else
+               t.tm_year -= 1900;
+       if (t.tm_mon < 0) {
+               t.tm_mon = today->tm_mon;
+               in_date = 2;
+       } else
+               t.tm_mon--;
+       if (t.tm_mday < 0) {
+               t.tm_mday = today->tm_mday;
+               in_date = 3;
+       }
+
+       n = 0;
+       if (t.tm_min < 0) {
+               t.tm_hour = t.tm_min = 0;
+       } else if (t.tm_hour < 0) {
+               if (in_date != 3)
+                       return (time_t)-1;
+               in_date = 0;
+               t.tm_hour = today->tm_hour;
+               n = 60*60;
+       }
+
+       /* Note that mktime() might change a too-large tm_mday into the start of
+        * the following month which we need to undo in the following code! */
+       old_mday = t.tm_mday;
+       if (t.tm_hour > 23 || t.tm_min > 59
+           || t.tm_mon < 0 || t.tm_mon >= 12
+           || t.tm_mday < 1 || t.tm_mday > 31
+           || (val = mktime(&t)) == (time_t)-1)
+               return (time_t)-1;
+
+       while (in_date && (val <= now || t.tm_mday < old_mday)) {
+               switch (in_date) {
+               case 3:
+                       old_mday = ++t.tm_mday;
+                       break;
+               case 2:
+                       if (t.tm_mday < old_mday)
+                               t.tm_mday = old_mday; /* The month already got bumped forward */
+                       else if (++t.tm_mon == 12) {
+                               t.tm_mon = 0;
+                               t.tm_year++;
+                       }
+                       break;
+               case 1:
+                       if (t.tm_mday < old_mday) {
+                               /* mon==1 mday==29 got bumped to mon==2 */
+                               if (t.tm_mon != 2 || old_mday != 29)
+                                       return (time_t)-1;
+                               t.tm_mon = 1;
+                               t.tm_mday = 29;
+                       }
+                       t.tm_year++;
+                       break;
+               }
+               if ((val = mktime(&t)) == (time_t)-1) {
+                       /* This code shouldn't be needed, as mktime() should auto-round to the next month. */
+                       if (in_date != 3 || t.tm_mday <= 28)
+                               return (time_t)-1;
+                       t.tm_mday = old_mday = 1;
+                       in_date = 2;
+               }
+       }
+       if (n) {
+               while (val <= now)
+                       val += n;
+       }
+       return val;
+}
+#endif
+
 static void create_refuse_error(int which)
 {
        const char *msg;
@@ -1892,6 +2047,32 @@ int parse_arguments(int *argc_p, const char ***argv_p)
                        return 0;
 #endif
 
+               case OPT_STOP_AFTER: {
+                       long val;
+                       arg = poptGetOptArg(pc);
+                       stop_at_utime = time(NULL);
+                       if ((val = atol(arg) * 60) <= 0 || val + (long)stop_at_utime < 0) {
+                               snprintf(err_buf, sizeof err_buf, "invalid --stop-after value: %s\n", arg);
+                               return 0;
+                       }
+                       stop_at_utime += val;
+                       break;
+               }
+
+#ifdef HAVE_MKTIME
+               case OPT_STOP_AT:
+                       arg = poptGetOptArg(pc);
+                       if ((stop_at_utime = parse_time(arg)) == (time_t)-1) {
+                               snprintf(err_buf, sizeof err_buf, "invalid --stop-at format: %s\n", arg);
+                               return 0;
+                       }
+                       if (stop_at_utime <= time(NULL)) {
+                               snprintf(err_buf, sizeof err_buf, "--stop-at time is not in the future: %s\n", arg);
+                               return 0;
+                       }
+                       break;
+#endif
+
                default:
                        /* A large opt value means that set_refuse_options()
                         * turned this option off. */
index 685c5c373b6831c5a9ac1ea597b45462131153f7..5be8029cbc61f4253ea059b37bc45ea1320aced5 100644 (file)
@@ -457,6 +457,8 @@ detailed description below for a complete description.
 --early-input=FILE       use FILE for daemon's early exec input
 --list-only              list the files instead of copying them
 --bwlimit=RATE           limit socket I/O bandwidth
+--stop-after=MINS        Stop rsync after MINS minutes have elapsed
+--stop-at=y-m-dTh:m      Stop rsync at the specified moment in time
 --write-batch=FILE       write a batched update to FILE
 --only-write-batch=FILE  like --write-batch but w/o updating dest
 --read-batch=FILE        read a batched update from FILE
@@ -3113,6 +3115,45 @@ your home directory (remove the '=' for that).
     buffered, while other can show up as very slow when the flushing of the
     output buffer occurs.  This may be fixed in a future version.
 
+0.  `--stop-after=MINS
+
+    This option tells rsync to stop copying when the specified number of
+    minutes has elapsed.
+
+    Rsync also accepts an earlier version of this option: `--time-limit=MINS`.
+
+    For maximal flexibility, rsync does not communicate this option to the
+    remote rsync since it is usually enough that one side of the connection
+    quits as specified.  This allows the option's use even when only one side
+    of the connection supports it.  You can tell the remote side about the time
+    limit using `--remote-option` (`-M`), should the need arise.
+
+0.  `--stop-at=y-m-dTh:m
+
+    This option tells rsync to stop copying when the specified point in time
+    has been reached. The date & time can be fully specified in a numeric
+    format of year-month-dayThour:minute (e.g. 2000-12-31T23:59) in the local
+    timezone.  You may choose to separate the date numbers using slashes
+    instead of dashes.
+
+    The value can also be abbreviated in a variety of ways, such as specifying
+    a 2-digit year and/or leaving off various values.  In all cases, the value
+    will be taken to be the next possible future moment where the supplied
+    information matches.  If the value specifies the current time or a past
+    time, rsync exits with an error.
+
+    For example, "1-30" specifies the next January 30th (at midnight local
+    time), "14:00" specifies the next 2 P.M., "1" specifies the next 1st of the
+    month at midnight, and ":59" specifies the next 59th minute after the hour.
+
+    For maximal flexibility, rsync does not communicate this option to the
+    remote rsync since it is usually enough that one side of the connection
+    quits as specified.  This allows the option's use even when only one side
+    of the connection supports it.  You can tell the remote side about the time
+    limit using `--remote-option` (`-M`), should the need arise.  Do keep in
+    mind that the remote host may have a different default timezone than your
+    local host.
+
 0.  `--write-batch=FILE`
 
     Record a file that can later be applied to another identical destination