#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#ifndef NT
#include <dirent.h>
#endif /* NT */
#include <errno.h>

#include <atomic_ops.h>
#include <pipeline_defs.h>

/* Solaris 2.5.1 lacks prototype for utimes() */
#if defined(HAVE_DECL_UTIMES) && ! HAVE_DECL_UTIMES
extern int utimes(char *file, struct timeval *tvp);
#endif

/* lokel yokel's */

static int  fastcopy	   (char *from, char *to, int domv);
static int  my_mv          (char *, char *);
static int  my_cp          (char *, char *);
static int  file_exists    (char *, char *);
static int  restore_files  (uii_connection_t *, atom_finfo_t *);
static int  my_rename      (char *, char *, uii_connection_t *);
static int  rm_tmpfiles    (atom_finfo_t *);
static void rm_backups     (atom_finfo_t *); 

/* Move file (name) to directory (dir).  
 *
 * First a backup copy of (name) is made and then a move
 * operation is performed from a temp dir to (dir).  The 
 * temp dir is defined in the (af) struct.  If any errors 
 * occur the backup copy is restored and the operation is aborted. 
 *
 * This function can backout from any point in the process
 * of moving multiple files into (dir) (eg, RGNET.CURRENTSERIAL
 * and rgnet.db).  It keeps a list of files in (af) indicating
 * which files need to be restored in case of an abort operation.
 *
 * Input:
 *  -target directory for the move operation (dir)
 *  -name of the file to move (name)
 *  -struct to allow messages to be passed to a UII user (uii)
 *  -struct which defines the from dir of the move and also the list 
 *   of files which would need to be restored in case of an abort 
 *   operation (af)
 *
 * Return:
 *  -1 if the operation was a success
 *  -0 otherwise, in which case the old file(s) state have been restored
 */
int atomic_move (char *dir, char *name, uii_connection_t *uii, 
		 atom_finfo_t *af) {
  int made_backup = 1;
  char dbname[BUFSIZE+1], tname[BUFSIZE+1];

  /* sanity check */
  if (dir  == NULL ||
      name == NULL ||
      af   == NULL) {
    trace (ERROR, default_trace, 
	   "atomic_move (): NULL dir (%s), name (%s) or af (%s).\n",
	   ((dir == NULL) ? "NULL" : dir), ((name == NULL) ? "NULL" : name),
	   ((af == NULL) ? "NULL" : "non-NULL"));
    return 0;
  }

  /* target path for the move */
  sprintf (dbname, "%s/%s", dir, name);
  
  /* trace (NORM, default_trace, "atomic_move: dbname (%s)\n", dbname); */

  /* do we need to make a backup copy?  no if no orignal exists */
  switch (file_exists (dir, name)) {
  case 1:  /* make a backup copy of (name) */
    sprintf (tname, "%s/%s.%s", dir, name, BAK_SFX);
    if (!my_rename (dbname, tname, uii)) {
      if (!restore_files (uii, af) ||
	  !rm_tmpfiles (af))
	exit (0);
      return 0;
    }
    break;
  case 0:  /* no file to make a backup from */
    made_backup = 0;
    break;
  default: /* couldn't read the cache directory */
    trace (ERROR, default_trace,
	   "atomic_move (): Insufficient permissions or bad directory path.  Backout name (%s) dir (%s)\n", name, dir);
    if (!restore_files (uii, af) ||
	!rm_tmpfiles (af))
      exit (0);
    return 0;
    break;
  }
  
  /* compile a list of files in case we need to restore state/files.
   * the restore function needs to know if a file does not exist 
   * and thus no backup was made.  see my_backout () for a description
   * of the string format of (file_list) */
  sprintf (&(af->file_list[strlen (af->file_list)]), "%c%s/%s!", 
	   ((made_backup) ? 'y' : 'n'), dir, name);
  sprintf (&(af->tmp_list[strlen (af->tmp_list)]), "%s!", name);

  /* trace (NORM, default_trace, "file_list (%s)\n", af->file_list);
     trace (NORM, default_trace, "tmp_list  (%s)\n", af->tmp_list); */

  /* move the new file from the temp dir to our cache area */
  sprintf (tname, "%s/%s", af->tmp_dir, name);
  if (!my_mv (tname, dbname)) {
    if (!restore_files (uii, af) ||
	!rm_tmpfiles (af))
      exit (0);

    return 0;
  }

  return 1;
}


