Added -clobber switch to mhstore(1) [Bug #11160].
authorDavid Levine <levinedl@acm.org>
Sat, 15 Sep 2012 18:40:50 +0000 (13:40 -0500)
committerDavid Levine <levinedl@acm.org>
Sat, 15 Sep 2012 18:40:50 +0000 (13:40 -0500)
docs/README.developers
docs/pending-release-notes
man/mhstore.man
test/mhstore/test-mhstore
uip/mhstore.c
uip/mhstoresbr.c

index bae6d9c..9038cd2 100644 (file)
@@ -6,7 +6,19 @@ This file is intended to provide a few tips for anyone doing development on nmh.
 Developers who learn things "the hard way" about the nmh codebase (as opposed to
 local info best encoded in a comment) are encouraged to share their wisdom here.
 
-The topics are organized alphabetically.
+Following a commit checklist, the topics are organized alphabetically.
+
+----------------
+commit checklist
+----------------
+
+1. code updated?
+2. test added?
+3. man page and other documentation updated?
+4. docs/pending-release-notes updated?
+5. should commit message reference bug report?
+6. update/close bug report (with commit id)?
+7. notify nmh-users?
 
 
 -------------------------
index c286c3d..468da54 100644 (file)
@@ -22,6 +22,8 @@ NEW FEATURES
   pid.time@localname, where time is in seconds, and matches previous
   behavior.  The random style replaces the localname with some
   (pseudo)random bytes and uses microsecond-resolution time.
+- Added -clobber switch to mhstore(1) to control overwriting of
+  existing files.
 
 ----------------------------
 OBSOLETE/DEPRECATED FEATURES
index 470714c..5efc66f 100644 (file)
@@ -19,6 +19,8 @@ mhstore \- store contents of MIME messages into files
 .IR content ]
 \&...
 .RB [ \-auto " | " \-noauto ]
