Backdir: A Programmer's Backup Tool

I write a lot of little C utilities and this is one. When working on some project, every time I reach a major (however trivial it really is) milestone, I like to make a backup copy of the file(s) I'm working on. I make a "backups" directory as a subdirectory of the one I'm working in.

Then I'd do an "ls -la" to see the date and time on the files, and copy the main C file, like

-rw-r--r--    1 root  alan   3928 May  4 00:04 bd.c
to backups/bd_2013-05-04_0004.c

Finally I invested a few hours and wrote a little program that does it for me. It isn't fancy by any means, but it can do this with just a few keystrokes:

d530# ls -lR
total 20
-rw-------  1 alan  alan    34 May  3 17:49 Makefile
-rw-r--r--  1 root  alan  1704 May  4 12:13 backdir.tar.gz
drwxr-xr-x  2 alan  alan  1024 May  4 12:14 backups
-rwxr-xr-x  1 root  alan  7650 May  4 12:09 bd
-rw-r--r--  1 alan  alan  4243 May  4 12:09 bd.c

./backups:
total 56
-rw-------  1 alan  alan    34 May  3 23:59 Makefile_2013-05-03_1749
-rw-r--r--  1 root  alan  1614 May  4 10:08 backdir.tar_2013-05-04_0022.gz
-rw-r--r--  1 root  alan  1704 May  4 12:14 backdir.tar_2013-05-04_1213.gz
-rw-r--r--  1 alan  alan  4041 May  3 23:59 bd_2013-05-03_2359.c
-rw-r--r--  1 alan  alan  3928 May  4 00:04 bd_2013-05-04_0004.c
-rw-r--r--  1 root  alan  4286 May  4 10:08 bd_2013-05-04_1008.c
-rw-r--r--  1 root  alan  4349 May  4 10:57 bd_2013-05-04_1057.c
-rw-r--r--  1 root  alan  4548 May  4 11:32 bd_2013-05-04_1132.c
-rw-r--r--  1 root  alan  4134 May  4 11:37 bd_2013-05-04_1137.c
-rw-r--r--  1 root  alan  4314 May  4 11:48 bd_2013-05-04_1148.c
-rw-r--r--  1 root  alan  4400 May  4 11:51 bd_2013-05-04_1151.c
-rw-r--r--  1 root  alan  4243 May  4 12:09 bd_2013-05-04_1209.c

You see the main files I'm working on, Makefile and bd.c, have been copied to the backups directory with the original date and time as part of the filename. All I did was type bd to run the program. It doesn't copy a file to a backup name if that name already exists, so there's only one Makefile here. The time it takes to run is negligible, so you can type "bd" every few minutes to grab a snapshot. Since it saves mutiple versions, you could fall back to something you did hours or days before if you want. It could also be used for any documents you were writing or any file type, as long as it can be copied1. It backed up my tarballs too. Executables and anything but regular files are skipped.

The program was written under OpenBSD 5.2, and has been tested under 4.7 and 5.0. There are no dependencies as far as I know. The dates and times in the backup flenames are dates and times of the files, not when they were backed up. Notice there's only 1 backup of Makefile because it didn't change.

5/5/2013:
I added #include <time.h> so now it works on Linux too. At least my old Debian 5.03, which has headers and much of the dev stuff that I've come to take for granted under OpenBSD.

5/6/2013:
Should handle spaces in filenames now. A complete afterthought since I never use them.

5/8/2013:
This might actually make it into the OpenBSD ports collection, maybe for 5.4.

5/9/2013:
This works fine from within mc, of course, just type bd wherever you're working.

5/12/2013:
Man page added

5/14/2013:
It's a port now (I had help)

3/26/2014:
It didn't make ports. There was a never-ending stream of requests for changes, some of which would have made it a lot bigger so I stopped responding. It works, has a man page, I use it several times a week. Take it or leave it.

Unpack it, cd into the backdir directory it creates, then type make. Type ./bd to test it, then copy bd to someplace in your path like /usr/local/bin

There's a tarball of the program and makefile here: backdir_20130513.tar.gz, but I'm also putting the C code on this page to look at (this version is mostly accurate.):