/* Remove file (dir)/(name).  
 *
 * atomic_del () can be called multiple times to delete
 * a set of files atomically.  atomic_del () makes a 
 * backup of the file to be deleted and keeps track 
 * in the (af) struct.  If any of the files cannot be
 * deleted then all files are restored to their orignal state.
 *
 * Input:
 *  -directory of the file to be removed (dir)
 *  -name of the file to be removed (name)
 *  -struct to allow messages to be passed to a UII user (uii)
 *  -struct which defines the from dir of the move and also the list 
 *   of files which would need to be restored in case of an abort 
 *   operation (af)
 *
 * Return:
 *  -1 if the operation was a success
 *  -0 otherwise, in which case the previously deleted file(s) 
 *     have been restored
 */
int atomic_del (char *dir, char *name, uii_connection_t *uii, atom_finfo_t *af) {
  char fname[BUFSIZE+1], tname[BUFSIZE+1];

  /* do we need to make a backup copy?  no if no orignal exists */
  switch (file_exists (dir, name)) {
  case 1:  /* make a backup copy of (name) */
    sprintf (fname, "%s/%s", dir, name);
    sprintf (tname, "%s/%s.%s", dir, name, BAK_SFX);
    if (!my_cp (fname, tname)) {
      if (!restore_files (uii, af) ||
	  !rm_tmpfiles (af))
	exit (0);
      return 0;
    }
    break;
  case 0:  /* no file to make a backup, we're done */
    return 1;
    break;
  default: /* couldn't read the cache directory */
    trace (ERROR, default_trace,
	   "atomic_move (): Insufficient permissions or bad directory path.  Backout name (%s)\n", name);
    if (!restore_files (uii, af) ||
	!rm_tmpfiles (af))
      exit (0);
    return 0;
    break;
  }
  
  /* compile a list of files in case we need to restore state/files.
   * the restore function needs to know if a file does not exist 
   * and thus no backup was made.  see my_backout () for a description
   * of the string format of (file_list) */
  sprintf (&(af->file_list[strlen (af->file_list)]), "%c%s/%s!", 'y', dir, name);

  /* remove (fname) from our cache area */
  if (remove (fname)) {
    if (!restore_files (uii, af) ||
	!rm_tmpfiles (af))
      exit (0);

    return 0;
  }

  return 1;
}


/* Clean up the back-up files and temp ftp files after
 * an atomic operation.
 *
 * Input:
 *  -pointer to struct with back-up and temp file info (af)
 *
 * Return:
 *  -void
 *
 * Later there will be more to do to prepare for disk crashes
 */
void atomic_cleanup (atom_finfo_t *af) {
  rm_backups  (af);
  rm_tmpfiles (af);
}



/* Rename file (from) to (to).
 *
 * Files (from) and (to) need to be on the same filesystem
 *
 * Input:
 *  -old file name (from)
 *  -new file name (to)
 *  -uii connection for communication purposes (uii)
 *   ie, the user invoked a command from the uii interface
 *
 * Return:
 *  -1 if there were no errors
 *  -0 otherwise
 */
int my_rename (char *from, char *to, uii_connection_t *uii) {

  if (rename (from, to)) {
    trace (ERROR, default_trace, 
	   "my_rename () Unable to rename %s to %s:(%s)\n", 
	   from, to, strerror (errno));
    if (uii != NULL)
      uii_send_data (uii, "unable to rename (%s):(%s)\n", from, strerror (errno));
    
    return 0;
  }

  return 1;
}

#define FILEBUFSIZE 8096