+.RB [ \-clobber
+.IR always " | " auto " | " suffix " | " ask " | " never ]
 .RB [ \-rcache
 .IR policy ]
 .RB [ \-wcache
@@ -268,6 +270,73 @@ mhstore-store-application/PostScript: %m%P.ps
 .fi
 .RE
 .PP
+.SS "Overwriting Existing Files"
+The
+.B \-clobber
+switch controls whether
+.B mhstore
+should overwrite existing files.  The allowed values for this switch
+and corresponding behavior when
+.B mhstore
+encounters an existing file are:
+.PP
+.RS 5
+.nf
+.ta \w'suffix  'u
+always    Overwrite existing file (default)
+auto      Create new file of form name-n.extension
+suffix    Create new file of form name.extension.n
+ask       Prompt the user to specify whether or not to overwrite
+          the existing file
+never     Do not overwrite existing file
+.fi
+.RE
+.PP
+With
+.I auto
+and
+.IR suffix ,
+.I n
+is one beyond the existing file with the highest number in the same
+form.
+If a filename does not have an extension (following a '.'), then
+.I auto
+and
+.I suffix
+create a new file of the form
+.I name-n
+and
+.IR name.n ,
+respectively.  With
+.I never
+and
+.IR ask ,
+the exit status of
+.B mhstore
+will be the number of files that were requested but not stored.
+.PP
+With
+.IR ask ,
+if standard input is connected to a terminal,
+the user is prompted to respond
+.IR yes ,
+.IR no ,
+or
+.I rename
+to whether the file should be overwritten.  The responses
+can be abbreviated.  If the user responds with
+.IR rename ,
+then
+.B mhstore
+prompts the user for the name of the new file to be created.  If it is
+a relative path name (does not begin with '/'), then it is relative to
+the current directory.  If it is an absolute or relative path to a
+directory that does not exist, the user will be prompted whether to
+create the directory.  If standard input is not connected to a
+terminal,
+.I ask
+behaves the same as
+.IR always .
 .SS "Reassembling Messages of Type message/partial"
 .B mhstore
 is also able to reassemble messages that have been
@@ -462,6 +531,7 @@ mhbuild(1), mhlist(1), mhshow(1), sendfiles(1)
 .RB ` +folder "' defaults to the current folder"
 .RB ` msgs "' defaults to cur"
 .RB ` \-noauto '
+.RB ` \-clobber\ always '
 .RB ` \-nocheck '
 .RB ` \-rcache\ ask '
 .RB ` \-wcache\ ask '
index 55cea9f..421580b 100755 (executable)
@@ -239,6 +239,45 @@ EOF
 run_test 'mhstore last' 'storing message 15 as file 15.txt'
 check $expected 15.txt
 
+# check -clobber always
+folder +inbox 7 > /dev/null
+touch 7.txt
+cat > $expected <<EOF
+This is message number 7
+EOF
+run_test 'mhstore' 'storing message 7 as file 7.txt'
+check $expected 7.txt 'keep first'
+run_test 'mhstore -clobber always' 'storing message 7 as file 7.txt'
+check $expected 7.txt 'keep first'
+
+# check -clobber auto
+touch 7.txt
+run_test 'mhstore -clobber auto' 'storing message 7 as file 7-1.txt'
+check $expected 7-1.txt 'keep first'
+touch 7-1.txt
+run_test 'mhstore -clobber auto' 'storing message 7 as file 7-2.txt'
+check $expected 7-2.txt 'keep first'
+
+# check -clobber suffix
+run_test 'mhstore -clobber suffix' 'storing message 7 as file 7.txt.1'
+check $expected 7.txt.1 'keep first'
+touch 7.txt.1
+run_test 'mhstore -clobber suffix' 'storing message 7 as file 7.txt.2'
+check $expected 7.txt.2 'keep first'
+
+# Don't check -clobber ask because it requires connection to a
+# terminal, and this test won't always be run with one.
+
+# check -clobber never.  Its exit status is the number of files not overwritten.
+run_test 'mhstore -clobber never' \
+         "mhstore: will not overwrite `pwd`/7.txt with -clobber never"
+set +e
+mhstore -clobber never >/dev/null 2>&1
+run_test "echo $?" 1
+set -e
+
+/bin/rm -f 7.txt 7-1.txt 7.txt.1
+
 # check with relative nmh-storage profile component
 storagedir=storagedir
 dir="$MH_TEST_DIR/Mail/inbox/$storagedir"
index e491dd9..fa30e03 100644 (file)
@@ -47,16 +47,21 @@ static struct swit switches[] = {
     { "version", 0 },
 #define        HELPSW                 12
     { "help", 0 },
+#define        CLOBBERSW              13
+    { "clobber always|auto|suffix|ask|never", 0 },
 
 /*
  * switches for debugging
  */
-#define        DEBUGSW                13
+#define        DEBUGSW                14
     { "debug", -5 },
     { NULL, 0 }
 };
 
 
+int save_clobber_policy (const char *);
+extern int files_not_clobbered;
+
 /* mhparse.c */
 extern char *tmp;      /* directory to place temp files */
 
@@ -213,6 +218,14 @@ do_cache:
            case NVERBSW: 
                verbosw = 0;
                continue;
+            case CLOBBERSW:
+               if (!(cp = *argp++) || *cp == '-')
+                   adios (NULL, "missing argument to %s", argp[-2]);
+                if (save_clobber_policy (cp)) {
+                  adios (NULL, "invalid argument, %s, to %s", argp[-1],
+                         argp[-2]);
+                }
+               continue;
            case DEBUGSW:
                debugsw = 1;
                continue;
@@ -375,7 +388,7 @@ do_cache:
        context_save ();                  /* save the context file  */
     }
 
-    done (0);
+    done (files_not_clobbered);
     return 1;
 }
 
index ed1b822..f5e3347 100644 (file)
@@ -75,7 +75,7 @@ static int output_content_folder (char *, char *);
 static int parse_format_string (CT, char *, char *, int, char *);
 static void get_storeproc (CT);
 static int copy_some_headers (FILE *, CT);
-
+static char *clobber_check (char *);
 
 /*
  * Main entry point to store content
@@ -589,7 +589,9 @@ store_content (CT ct, CT p)
        return show_content_aux (ct, 1, 0, buffer + 1, dir);
 
     /* record the filename */
-    ct->c_storage = add (buffer, NULL);
+    if ((ct->c_storage = clobber_check (add (buffer, NULL))) == NULL) {
+      return NOTOK;
+    }
 
 got_filename:
     /* flush the output stream */
@@ -1076,3 +1078,222 @@ copy_some_headers (FILE *out, CT ct)
 
     return OK;
 }
+
+/******************************************************************************/
+/* -clobber support */
+
+enum clobber_policy_t {
+  NMH_CLOBBER_ALWAYS,
+  NMH_CLOBBER_AUTO,
+  NMH_CLOBBER_SUFFIX,
+  NMH_CLOBBER_ASK,
+  NMH_CLOBBER_NEVER
+};
+
+static enum clobber_policy_t clobber_policy = NMH_CLOBBER_ALWAYS;
+
+int files_not_clobbered = 0;
+
+int
+save_clobber_policy (const char *value) {
+  if (! mh_strcasecmp (value, "always")) {
+    clobber_policy = NMH_CLOBBER_ALWAYS;
+  } else if (! mh_strcasecmp (value, "auto")) {
+    clobber_policy = NMH_CLOBBER_AUTO;
+  } else if (! mh_strcasecmp (value, "suffix")) {
+    clobber_policy = NMH_CLOBBER_SUFFIX;
+  } else if (! mh_strcasecmp (value, "ask")) {
+    clobber_policy = NMH_CLOBBER_ASK;
+  } else if (! mh_strcasecmp (value, "never")) {
+    clobber_policy = NMH_CLOBBER_NEVER;
+  } else {
+    return 1;
+  }
+
+  return 0;
+}
+
+
+static char *
+next_version (char *file, enum clobber_policy_t clobber_policy) {
+  const size_t max_versions = 1000000;
+  /* 8 = log max_versions  +  one for - or .  +  one for null terminator */
+  const size_t buflen = strlen (file) + 8;
+  char *buffer = mh_xmalloc (buflen);
+  size_t version;
+
+  char *extension = NULL;
+  if (clobber_policy == NMH_CLOBBER_AUTO  &&
+      ((extension = strrchr (file, '.')) != NULL)) {
+    *extension++ = '\0';
+  }
+
+  for (version = 1; version < max_versions; ++version) {
+    struct stat st;
+
+    switch (clobber_policy) {
+      case NMH_CLOBBER_AUTO: {
+        snprintf (buffer, buflen, "%s-%ld%s%s", file, (long) version,
+                  extension == NULL  ?  ""  :  ".",
+                  extension == NULL  ?  ""  :  extension);
+        break;
+      }
+
+      case NMH_CLOBBER_SUFFIX:
+        snprintf (buffer, buflen, "%s.%ld", file, (long) version);
+        break;
+
+      default:
+        /* Should never get here. */
+        advise (NULL, "will not overwrite %s, invalid clobber policy", buffer);
+        free (buffer);
+        ++files_not_clobbered;
+        return NULL;
+    }
+
+    if (stat (buffer, &st) == NOTOK) {
+      break;
+    }
+  }
+
+  free (file);
+
+  if (version >= max_versions) {
+    advise (NULL, "will not overwrite %s, too many versions", buffer);
+    free (buffer);
+    buffer = NULL;
+    ++files_not_clobbered;
+  }
+
+  return buffer;
+}
+
+
+static char *
+clobber_check (char *original_file) {
+  /* clobber policy        return value
+   * --------------        ------------
+   *   -always                 file
+   *   -auto           file-<digits>.extension
+   *   -suffix             file.<digits>
+   *   -ask          file, 0, or another filename/path
+   *   -never                   0
+   */
+
+  char *file;
+  char *cwd = NULL;
+  int check_again;
+
+  if (clobber_policy == NMH_CLOBBER_ASK) {
+    /* Save cwd for possible use in loop below. */
+    char *slash;
+
+    cwd = add (original_file, NULL);
+    slash = strrchr (cwd, '/');
+
+    if (slash) {
+      *slash = '\0';
+    } else {
+      /* original_file wasn't a full path, which shouldn't happen. */
+      cwd = NULL;
+    }
+  }
+
+  do {
+    struct stat st;
+
+    file = original_file;
+    check_again = 0;
+
+    switch (clobber_policy) {
+      case NMH_CLOBBER_ALWAYS:
+        break;
+
+      case NMH_CLOBBER_SUFFIX:
+      case NMH_CLOBBER_AUTO:
+        if (stat (file, &st) == OK) {
+          file = next_version (original_file, clobber_policy);
+        }
+        break;
+
+      case NMH_CLOBBER_ASK:
+        if (stat (file, &st) == OK) {
+          enum answers { NMH_YES, NMH_NO, NMH_RENAME };
+          static struct swit answer[4] = {
+            { "yes", 0 }, { "no", 0 }, { "rename", 0 }, { NULL, 0 } };
+          char **ans;
+
+          if (isatty (fileno (stdin))) {
+            char *prompt =
+              concat ("Overwrite \"", file, "\" [y/n/rename]? ", NULL);
+            ans = getans (prompt, answer);
+            free (prompt);
+          } else {
+            /* Overwrite, that's what nmh used to do.  And warn. */
+            advise (NULL, "-clobber ask but no tty, so overwrite %s", file);
+            break;
+          }
+
+          switch ((enum answers) smatch (*ans, answer)) {
+            case NMH_YES:
+              break;
+            case NMH_NO:
+              free (file);
+              file = NULL;
+              ++files_not_clobbered;
+              break;
+            case NMH_RENAME: {
+              char buf[PATH_MAX];
+              printf ("Enter filename or full path of the new file: ");
+              if (fgets (buf, sizeof buf, stdin) == NULL  ||
+                  buf[0] == '\0') {
+                file = NULL;
+                ++files_not_clobbered;
+              } else {
+                char *newline = strchr (buf, '\n');
+                if (newline) {
+                  *newline = '\0';
+                }
+              }
+
+              free (file);
+
+              if (buf[0] == '/') {
+                /* Full path, use it. */
+                file = add (buf, NULL);
+              } else {
+                /* Relative path. */
+                file = cwd  ?  concat (cwd, "/", buf, NULL)  :  add (buf, NULL);
+              }
+
+              check_again = 1;
+              break;
+            }
+          }
+        }
+        break;
+
+      case NMH_CLOBBER_NEVER:
+        if (stat (file, &st) == OK) {
+          /* Keep count of files that would have been clobbered,
+             and return that as process exit status. */
+          advise (NULL, "will not overwrite %s with -clobber never", file);
+          free (file);
+          file = NULL;
+          ++files_not_clobbered;
+        }
+        break;
+    }
+
+    original_file = file;
+  } while (check_again);
+
+  if (cwd) {
+    free (cwd);
+  }
+
+  return file;
+}
+
+/* -clobber support */
+/******************************************************************************/