/*
 * Copyright (c) 2013 Alan Corey, ab1jx.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR OR HIS RELATIVES BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF MIND, USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 *

    backdir - makes a backup of the files in a directory
    uses the original filenames with mtim appended to give versions

    Mostly for use in programming, doesn't copy to another machine or anything,
    just provides versioning.

    Copies file.c to backups/file_yyyy-mm-dd_hhmm.c, keeping the original
    date and time as part the new filename.

    Alan Corey, AB1JX 5/9/2013
*/

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
/* for linux: */
#include <time.h>

#define NEWMODE 0755  /* mode for backups dir */

int filecount = 0;

void checkdir(void) {  // check for a backups dir, make one if needed
  int rslt;
  struct stat sb;
  rslt = stat("backups", &sb);
  if (rslt == -1) {
    if (errno ==  ENOENT) {
      rslt = mkdir("backups",NEWMODE);
      if (rslt != 0) {
        printf("Error creating backups dir.\n");
        perror("mkdir ");
        exit(1);
      }
    }
  }
}

int fileexists(char *fname) { // like Delphi's a little
  int rslt;
  struct stat sb;
  rslt = stat(fname, &sb);
  if (rslt == 0)
    return 1;
  else
    return 0;
}

void master(void) {     /* slavedriver  :) */
  struct dirent *de;    /* in sys/dirent */
  struct stat sb;
  struct timespec mtim; /* 1 of the 3 time values: modified time */
  time_t mtim_t;
  struct tm mtim_tm;
  char timestr[40];
  size_t siz;
  char oldname[512];
  char oldext[512];
  char nametmp1[512];
  char *p;
  char newname[512];
  char systr[1024];
  int rslt;             /* result, of various things */
  DIR* dirp;
  dirp = opendir(".");
  if (dirp != NULL) {
    while ((de = readdir(dirp)) != NULL) {
      if ((de->d_type & DT_REG) == DT_REG) {  // regular files only
        strncpy(oldname,de->d_name,512);
        if (oldname[0] != '.') {   // ignore . and .. (also hiddens)
          rslt = stat(oldname,&sb);
          if (rslt != 0) {
            printf("Error running stat() on %s\n",oldname);
            perror("stat ");
            closedir(dirp);
            exit(1);
          }
          if ((sb.st_mode & 0111) == 0) {  // nothing executable
            // gets a struct timespec
            mtim = sb.st_mtim;
            // gets the time_t part
            mtim_t = mtim.tv_sec;
            // make a struct tm
            (void) localtime_r(&mtim_t,&mtim_tm);
            // now strftime to format into timestr
            siz = strftime(timestr,40,"%Y-%m-%d_%H%M",&mtim_tm);
            if (siz != 0) {
              strncpy(nametmp1,oldname,512);
              p = strcasestr(nametmp1,".tar.");  // special case
              if (p == NULL)
                p = strrchr(nametmp1,'.');  // find last period
              if (p != NULL) {
                strncpy(oldext,p,512);  // copy extension
                *p = '\0';  // terminate
                rslt = snprintf(newname,512,"backups/%s_%s%s",nametmp1,timestr,\
                  oldext);
              } else {
                rslt = snprintf(newname,512,"backups/%s_%s",nametmp1, timestr);
              }
              if (rslt == -1) {
                printf("Error making new name from %s\n",nametmp1);
                exit(1);
              }
              if (fileexists(newname) == 0) { // may not be new
                // may have spaces and junk, so quote it before system() call:
                rslt = snprintf(systr,1024,"cp %c%s%c %c%s%c",0x22, oldname,\
                  0x22, 0x22, newname, 0x22);
                if (rslt == -1) {  // from snprintf
                  printf("Error making system string 'cp %s %s'",oldname,newname);
                  exit(1);
                }
                rslt = system(systr);
                if (rslt == -1) {  // from system()
                  printf("Error copying %s to %s\n",oldname,newname);
                  exit(1);
                }
                filecount++;
              }  // if backup doesn't exist already
            }  // if siz != 0
          }  // mode ok
        }  // if not . or ..
      }  // if type OK
    }  // while readdir
    rslt = closedir(dirp);
    if (rslt != 0) {
      printf("closedir on current dir failed.\n");
      perror("closedir ");
      exit(1);
    }
  } else {
    printf("opendir on current dir failed.\n");
    perror("opendir ");
    exit(1);
  }
}

int main(void) {
  checkdir();
  master();
  if (filecount == 1) {
    printf("1 file backed up\n");
  } else {
    printf("%i files backed up\n",filecount);
  }
  return 0;
}

1   Some programs may prevent copying the file while they still have it open.


AB1JX / calcs