/* a file copy function */
int fastcopy(char *from, char *to, int domv) {
  struct stat sb;
  struct timeval tval[2];
  char buff[FILEBUFSIZE];
  mode_t oldmode;
  register int nread, from_fd, to_fd;

  if (lstat(from, &sb)) {
    trace (ERROR, default_trace, 
	   "fastcopy () Unable to lstat %s:(%s)\n", 
	   from, strerror (errno));
    return (0);
  }

  if ((from_fd = open(from, O_RDONLY, 0)) < 0) {
    trace (ERROR, default_trace, 
	   "fastcopy () unable to open %s:(%s)\n", 
	   from, strerror (errno));
    return (0);
  }

  while ((to_fd =
    open(to, O_CREAT | O_EXCL | O_TRUNC | O_WRONLY, 0)) < 0) {
      if (errno == EEXIST && unlink(to) == 0)
        continue;
      trace (ERROR, default_trace,
           "fastcopy () unable to open %s:(%s)\n",
            to, strerror (errno));
      (void)close(from_fd);
      return (0);
  }
  while ((nread = read(from_fd, buff, FILEBUFSIZE)) > 0)
    if (write(to_fd, buff, nread) != nread) {
      trace (ERROR, default_trace,
           "fastcopy () error writing %s:(%s)\n",
            to, strerror (errno));
      goto err;
    }

  if (nread < 0) {
      trace (ERROR, default_trace,
           "fastcopy () error reading %s:(%s)\n",
            from, strerror (errno));
err:  if (unlink(to))
        trace (ERROR, default_trace,
           "fastcopy () error unlinking %s:(%s)\n",
            to, strerror (errno));
      (void)close(from_fd);
      (void)close(to_fd);
      return (0);
  }
  (void)close(from_fd);

  oldmode = sb.st_mode;
  if (fchown(to_fd, sb.st_uid, sb.st_gid)) {
    if (oldmode & (S_ISUID | S_ISGID)) {
      sb.st_mode &= ~(S_ISUID | S_ISGID);
    }
  }
  if (fchmod(to_fd, sb.st_mode))
    trace (ERROR, default_trace,
      "fastcopy () fchmod %s set mode (was: )%03o :(%s)\n",
       to, oldmode, strerror (errno));

  if (domv) {
    tval[0].tv_sec = sb.st_atime;
    tval[1].tv_sec = sb.st_mtime;
    tval[0].tv_usec = tval[1].tv_usec = 0;
    if (utimes(to, tval))
      trace (ERROR, default_trace,
        "fastcopy () set times %s:(%s)\n",
         to, strerror (errno));
  }

  if (close(to_fd)) {
     trace (ERROR, default_trace,
        "fastcopy () error closing %s:(%s)\n",
         to, strerror (errno));
    return (0);
  }
  return (1);
}

/* Move file (from) to file (to).
 *
 * This function duplicates exactly the normal "mv".
 *
 * Input:
 *  -a fully qualified file name to be moved from (from)
 *  -a fully qualified file name to be move to (to)
 *
 * Return:
 *  -1 if there were no errors
 *  -0 otherwise
 */ 
int my_mv (char *from, char *to) {

  trace (NORM, default_trace, "my_mv from: %s  to: %s\n", from, to);

  /* try a rename first */
  if (!rename(from, to))
    return(1);

  /* if not trying to cross devices, return an error */
  if (errno != EXDEV) {
    trace (ERROR, default_trace, 
	   "my_mv () Unable to move %s to %s:(%s)\n", 
	   from, to, strerror (errno));
    return(0);
  }

  /* going across devices, need to do a copy */
  if (!fastcopy(from, to, 1))
    return(0);

  if (unlink(from)) {
    trace (ERROR, default_trace, 
	   "my_mv () Unable to unlink %s:(%s)\n", 
	   from, strerror (errno));
    return(0);
  }

  return(1);
}

/* Copy file (from) to file (to).
 *
 * This function duplicates exactly the normal "cp".
 *
 * It is expected this routine will be used to make
 * backup copies of files within atomic operations.
 *
 * Input:
 *  -a fully qualified file name to be copied from (from)
 *  -a fully qualified file name to be copied to (to)
 *
 * Return:
 *  -1 if there were no errors
 *  -0 otherwise
 */ 
int my_cp (char *from, char *to) {

  trace (NORM, default_trace, "my_cp from: %s  to: %s\n", from, to);
  return fastcopy (from, to, 0);
}

/* Restore all the files in (af->file_list) to their original state.
 *
 * restore_files () renames the files in (af->file_list) from *.bak
 * to * as part of an atomic file operation.  Files are backed up 
 * by renaming them to *.BAK_SFX and can be restored by renaming
 * without the BAK_SFX suffix.
 *
 * Input:
 *  -struct to allow messages to be passed to a UII user (uii)
 *  -struct which defines the list of files to be restored/renamed
 *   to their original names (af)
 *
 * Return:
 *  -1 if the operation was a success
 *  -0 otherwise
 */
int restore_files (uii_connection_t *uii, atom_finfo_t *af) {
  char tname[BUFSIZE+1];
  char *p, *q;

  if ((p = af->file_list) == NULL)
    return 0;

  for (q = strchr (p, '!'); q != NULL; p = q + 1, q = strchr (p, '!')) {
    *q = '\0';
    
    /* restore the DB cache file if one existed at the start of the operation */
    if (*p++ == 'y') {
      sprintf (tname, "%s.%s", p, BAK_SFX);
      if (!my_rename (tname, p, uii))
	return 0;
    }
    
    *q = '!';
  }

  return 1;
}

/* Remove/cleanup the backup files in (af->file_list).
 *
 * Function is expected to be called after a unsuccessful
 * atomic operation and it is desired to clean up the
 * uneeded files.
 *
 * File names in (af->tmp_list) are not fully qualified.
 * The (af->tmp_dir) is prepended to the names to form
 * a fully qualified file name.
 * 
 * Input:
 *  -list of temp files to be removed (af->tmp_list)
 *
 * Return:
 *  -1 if the entire list of files were removed
 *  -0 otherwise
 */
int rm_tmpfiles (atom_finfo_t *af) {
  char tname[BUFSIZE+1];
  char *p, *q;

  if ((p = af->tmp_list) == NULL)
    return 1;

  for (q = strchr (p, '!'); q != NULL; p = q + 1, q = strchr (p, '!')) {
    *q = '\0';
    
    /* remove the *.db from the tmp directory */
    sprintf (tname, "%s/%s", af->tmp_dir, p);
    remove (tname);

    *q = '!';
  }

  return 1;
}


/* See if (fname) exists in  directory (dir).
 *
 * Input:
 *  -a fully qualified directory path (dir)
 *  -a file name to look for in the directory (fname)
 *
 * Return:
 *  -1 if the file (fname) exists in (dir)
 *  -0 if (fname) does not exist in (dir)
 *  --1 if directory (dir) could not be opened for reading
 */
int file_exists (char *dir, char *fname) {
  int ret_code = 0;
  struct dirent *dp;
  DIR *dirp;

  /* sanity check */
  if (dir == NULL) {
    trace (ERROR, default_trace, 
	   "file_exists (): NULL directory string\n");
    return -1;
  }

  /* loop through the directory entries and see if we can find (fname) */
  if ((dirp = opendir (dir)) != NULL) {
    while ((dp = readdir (dirp)) != NULL)
      if (!strcmp (dp->d_name, fname)) {
	ret_code = 1;
	break;
      }
  }
  /* something went wrong, we couldn't open the directory for reading */
  else {
    trace (ERROR, default_trace, 
	   "file_exists (): Couldn't open dir (%s):(%s)\n", 
	   dir, strerror (errno));
    return -1;
  }

  closedir (dirp);

  return ret_code;
}

/* Remove/cleanup the backup files in (af->file_list).
 *
 * Function is expected to be called after a successful
 * atomic operation in which the back up files are no
 * longer needed.
 *
 * File names are assumed to be fully qualified.
 * 
 * The (af->file_list) string is in the 
 * following format, eg, "y/.../rgnet.db!n/.../RGNET.CURRENTSERIAL!\0"
 * The first character indicates if there was a backup file made.  If no
 * file existed originally then no backup file was made.  The '!' acts
 * as a delimiter between file names and the part in between is the fully
 * qualified file in which a backup was made.
 *
 * Input:
 *  -list of files for which backups were made (af->file_list)
 *
 * Return:
 *  -void
 */
void rm_backups (atom_finfo_t *af) {
  char tname[BUFSIZE], *p, *q;

  if ((p = af->file_list) == NULL)
    return;

  /* remove the backup files from our DB cache area */
  for (q = strchr (p, '!'); q != NULL; p = q + 1, q = strchr (p, '!')) {
    *q = '\0';

    /* remove the DB backup file if one existed at the start of the operation */
    if (*p++ == 'y') {
      sprintf (tname, "%s.%s", p, BAK_SFX);
      remove (tname);
    }

    *q = '!';
  }
}



syntax highlighted by Code2HTML, v. 0.9.1