/**************************************************************************
   ukopp - disk to disk backup and restore program

   Copyright 2007-2014 Michael Cornelison
   source URL:  kornelix.com
   contact: kornelix@posteo.de

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.

***************************************************************************/

#include <dirent.h>
#include <fcntl.h>
#include "zfuncs.h"

#define ukopp_title "ukopp v.4.9"                                          //  version
#define ukopp_license "GNU General Public License v.3"

//  parameters and limits

#define normfont "monospace 8"
#define boldfont "monospace bold 8"
#define BIOCC  512*1024                                                    //  read and write I/O buffer size
#define maxnx 200                                                          //  max include/exclude in job file
#define maxfs 500000                                                       //  max disk files, 500K             v.4.4
#define MODTIMETOLR 2.0                                                    //  tolerance for "equal" mod times  v.4.1
#define nano 0.000000001                                                   //  nanosecond
#define mega (1024*1024)                                                   //  computer million
#define VSEP1 " ("                                                         //  file version appendage format:
#define VSEP2 ")"                                                          //     /xxxx.../filename (nnn)
#define RSEP1 " ("                                                         //  file retention appendage format:
#define RSEP2 ")"                                                          //     /xxxx.../filename (nn,nn)

//  special control files in backup directory

#define BD_UKOPPDIRK  "/ukopp-data"                                        //  directory for special files
#define BD_POOPFILE   "/ukopp-data/poopfile"                               //  file owner & permissions file
#define BD_JOBFILE    "/ukopp-data/jobfile"                                //  backup job file
#define BD_DATETIME   "/ukopp-data/datetime"                               //  backup date-time file

//  GTK GUI widgets

GtkWidget      *mWin, *mVbox, *mScroll, *mLog;                             //  main window
GtkWidget      *fc_widget;                                                 //  file-chooser dialog widget
GtkWidget      *editwidget;                                                //  edit box in file selection dialogs
GdkCursor      *watchcursor;
GdkWindow      *mLogwin;
PangoFontDescription    *monofont;                                         //  fixed-width font

//  file scope variables

int      main_argc;                                                        //  command line args
char     **main_argv;

int      killFlag;                                                         //  tell function to quit
int      pauseFlag;                                                        //  tell function to pause/resume
int      menuLock;                                                         //  menu lock flag
int      Fgui;                                                             //  flag, GUI mode or not
int      clrun;                                                            //  flag, command line 'run' command

char     TFbakfiles[100];                                                  //  /home/user/.ukopp/xxx temp. files
char     TFjobfile[100], TFpoopfile[100];
char     TFdatetime[100], TFformatscript[100];

//  disk devices and mount points

char     diskdev[100][40];                                                 //  /dev/xxx
char     diskdesc[100][60];                                                //  device description
char     diskmp[100][60];                                                  //  mount point, /media/xxxx
int      Ndisk, maxdisk = 99;                                              //  max. disks / partitions

int      devMounted = 0;                                                   //  backup device mounted status
int      ukoppMounted = 0;                                                 //  device was mounted by me
int      ukoppMpoint = 0;                                                  //  mount point was made by me
char     mountdev[40];                                                     //  current mount data
char     mountdirk[200];

//  backup job data

char     BJfilespec[maxfcc];                                               //  backup job file
int      BJnnx;                                                            //  filespec count, 0...maxnx
int      BJrtype[maxnx];                                                   //    1/2/3 = comment/include/exclude
char    *BJfspec[maxnx];                                                   //    filespec (wild)
int      BJretND[maxnx];                                                   //    retention days
int      BJretNV[maxnx];                                                   //    retention versions
int      BJfiles[maxnx];                                                   //    count of matching disk files
double   BJbytes[maxnx];                                                   //    matching files byte count
int      BJvmode;                                                          //  0/1/2/3 = none/incr/full/comp
char     BJdev[40] = "";                                                   //  backup device (maybe)
char     BJdirk[200] = "";                                                 //  backup target directory
int      BJdcc;                                                            //  target directory cc
int      BJvalid = 0;                                                      //  backup job valid flag
int      BJedited = 0;                                                     //  job edited and not saved

const char  *vertype[4] = { "none","incremental","full","compare" };       //  verify types

//  disk files specified in backup job

struct dfrec {                                                             //  disk file record
   char    *file;                                                          //    file: /directory.../filename
   double   size;                                                          //    byte count
   double   mtime;                                                         //    mod time
   int      err;                                                           //    fstat() status
   int      jindx;                                                         //    index to job data BJfspec[] etc.
   int      bindx;                                                         //    index to backup files Brec[]
   int      finc;                                                          //    included in curr. backup
   char     disp;                                                          //    status: new mod unch
};

int      Dnf;                                                              //  actual file count < maxfs
double   Dbytes;                                                           //  disk files, total bytes
dfrec    Drec[maxfs];                                                      //  disk file data records

//  backup files (copies at backup location)

struct   bfrec {                                                           //  backup file record
   char    *file;                                                          //    file: /directory.../filename
   double   size;                                                          //    byte count
   double   mtime;                                                         //    mod time
   int      err;                                                           //    file fstat() status
   int      retND;                                                         //    retention days
   int      retNV;                                                         //    retention versions
   int      lover, hiver;                                                  //    range of previous versions
   int      nexpv;                                                         //    no. expired versions
   int      finc;                                                          //    included in curr. backup
   char     disp;                                                          //    file status: del mod unch
};

int      Bnf;                                                              //  actual file count < maxfs
double   Bbytes;                                                           //  backup files, total bytes
bfrec    Brec[maxfs];                                                      //  backup file data records
                                                                           //  backup file statistics:
int      Cfiles;                                                           //    curr. version file count
double   Cbytes;                                                           //       and total bytes
int      Vfiles;                                                           //    prior version file count
double   Vbytes;                                                           //       and total bytes
int      Pfiles;                                                           //    expired prior versions
double   Pbytes;                                                           //       and total bytes
int      fverrs, fcerrs;                                                   //  file verify and compare errors

//  disk::backup comparison data

int      nnew, ndel, nmod, nunc;                                           //  new, del, mod, unch file counts
int      Mfiles;                                                           //  new + mod + del file count
double   Mbytes;                                                           //  new + mod files, total bytes

//  restore job data

char     RJfrom[300];                                                      //  restore copy-from: /directory/.../
char     RJto[300];                                                        //  restore copy-to: /directory/.../
int      RJnnx;                                                            //  filespec count, 0...maxnx
int      RJrtype[maxnx];                                                   //    record type: include/exclude
char    *RJfspec[maxnx];                                                   //    filespec of include/exclude
int      RJvalid;                                                          //  restore job valid flag

//  restore file data

struct   rfrec {                                                           //  restore file record
   char     *file;                                                         //  restore filespec: /directory.../file
   int      finc;                                                          //  flag, file restore was done
};

rfrec    Rrec[maxfs];                                                      //  restore file data records
int      Rnf;                                                              //  actual file count < maxfs

//  ukopp functions

int initfunc(void *data);                                                  //  GTK init function
void buttonfunc(GtkWidget *, cchar *menu);                                 //  process toolbar button event
void menufunc(GtkWidget *, cchar *menu);                                   //  process menu select event

int getroot(cchar *);                                                      //  get root privileges
int quit_ukopp(cchar *);                                                   //  exit application
int clearScreen(cchar *);                                                  //  clear logging window
int signalFunc(cchar *);                                                   //  kill/pause/resume curr. function
int checkKillPause();                                                      //  test flags: killFlag and pauseFlag

int BDpoop();                                                              //  get all devices and mount points
int chooseTarget(cchar *);                                                 //  choose device and mount point

int BJfileOpen(cchar *);                                                   //  job file open dialog
int BJfileSave(cchar *);                                                   //  job file save dialog
int BJload(cchar *fspec);                                                  //  backup job data <<< file
int BJstore(cchar *fspec);                                                 //  backup job data >>> file
int BJlist(cchar *);                                                       //  backup job >>> log window
int BJedit(cchar *);                                                       //  backup job edit dialog

cchar * parseNXrec(cchar *, int &, char *&, int &, int &);                 //  parse include/exclude record
cchar * parseTarget(cchar *);                                              //  parse target record
cchar * parseVerify(cchar *);                                              //  parse verify record

int Backup(cchar *);                                                       //  backup function
int Synch(cchar *);                                                        //  synchronize function
int Verify(cchar *);                                                       //  verify functions
int Report(cchar *);                                                       //  report functions

int RJedit(cchar *);                                                       //  restore job edit dialog
int RJlist(cchar *);                                                       //  list backup files to be restored
int Restore(cchar *);                                                      //  file restore function

int Format(cchar *);                                                       //  format disk function
int helpFunc(cchar *);                                                     //  help function

int mount(cchar *);                                                        //  mount target device
int unmount(cchar *);                                                      //  unmount target device
int saveScreen(cchar *);                                                   //  save logging window to file
int writeDT();                                                             //  write date-time to temp file
int synch_poop(const char *mode);                                          //  synch owner and permissions data

int dGetFiles();                                                           //  generate backup files from job data
int bGetFiles();                                                           //  get backup file list
int rGetFiles();                                                           //  generate file list from restore job
int setFileDisps();                                                        //  set file disps: new del mod unch
int SortFileList(char *recs, int RL, int NR, char sort);                   //  sort file list in memory
int filecomp(cchar *file1, cchar *file2);                                  //  compare files, directories first

int BJreset();                                                             //  reset backup job file data
int RJreset();                                                             //  reset restore job data
int dFilesReset();                                                         //  reset disk file data and free memory
int bFilesReset();                                                         //  reset backup file data, free memory
int rFilesReset();                                                         //  reset restore file data, free memory

cchar * copyFile(cchar *file1, cchar *file2, int mpf);                     //  copy backup file << >> disk file
cchar * checkFile(cchar *file, int compf, double &bcc);                    //  validate file and return length
cchar * setnextVersion(bfrec &rec);                                        //  backup file: assign next version
cchar * purgeVersions(bfrec &rec, int fkeep);                              //  backup file: delete expired vers.
cchar * deleteFile(cchar *file);                                           //  delete backup file
int setFileVersion(char *file, int vers);                                  //  (re)set filespec version in memory
int do_shell(cchar *pname, cchar *command);                                //  do shell command and echo outputs

//  ukopp menu table

struct menuent {
   char     menu1[20], menu2[40];                                          //  top-menu, sub-menu
   int      lock;                                                          //  lock funcs: no run parallel
   int      (*mfunc)(cchar *);                                             //  processing function
};

#define nmenu  40
struct menuent menus[nmenu] = {
//  top-menu    sub-menu               lock    menu-function
{  "button",   "root",                   1,    getroot        },
{  "button",   "edit job",               1,    BJedit         },
{  "button",   "target",                 1,    chooseTarget   },
{  "button",   "clear",                  0,    clearScreen    },
{  "button",   "run job",                1,    Backup         },
{  "button",   "mount",                  1,    mount          },
{  "button",   "unmount",                1,    unmount        },
{  "button",   "pause",                  0,    signalFunc     },
{  "button",   "resume",                 0,    signalFunc     },
{  "button",   "kill job",               0,    signalFunc     },
{  "button",   "quit",                   0,    quit_ukopp     },
{  "File",     "open job",               1,    BJfileOpen     },
{  "File",     "edit job",               1,    BJedit         },
{  "File",     "list job",               0,    BJlist         },
{  "File",     "save job",               0,    BJfileSave     },
{  "File",     "save job as",            0,    BJfileSave     },
{  "File",     "run job",                1,    Backup         },
{  "File",     "quit",                   0,    quit_ukopp     },
{  "Backup",   "backup only",            1,    Backup         },
{  "Backup",   "synchronize",            1,    Synch          },
{  "Verify",   "incremental",            1,    Verify         },
{  "Verify",   "full",                   1,    Verify         },
{  "Verify",   "compare",                1,    Verify         },
{  "Report",   "get disk files",         1,    Report         },
{  "Report",   "diffs summary",          1,    Report         },
{  "Report",   "diffs by directory",     1,    Report         },
{  "Report",   "diffs by file status",   1,    Report         },
{  "Report",   "diffs by file",          1,    Report         },
{  "Report",   "file versions",          1,    Report         },
{  "Report",   "expired versions",       1,    Report         },
{  "Report",   "list disk files",        1,    Report         },
{  "Report",   "list backup files",      1,    Report         },
{  "Report",   "find files",             1,    Report         },
{  "Report",   "save screen",            0,    saveScreen     },
{  "Restore",  "setup restore job",      1,    RJedit         },
{  "Restore",  "list restore files",     1,    RJlist         },
{  "Restore",  "restore files",          1,    Restore        },
{  "Format",   "format device",          1,    Format         },
{  "Help",     "about",                  0,    helpFunc       },
{  "Help",     "contents",               0,    helpFunc       }  };


//  ukopp main program

int main(int argc, char *argv[])
{
   GtkWidget   *mbar, *tbar;
   GtkWidget   *mFile, *mBackup, *mVerify, *mReport, *mRestore;
   GtkWidget   *mFormat, *mHelp;
   int         ii;

   zinitapp("ukopp",null);                                                 //  setup app directories

   clrun = 0;                                                              //  no command line run command
   *BJfilespec = 0;                                                        //  no backup job file
   Fgui = 1;                                                               //  assume GUI mode

   main_argc = argc;                                                       //  save command line arguments
   main_argv = argv;

   for (ii = 1; ii < argc; ii++)                                           //  process command line
   {
      if (strEqu(argv[ii],"-nogui")) Fgui = 0;                             //  command line mode, no GUI
      else if (strEqu(argv[ii],"-job") && argc > ii+1)                     //  -job jobfile  (load only)
            strcpy(BJfilespec,argv[++ii]);
      else if (strEqu(argv[ii],"-run") && argc > ii+1)                     //  -run jobfile  (load and run)
          { strcpy(BJfilespec,argv[++ii]); clrun++; }
      else  strcpy(BJfilespec,argv[ii]);                                   //  assume a job file and load it
   }

   if (! Fgui) {                                                           //  no GUI                       v.3.6
      mLog = mWin = 0;                                                     //  outputs go to STDOUT
      initfunc(0);                                                         //  run job
      if (devMounted && ukoppMounted) unmount(0);
      return 0;                                                            //  exit
   }

   gtk_init(&argc, &argv);                                                 //  GTK command line options

   mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL);                             //  create main window
   gtk_window_set_title(GTK_WINDOW(mWin),ukopp_title);
   gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER);
   gtk_window_set_default_size(GTK_WINDOW(mWin),800,500);

   mVbox = gtk_box_new(VERTICAL,0);                                        //  vertical packing box
   gtk_container_add(GTK_CONTAINER(mWin),mVbox);                           //  add to main window

   mScroll = gtk_scrolled_window_new(0,0);                                 //  scrolled window
   gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0);                         //  add to main window mVbox

   mLog = gtk_text_view_new();                                             //  text edit window
   gtk_container_add(GTK_CONTAINER(mScroll),mLog);                         //  add to scrolled window

   monofont = pango_font_description_from_string(normfont);                //  set fixed pitch font
   gtk_widget_override_font(mLog,monofont);

   mbar = create_menubar(mVbox);                                           //  create menu bar

   mFile = add_menubar_item(mbar,"File",menufunc);                         //  add menu bar items
      add_submenu_item(mFile,"open job",menufunc);
      add_submenu_item(mFile,"edit job",menufunc);
      add_submenu_item(mFile,"list job",menufunc);
      add_submenu_item(mFile,"save job",menufunc);
      add_submenu_item(mFile,"save job as",menufunc);
      add_submenu_item(mFile,"run job",menufunc);
      add_submenu_item(mFile,"quit",menufunc);
   mBackup = add_menubar_item(mbar,"Backup",menufunc);
      add_submenu_item(mBackup,"backup only",menufunc);
      add_submenu_item(mBackup,"synchronize",menufunc);
   mVerify = add_menubar_item(mbar,"Verify",menufunc);
      add_submenu_item(mVerify,"incremental",menufunc);
      add_submenu_item(mVerify,"full",menufunc);
      add_submenu_item(mVerify,"compare",menufunc);
   mReport = add_menubar_item(mbar,"Report",menufunc);
      add_submenu_item(mReport,"get disk files",menufunc);
      add_submenu_item(mReport,"diffs summary",menufunc);
      add_submenu_item(mReport,"diffs by directory",menufunc);
      add_submenu_item(mReport,"diffs by file status",menufunc);
      add_submenu_item(mReport,"diffs by file",menufunc);
      add_submenu_item(mReport,"file versions",menufunc);
      add_submenu_item(mReport,"expired versions",menufunc);
      add_submenu_item(mReport,"list disk files",menufunc);
      add_submenu_item(mReport,"list backup files",menufunc);
      add_submenu_item(mReport,"find files",menufunc);
      add_submenu_item(mReport,"save screen",menufunc);
   mRestore = add_menubar_item(mbar,"Restore",menufunc);
      add_submenu_item(mRestore,"setup restore job",menufunc);
      add_submenu_item(mRestore,"list restore files",menufunc);
      add_submenu_item(mRestore,"restore files",menufunc);
   mFormat = add_menubar_item(mbar,"Format",menufunc);
      add_submenu_item(mFormat,"format device",menufunc);
   mHelp = add_menubar_item(mbar,"Help",menufunc);
      add_submenu_item(mHelp,"about",menufunc);
      add_submenu_item(mHelp,"contents",menufunc);

   tbar = create_toolbar(mVbox);                                           //  create toolbar and buttons
   gtk_toolbar_set_style(GTK_TOOLBAR(tbar),GTK_TOOLBAR_BOTH);              //  v.4.5

   if (getuid() > 0)
      add_toolbar_button(tbar,"root","get root privileges","root.png",buttonfunc);
   else
      add_toolbar_button(tbar,"root","you have root privileges","root.png",buttonfunc);

   add_toolbar_button(tbar,"target","select backup device or directory","target.png",buttonfunc);
   add_toolbar_button(tbar,"mount","mount target device","mount.png",buttonfunc);
   add_toolbar_button(tbar,"unmount","unmount target device","unmount.png",buttonfunc);
   add_toolbar_button(tbar,"edit job","edit backup job","edit.png",buttonfunc);
   add_toolbar_button(tbar,"run job","run backup job","run.png",buttonfunc);
   add_toolbar_button(tbar,"pause","pause running job","media-pause.png",buttonfunc);
   add_toolbar_button(tbar,"resume","resume running job","media-play.png",buttonfunc);
   add_toolbar_button(tbar,"kill job","kill running job","stop.png",buttonfunc);
   add_toolbar_button(tbar,"clear","clear screen","clear.png",buttonfunc);
   add_toolbar_button(tbar,"quit","quit ukopp","quit.png",buttonfunc);

   gtk_widget_show_all(mWin);                                              //  show all widgets

   G_SIGNAL(mWin,"destroy",quit_ukopp,0);                                  //  connect window destroy event
   G_SIGNAL(mWin,"delete_event",quit_ukopp,0);

   watchcursor = gdk_cursor_new(GDK_WATCH);                                //  v.4.1
   mLogwin = gtk_text_view_get_window(GTK_TEXT_VIEW(mLog),                 //  GDK window for mLog
                                       GTK_TEXT_WINDOW_TEXT);

   g_timeout_add(0,initfunc,0);                                            //  setup initial call from gtk_main()
   gtk_main();                                                             //  process window events
   return 0;
}


//  initial function called from gtk_main() at startup

int initfunc(void *data)
{
   int         ii;
   const char  *home, *appdirk;
   time_t      datetime;

   datetime = time(0);
   wprintf(mLog,"ukopp %s \n",ctime(&datetime));                           //  v.4.1

   if (getuid() == 0)                                                      //  v.4.1
      wprintx(mLog,0,"you have root privileges \n",boldfont);
   else {
      menufunc(null,"Help");                                               //  show version and license
      menufunc(null,"about");
   }

   appdirk = get_zuserdir();
   sprintf(TFbakfiles,"%s/bakfiles",appdirk);                              //  make temp file names
   sprintf(TFpoopfile,"%s/poopfile",appdirk);
   sprintf(TFjobfile,"%s/jobfile",appdirk);
   sprintf(TFdatetime,"%s/datetime",appdirk);
   sprintf(TFformatscript,"%s/formatscript.sh",appdirk);

   menuLock = killFlag = pauseFlag = 0;                                    //  initialize controls

   BJnnx = 4;                                                              //  default backup job data
   for (ii = 0; ii < BJnnx; ii++)
      BJfspec[ii] = (char *) malloc(60);
   home = getenv("HOME");                                                  //  get "/home/username"
   if (! home) home = "/root";

   strcpy(BJfspec[0],"# default backup job");                              //  comment
   sprintf(BJfspec[1],"%s/*",home);                                        //  /home/username/*
   sprintf(BJfspec[2],"%s/*/Trash/*",home);                                //  /home/username/*/Trash/*
   sprintf(BJfspec[3],"%s/.thumbnails/*",home);                            //  /home/username/.thumbnails/*

   BJrtype[0] = 1;                                                         //  comment
   BJrtype[1] = 2;                                                         //  include
   BJrtype[2] = 3;                                                         //  exclude
   BJrtype[3] = 3;                                                         //  exclude

   BJretND[1] = BJretNV[1] = 0;                                            //  no retention specs   v.3.5
   BJvmode = 0;                                                            //  no verify
   BJvalid = 0;                                                            //  not validated

   strcpy(BJdev,"");                                                       //  backup target device (maybe)
   strcpy(BJdirk,"/unknown");                                              //  backup target directory, cc
   BJdcc = strlen(BJdirk);

   strcpy(RJfrom,"/home/");                                                //  file restore copy-from location
   strcpy(RJto,"/home/");                                                  //  file restore copy-to location
   RJnnx = 0;                                                              //  no. restore include/exclude recs
   RJvalid = 0;                                                            //  not validated

   BDpoop();                                                               //  find devices and mount points

   if (*BJfilespec) BJload(BJfilespec);                                    //  load command line job file
   else snprintf(BJfilespec,maxfcc,"%s/ukopp.job",get_zuserdir());         //  or set default job file

   if (clrun) {
      menufunc(null,"File");                                               //  run command line job file
      menufunc(null,"run job");
   }

   return 0;
}


//  process toolbar button events (simulate menu selection)

void buttonfunc(GtkWidget *, cchar *button)
{
   char     button2[20], *pp;

   strncpy0(button2,button,19);
   pp = strchr(button2,'\n');                                              //  replace \n with blank
   if (pp) *pp = ' ';

   menufunc(0,"button");
   menufunc(0,button2);
   return;
}


//  process menu selection event

void menufunc(GtkWidget *, cchar *menu)                                    //  revised for change in GTK behavior    v.4.9
{                                                                          //    starting with Ubuntu 14.04
   int            ii;
   char           command[100];

   for (ii = 0; ii < nmenu; ii++)                                          //  search menu table
      if (strEqu(menu,menus[ii].menu2)) break;                             //  mark sub-menu selection
   if (ii == nmenu) return;                                                //  a top menu, ignore

   if (menuLock && menus[ii].lock) {                                       //  no lock funcs can run parallel
      zmessageACK(mWin,0,"a blocking function is active");
      return;
   }

   if (! menuLock)
      killFlag = pauseFlag = 0;                                            //  reset controls

   snprintf(command,99,"\n""command: %s \n",menu);
   wprintx(mLog,0,command,boldfont);

   if (menus[ii].lock) ++menuLock;
   menus[ii].mfunc(menu);                                                  //  call menu function
   if (menus[ii].lock) --menuLock;

   return;
}


//  get root privileges if password is OK

int getroot(cchar * menu)                                                  //  v.3.8
{
   if (getuid() == 0)                                                      //  v.4.1
      wprintx(mLog,0,"\nyou have root privileges \n",boldfont);
   else
      beroot(main_argc-1,main_argv+1);                                     //  does not return
   return 0;
}


//  quit ukopp

int quit_ukopp(cchar *menu)
{
   int      yn;
   char     logfile[200];

   if (devMounted && ukoppMounted) unmount(0);                             //  v.3.5.2

   if (BJedited && Fgui) {                                                 //  v.4.0
      yn = zmessageYN(mWin,"job file modified, QUIT anyway?");
      if (! yn) return 1;
      BJedited = 0;
   }

   if (mLog) {
      sprintf(logfile,"%s/ukopp.log2",get_zuserdir());                     //  dump window to log file
      wfiledump(mLog,logfile);                                             //  v.3.9
   }

   gtk_main_quit();                                                        //  tell gtk_main() to quit
   return 0;
}


//  clear logging window

int clearScreen(cchar *menu)
{
   wclear(mLog);
   return 0;
}


//  kill/pause/resume current function - called from menu function

int signalFunc(cchar *menu)
{
   if (strEqu(menu,"kill job"))
   {
      if (! menuLock) {
         wprintf(mLog,"\n""ready \n");
         return 0;
      }

      if (killFlag) {
         wprintf(mLog," *** waiting for function to quit \n");
         return 0;
      }

      wprintf(mLog," *** KILL current function \n");
      pauseFlag = 0;
      killFlag = 1;
      return 0;
   }

   if (strEqu(menu,"pause")) {
      pauseFlag = 1;
      return 0;
   }

   if (strEqu(menu,"resume")) {
      pauseFlag = 0;
      return 0;
   }

   else zappcrash("signalFunc: %s",menu);
   return 0;
}


//  check kill and pause flags
//  called periodically from long-running functions

int checkKillPause()
{
   while (pauseFlag)                                                       //  idle loop while paused
   {
      zsleep(0.1);
      zmainloop();                                                         //  process menus
   }

   zmainloop();                                                            //  keep menus working     v.4.0

   if (! killFlag) return 0;                                               //  keep running
   return 1;                                                               //  die now and reset killFlag
}


//  find all disk devices and mount points via Linux utilities

int BDpoop()                                                               //  v.3.3  new udevinfo format
{
   int      ii, jj, contx = 0, pii, pjj, err;
   int      diskf, filsysf, usbf, Nth, Nmounted;
   char     *buff, diskdev1[40], diskdesc1[60], work[100];
   cchar    *pp1, *pp2;

   Ndisk = diskf = filsysf = usbf = 0;

   err = system("udevadm --version >/dev/null 2>&1");                      //  keep up with dynamic Linux  v.3.4
   if (! err) strcpy(work,"udevadm info -e");                              //  new Linux command
   else strcpy(work,"udevinfo -e");                                        //  old Linux command

   while ((buff = command_output(contx,work)))
   {
      if (strnEqu(buff,"P: ",3)) {                                         //  start new device
         if (diskf && filsysf) {                                           //  if last device = formatted disk
            strncpy0(diskdev[Ndisk],diskdev1,39);                          //    save /dev/devid
            strncpy0(diskdesc[Ndisk],diskdesc1,59);                        //    save description
            if (usbf) strcat(diskdesc[Ndisk]," (USB)");                    //    note if USB device
            strcpy(diskmp[Ndisk],"(not mounted)");                         //    mount point TBD
            Ndisk++;
            if (Ndisk == maxdisk) {
               wprintf(mLog," *** exceeded %d devices \n",maxdisk);
               break;
            }
         }

         diskf = filsysf = usbf = 0;                                       //  clear new device flags
      }

      if (strnEqu(buff,"N: ",3)) {
         strcpy(diskdev1,"/dev/");
         strncat(diskdev1,buff+3,14);                                      //  save /dev/devid
      }

      if (strnEqu(buff,"E: ",3)) {
         pp1 = strstr(buff,"ID_TYPE=disk");
         if (pp1) diskf = 1;                                               //  device is a disk
         pp1 = strstr(buff,"ID_FS_TYPE=");
         if (pp1) filsysf = 1;                                             //  device has a file system
         pp1 = strstr(buff,"ID_BUS=usb");
         if (pp1) usbf = 1;                                                //  device is a USB device
         pp1 = strstr(buff,"ID_MODEL=");
         if (pp1) strncpy0(diskdesc1,pp1+9,59);                            //  save description
      }
   }

   if (! Ndisk) {
      wprintf(mLog," no devices found \n");
      return 0;
   }

   contx = Nmounted = 0;

   while ((buff = command_output(contx,"cat /proc/mounts")))               //  get mounted disk info   v.3.2
   {
      if (strnNeq(buff,"/dev/",5)) continue;                               //  not a /dev/xxx record

      Nth = 1;
      pp1 = strField(buff,' ',Nth++);                                      //  parse /dev/xxx /media/xxx
      pp2 = strField(buff,' ',Nth++);

      for (ii = 0; ii < Ndisk; ii++)                                       //  look for matching device
      {
         if (strNeq(pp1,diskdev[ii])) continue;
         strncpy0(diskmp[ii],pp2,59);                                      //  copy its mount point
         strTrim(diskmp[ii]);
         Nmounted++;
         break;
      }
   }

   #define swap(name,ii,jj) {             \
         strcpy(work,name[ii]);           \
         strcpy(name[ii],name[jj]);       \
         strcpy(name[jj],work); }

   for (ii = 0; ii < Ndisk; ii++)                                          //  sort USB and mounted devices
   for (jj = ii + 1; jj < Ndisk; jj++)                                     //    to the top of the list
   {
      pii = pjj = 0;
      if (strstr(diskdesc[ii],"(USB)")) pii += 2;
      if (! strEqu(diskmp[ii],"(not mounted)")) pii += 1;
      if (strstr(diskdesc[jj],"(USB)")) pjj += 2;
      if (! strEqu(diskmp[jj],"(not mounted)")) pjj += 1;
      if (pjj > pii) {
         swap(diskdev,jj,ii);
         swap(diskmp,jj,ii);
         swap(diskdesc,jj,ii);
      }
   }

   return Nmounted;
}


//  choose backup device or enter a target directory
//  update backup job target device and directory

int chooseTarget(cchar *)                                                  //  overhauled   v.3.2
{
   int            ii, zstat;
   char           text[300];
   zdialog        *zd;
   cchar          *instruct = "Select target device or directory";
   const char     *errmess = 0;

   BDpoop();                                                               //  refresh available devices

   zd = zdialog_new("Choose Backup Target",mWin,"OK","cancel",null);
   zdialog_add_widget(zd,"vbox","vb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"label","lab1","vb1",instruct);                   //    select backup device ...
   zdialog_add_widget(zd,"comboE","target","vb1",BJdirk);                  //   [_______________________][v]

   for (ii = 0; ii < Ndisk; ii++)
   {                                                                       //  load combo box with device poop
      strcpy(text,diskdev[ii]);                                            //    /dev/xxx  /media/xxx  (description)
      strncatv(text,299,"   ",diskmp[ii],"   (",diskdesc[ii],")",null);
      zdialog_cb_app(zd,"target",text);
   }

   zdialog_resize(zd,300,0);                                               //  v.4.1
   zdialog_run(zd,0,"mouse");                                              //  run dialog   posn v.4.1
   zstat = zdialog_wait(zd);
   if (zstat != 1) {
      zdialog_free(zd);
      return 0;
   }

   zdialog_fetch(zd,"target",text,299);                                    //  get device or target directory

   zdialog_free(zd);                                                       //  kill dialog

   errmess = parseTarget(text);                                            //  parse selected device, directory
   wprintf(mLog," new target: %s %s \n",BJdev,BJdirk);
   if (errmess) wprintf(mLog," *** %s \n",errmess);

   BJedited++;                                                             //  v.4.1
   return 0;
}


//  job file open dialog - get backup job data from a file
//  return 1 if OK, else 0

int BJfileOpen(cchar *menu)
{
   char        *file;

   file = zgetfile("open backup job","file",BJfilespec,"hidden");          //  get file from user
   if (file) {
      strncpy0(BJfilespec,file,maxfcc-2);
      free(file);
      BJload(BJfilespec);                                                  //  load job file, set BJvalid
   }

   return 0;
}


//  job file save dialog - save backup job data to a file
//  return 1 if OK, else 0

int BJfileSave(cchar *menu)
{
   char        *file;
   int         yn;

   if (! BJvalid && Fgui) {
      yn = zmessageYN(mWin,"backup job has errors, save anyway?");         //  v.3.5
      if (! yn) return 0;
   }

   if (strEqu(menu,"save job")) {
      BJstore(BJfilespec);
      return 0;
   }

   file = zgetfile("save backup job","save",BJfilespec,"hidden");
   if (file) {
      strncpy0(BJfilespec,file,maxfcc-2);
      free(file);
      BJstore(BJfilespec);
   }

   return 0;
}


//  backup job data <<< jobfile

int BJload(cchar *jobfile)
{
   FILE           *fid;
   char           *pp, *fspec, buff[1000];
   const char     *errmess, *jobname;
   int            rtype, days, vers, nerrs = 0;

   snprintf(buff,999,"\n""loading job file: %s \n",jobfile);
   wprintx(mLog,0,buff,boldfont);

   fid = fopen(jobfile,"r");                                               //  open job file
   if (! fid) {
      wprintf(mLog," *** cannot open job file: %s \n",jobfile);
      return 0;
   }

   BJreset();                                                              //  reset all job data

   while (true)
   {
      pp = fgets_trim(buff,999,fid,1);                                     //  read next job record
      if (! pp) break;                                                     //  EOF

      wprintf(mLog," %s \n",buff);                                         //  output

      if (strnEqu(pp,"target",6)) {
         errmess = parseTarget(buff);                                      //  target /dev/xxx /xxxxxxx
         if (errmess) wprintf(mLog," *** %s \n",errmess);
         continue;
      }

      if (strnEqu(pp,"verify",6)) {
         errmess = parseVerify(buff);                                      //  verify xxxxxx
         if (errmess) wprintf(mLog," *** %s \n",errmess);
         if (errmess) nerrs++;
         continue;
      }

      errmess = parseNXrec(buff,rtype,fspec,days,vers);                    //  comment/include/exclude
      if (errmess) wprintf(mLog," *** %s \n",errmess);
      if (errmess) nerrs++;
      BJfspec[BJnnx] = fspec;
      BJrtype[BJnnx] = rtype;
      BJretND[BJnnx] = days;
      BJretNV[BJnnx] = vers;
      BJnnx++;

      if (BJnnx == maxnx) {
         wprintf(mLog," *** max job records exceeded \n");
         nerrs++;
         break;
      }
   }

   fclose(fid);                                                            //  close file

   if (nerrs == 0) {
      BJvalid = 1;                                                         //  job valid if no errors
      jobname = strrchr(BJfilespec,'/') + 1;
      snprintf(buff,100,"%s   %s",ukopp_title,jobname);                    //  put job name in window title  v.4.0
      if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff);
      BJedited = 0;
   }

   return 1;
}


//  backup job data >>> jobfile
//  return 1 if OK, else 0

int BJstore(cchar *jobfile)
{
   FILE     *fid;
   char     buff[100];
   cchar    *jobname;

   fid = fopen(jobfile,"w");                                               //  open file
   if (! fid) {
      wprintf(mLog," *** cannot open job file: %s \n",jobfile);
      return 0;
   }

   for (int ii = 0; ii < BJnnx; ii++)
   {
      if (BJrtype[ii] == 1)
         fprintf(fid,"%s \n",BJfspec[ii]);                                 //  comment

      if (BJrtype[ii] == 2) {
         if (BJretND[ii] + BJretNV[ii] > 0)                                //  include /filespec (nd,nv)
            fprintf(fid,"include %s%s%d,%d%s\n",
                     BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2);
         else  fprintf(fid,"include %s\n",BJfspec[ii]);
      }

      if (BJrtype[ii] == 3)                                                //  exclude /filespec
         fprintf(fid,"exclude %s\n",BJfspec[ii]);
   }

   fprintf(fid,"verify %s \n",vertype[BJvmode]);                           //  verify xxxx
   fprintf(fid,"target %s %s \n",BJdev,BJdirk);                            //  target /dev/xxx /xxxxxxx
   fclose(fid);

   if (strNeq(jobfile,TFjobfile)) {                                        //  if not job file in temp storage,
      jobname = strrchr(jobfile,'/') + 1;
      snprintf(buff,100,"%s   %s",ukopp_title,jobname);                    //  put job name in window title  v.4.0
      if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff);
      BJedited = 0;
   }

   return 1;
}


//  list backup job data to log window

int BJlist(cchar *menu)
{
   wprintf(mLog,"\n backup job file: %s \n",BJfilespec);                   //  job file      v.21

   for (int ii = 0; ii < BJnnx; ii++)
   {
      if (BJrtype[ii] == 1)                                                //  comment
         wprintf(mLog," %s \n",BJfspec[ii]);

      if (BJrtype[ii] == 2) {                                              //  include /filespec (nd,nv)
         if (BJretND[ii] + BJretNV[ii] > 0)
            wprintf(mLog," include %s%s%d days, %d vers%s \n",
                     BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2);
         else  wprintf(mLog," include %s \n",BJfspec[ii]);
      }

      if (BJrtype[ii] == 3)                                                //  exclude /filespec
         wprintf(mLog," exclude %s \n",BJfspec[ii]);
   }

   wprintf(mLog," verify %s \n",vertype[BJvmode]);                         //  verify xxxx
   wprintf(mLog," target %s %s \n",BJdev,BJdirk);                          //  target /dev/xxx /xxxxxx

   return 0;
}


//  edit dialog for backup job data

int  BJedit_fchooser(cchar *dirk);
zdialog *BJedit_fchooser_zd = 0;

char     orgBJdev[40];                                                    //  v.4.1
char     orgBJdirk[200];

int BJedit(cchar *menu)
{
   int BJedit_dialog_event(zdialog *zd, const char *event);

   zdialog     *zd;
   char        text[300];

   zd = zdialog_new("Edit Backup Job",mWin,"browse","clear","done","cancel",null);

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=4");               //  target: /dev/xxx /xxxxx [choose]
   zdialog_add_widget(zd,"label","labtarg","hb1","backup target: ");       //  v.3.5
   zdialog_add_widget(zd,"label","target","hb1","/dev/xxx /xxxxxx");
   zdialog_add_widget(zd,"button","choosetarg","hb1","choose target");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=4");               //  verify: (o) none (o) incr (o) ...
   zdialog_add_widget(zd,"label","labverify","hb2","verify method: ");     //  v.3.5
   zdialog_add_widget(zd,"radio","vnone","hb2","none");
   zdialog_add_widget(zd,"radio","vincr","hb2","incremental","space=10");
   zdialog_add_widget(zd,"radio","vfull","hb2","full","space=10");
   zdialog_add_widget(zd,"radio","vcomp","hb2","compare","space=10");

   zdialog_add_widget(zd,"hsep","sep2","dialog");                          //  edit box for job recs
   zdialog_add_widget(zd,"label","labinex","dialog","Include / Exclude");
   zdialog_add_widget(zd,"frame","frminex","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrwinex","frminex");
   zdialog_add_widget(zd,"edit","edinex","scrwinex");

   snprintf(text,299,"%s %s",BJdev,BJdirk);                                //  stuff current target   v.3.5
   zdialog_stuff(zd,"target",text);

   strncpy0(orgBJdev,BJdev,40);                                            //  save in case of cancel    v.4.1
   strncpy0(orgBJdirk,BJdirk,200);

   zdialog_stuff(zd,"vnone",0);                                            //  stuff verify mode      v.3.5
   zdialog_stuff(zd,"vincr",0);
   zdialog_stuff(zd,"vfull",0);
   zdialog_stuff(zd,"vcomp",0);
   if (BJvmode == 0) zdialog_stuff(zd,"vnone",1);
   if (BJvmode == 1) zdialog_stuff(zd,"vincr",1);
   if (BJvmode == 2) zdialog_stuff(zd,"vfull",1);
   if (BJvmode == 3) zdialog_stuff(zd,"vcomp",1);

   editwidget = zdialog_widget(zd,"edinex");
   wclear(editwidget);                                                     //  stuff include/exclude recs

   for (int ii = 0; ii < BJnnx; ii++)
   {
      if (BJrtype[ii] == 1)                                                //  comment
         wprintf(editwidget,"%s\n",BJfspec[ii]);

      if (BJrtype[ii] == 2) {                                              //  include /filespec (nd,nv)
         if (BJretND[ii] + BJretNV[ii] > 0)
            wprintf(editwidget,"include %s%s%d,%d%s\n",
                        BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2);
         else  wprintf(editwidget,"include %s\n",BJfspec[ii]);
      }

      if (BJrtype[ii] == 3)                                                //  exclude /filespec
         wprintf(editwidget,"exclude %s\n",BJfspec[ii]);
   }

   zdialog_resize(zd,400,400);
   zdialog_run(zd,BJedit_dialog_event,"40/10");                            //  run dialog          posn v.4.1
   zdialog_wait(zd);                                                       //  wait for completion
   return 0;
}


//  job edit dialog event function

int BJedit_dialog_event(zdialog *zd, const char *event)
{
   int         rtype, days, vers, nerrs = 0;
   char        *pp, *fspec, text[300];
   cchar       *errmess = 0, *jobname;
   int         zstat, nn, ftf = 1;

   if (strEqu(event,"choosetarg")) {                                       //  set new target device, directory
      chooseTarget(0);
      snprintf(text,299,"%s   %s",BJdev,BJdirk);
      zdialog_stuff(zd,"target",text);
      return 0;
   }

   zstat = zd->zstat;                                                      //  zdialog complete?
   if (! zstat) {
      BJedited++;                                                          //  no, manual edit was done      v.4.1
      return 0;
   }

   zd->zstat = 0;                                                          //  dialog may continue

   if (zstat == 2) {
      wclear(editwidget);                                                  //  clear include/exclude recs
      BJedited++;                                                          //  v.4.1
      return 0;
   }

   if (zstat == 1) {                                                       //  browse, do file-chooser dialog
      if (! BJedit_fchooser_zd)
         BJedit_fchooser("/home");
      return 0;
   }

   if (BJedit_fchooser_zd)                                                 //  kill file chooser dialog if active
      zdialog_free(BJedit_fchooser_zd);

   if (zstat != 3) {                                                       //  cancel or kill
      zdialog_free(zd);
      strcpy(BJdev,orgBJdev);                                              //  restore original target   v.4.1
      strcpy(BJdirk,orgBJdirk);
      BJedited = 0;
      return 0;
   }

   if (! BJedited) {                                                       //  done
      zdialog_free(zd);                                                    //  no edits made             v.4.1
      return 0;
   }

   BJreset();                                                              //  reset job data

   zdialog_fetch(zd,"target",text,299);                                    //  get device or target directory
   wprintf(mLog," target: %s \n",text);
   errmess = parseTarget(text);                                            //  v.3.5
   if (errmess) wprintf(mLog," *** %s \n",errmess);

   BJvmode = 0;
   zdialog_fetch(zd,"vincr",nn);                                           //  get verify mode     v.3.5
   if (nn) BJvmode = 1;
   zdialog_fetch(zd,"vfull",nn);
   if (nn) BJvmode = 2;
   zdialog_fetch(zd,"vcomp",nn);
   if (nn) BJvmode = 3;

   for (BJnnx = 0; BJnnx < maxnx; )                                        //  get include/exclude records
   {
      pp = wscanf(editwidget,ftf);
      if (! pp) break;

      errmess = parseNXrec(pp,rtype,fspec,days,vers);
      if (errmess) {
         wprintf(mLog,"%s \n *** %s \n",pp,errmess);
         nerrs++;
      }

      BJfspec[BJnnx] = fspec;
      BJrtype[BJnnx] = rtype;
      BJretND[BJnnx] = days;
      BJretNV[BJnnx] = vers;
      BJnnx++;                                                             //  v.4.6
   }

   if (nerrs == 0) BJvalid = 1;                                            //  valid job if no errors

   jobname = strrchr(BJfilespec,'/') + 1;
   snprintf(text,100,"%s   %s (*)",ukopp_title,jobname);                   //  (*) in title for edited job   v.4.0
   gtk_window_set_title(GTK_WINDOW(mWin),text);
   zdialog_free(zd);                                                       //  destroy dialog

   return 0;
}


//  file chooser dialog for backup job edit

int BJedit_fchooser(cchar *dirk)                                           //  v.3.5
{
   int BJedit_fchooser_event(zdialog *zd, const char *event);

   BJedit_fchooser_zd = zdialog_new("Choose Files for Backup",mWin,"Done",null);
   zdialog *zd = BJedit_fchooser_zd;

   zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hb1",0,"expand");
   zdialog_add_widget(zd,"button","incl","hb1","include","space=5");
   zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5");
   zdialog_add_widget(zd,"check","showhf","hb1","Show hidden","space=10");
   zdialog_add_widget(zd,"label","space","hb2",0,"expand");
   zdialog_add_widget(zd,"label","lab1","hb2","Retain old files:   Days: ");
   zdialog_add_widget(zd,"spin","days","hb2","0|9999|1|0");
   zdialog_add_widget(zd,"label","lab2","hb2","   Versions: ");
   zdialog_add_widget(zd,"spin","vers","hb2","0|9999|1|0");
   zdialog_add_widget(zd,"label","space","hb2",0,"space=5");

   fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN);
   GtkWidget *frame = zdialog_widget(zd,"fr1");
   gtk_container_add(GTK_CONTAINER(frame),fc_widget);
   gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk);
   gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1);
   gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),1);

   zdialog_stuff(zd,"showhf",1);
   zdialog_resize(zd,550,500);
   zdialog_run(zd,BJedit_fchooser_event);
   zdialog_wait(zd);
   zdialog_free(zd);
   BJedit_fchooser_zd = 0;
   return 0;
}


int BJedit_fchooser_event(zdialog *zd, const char *event)
{
   GSList         *flist = 0;
   struct stat64   filestat;
   char           *file1, *file2;
   int             ii, err, showhf, days, vers;

   if (strEqu(event,"showhf"))                                             //  show/hide hidden files
   {
      zdialog_fetch(zd,"showhf",showhf);
      gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),showhf);
   }

   if (strEqu(event,"incl") || strEqu(event,"excl"))                       //  include or exclude
   {
      flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget));

      for (ii = 0; ; ii++)                                                 //  process selected files
      {
         file1 = (char *) g_slist_nth_data(flist,ii);
         if (! file1) break;

         file2 = strdupz(file1,2);                                         //  extra space for wildcard
         g_free(file1);

         err = lstat64(file2,&filestat);
         if (err) {
            wprintf(mLog," *** error: %s  file: %s \n",strerror(errno),file2);
            continue;
         }

         if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*");                //  if directory, append wildcard

         zdialog_fetch(zd,"days",days);                                    //  get corresp. retention specs
         zdialog_fetch(zd,"vers",vers);                                    //     from dialog

         if (strEqu(event,"incl")) {                                       //  include /filespec (dd,vv)   v.3.5
            if (days || vers)
               wprintf(editwidget,"include %s%s%d,%d%s\n",file2,RSEP1,days,vers,RSEP2);
            else  wprintf(editwidget,"include %s""\n",file2);
         }
         if (strEqu(event,"excl"))
            wprintf(editwidget,"exclude %s""\n",file2);

         free(file2);
         BJedited++;
      }

      gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget));
      g_slist_free(flist);
   }

   return 0;
}


//  parse and validate a comment/include/exclude record
//  filespec* means a /path.../filename with wildcards
//  # comment (or blank line)
//  include  filespec*  [ (days,vers) ]                                    //  v.3.5
//  exclude  filespec*

cchar * parseNXrec(const char *jobrec, int &rtype, char *&fspec, int &days, int &vers)
{
   int         nn, Nth = 1;
   const char  *pp1, *pp2;

   rtype = days = vers = -1;
   fspec = null;

   pp1 = strField(jobrec,' ',Nth++);

   if (! pp1 || *pp1 == '#') {                                             //  comment or blank line
      rtype = 1;
      if (pp1) fspec = strdup(pp1);
      else fspec = strdup("");
      return 0;
   }

   if (strEqu(pp1,"include")) {                                            //  include /filespec (nd,nv)
      rtype = 2;
      pp1 = jobrec + 7;
      while(*pp1 == ' ') pp1++;
      pp2 = strstr(pp1,RSEP1);
      if (! pp2) {                                                         //  assume no (nd,nv)
         fspec = strdupz(pp1,4);                                           //  v.4.1
         days = vers = 0;
      }
      else {                                                               //  parse (nd,nv)       v.3.5
         nn = sscanf(pp2,RSEP1" %d , %d "RSEP2,&days,&vers);
         if (nn != 2 || days < 0 || days > 9999 || vers < 0 || vers > 9999)
            return "invalid retention spec, use \" (nn,nn)\" ";
         fspec = strdupz(pp1,4);                                           //  v.4.1
         fspec[pp2-pp1] = 0;
      }
      strTrim2(fspec);                                                     //  strip trailing blanks   v.4.0
      pp1 = fspec;
      if (*pp1 == '"') {
         pp1++;                                                            //  allow quoted filespec   v.4.1
         pp2 = pp1 + strlen(pp1);
         if (pp2[-1] != '"') return "filespec closing quote missing";
      }
      if (*pp1 != '/') return "filespec missing /topdir/";
      pp1 = strchr(pp1+1,'/');
      if (!pp1) return "filespec missing /topdir/";
      pp2 = strchr(fspec,'*');
      if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed";
      pp2 = strchr(fspec,'?');
      if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed";
      return 0;
   }

   if (strEqu(pp1,"exclude")) {                                            //  exclude /filespec
      rtype = 3;
      pp1 = jobrec + 7;
      while(*pp1 == ' ') pp1++;
      fspec = strdupz(pp1,4);                                              //  v.4.1
      strTrim2(fspec);                                                     //  strip trailing blanks   v.4.0
      pp1 = fspec;
      if (*pp1 == '"') {                                                   //  allow quoted filespec   v.4.1
         pp2 = pp1 + strlen(pp1);
         if (pp2[-1] != '"') return "filespec closing quote missing";
      }
      return 0;
   }

   return "unrecognized record type";
}


//  parse a verify record: verify xxxxx

cchar * parseVerify(const char *text)                                      //  v.3.5
{
   const char     *pp;

   BJvmode = 0;

   pp = strField(text,' ',1);
   if (! pp || strNeq(pp,"verify")) return "bad verify record";

   pp = strField(text,' ',2);
   if (! pp) return "missing verify type";

   BJvmode = -1;
   if (strEqu(pp,"none")) BJvmode = 0;
   if (strnEqu(pp,"incr",4)) BJvmode = 1;
   if (strEqu(pp,"full")) BJvmode = 2;
   if (strnEqu(pp,"comp",4)) BJvmode = 3;
   if (BJvmode >= 0) return 0;

   BJvmode = 0;
   return "bad verify mode";
}


//  parse a target record and set backup device and directory accordingly
//  format: [ target ] [ /dev/xxx ] [ /directory ]

cchar * parseTarget(const char *text)                                      //  more robust    v.3.5
{
   int            ii, err, cc, yn, Nth = 1;
   int            direxists = 0, dirempty = 0;
   char           ch;
   const char     *pp;
   DIR            *dirf;
   struct dirent  *ppd;
   struct stat    dstat;

   bFilesReset();                                                          //  no files at backup location
   *BJdev = *BJdirk = BJdcc = 0;                                           //  reset target poop

   pp = strField(text,' ',Nth++);

   if (pp && strEqu(pp,"target"))                                          //  skip "target"
      pp = strField(text,' ',Nth++);

   if (pp && strnEqu(pp,"/dev/",5)) {
      strncpy0(BJdev,pp,39);                                               //  have /dev/xxxx
      pp = strField(text,' ',Nth++);
   }

   if (pp && *pp == '/') {
      strncpy0(BJdirk,pp,199);                                             //  have /directory/...
      BJdcc = strlen(BJdirk);
   }

   if (! *BJdev && ! *BJdirk) return "no backup target specified";

   BDpoop();                                                               //  refresh known device data

   if (*BJdev) {                                                           //  if device is specified
      for (ii = 0; ii < Ndisk; ii++)
         if (strEqu(BJdev,diskdev[ii])) break;                             //  look for device
      if (ii == Ndisk) return "target device not found";
   }

   if (*BJdev && ! *BJdirk) {                                              //  get mount point for device
      for (ii = 0; ii < Ndisk; ii++)
         if (strEqu(BJdev,diskdev[ii])) break;
      if (ii < Ndisk && *diskmp[ii] == '/') strcpy(BJdirk,diskmp[ii]);
   }

   if (! *BJdev && *BJdirk) {                                              //  get device for mount point
      for (ii = 0; ii < Ndisk; ii++)
         if (strEqu(BJdirk,diskmp[ii])) break;
      if (ii < Ndisk) strcpy(BJdev,diskdev[ii]);
   }

   if (*BJdev && ! *BJdirk) {                                              //  if no directory specified,
      strcpy(BJdirk,"/media");                                             //    set a default for device
      strcpy(BJdirk+6,BJdev+4);                                            //       e.g. /media/sdf1
   }

   BJdcc = strlen(BJdirk);                                                 //  set target directory cc

   err = stat(BJdirk,&dstat);                                              //  determine if directory
   if (! err && S_ISDIR(dstat.st_mode)) direxists = 1;                     //    exists in file system

   if (direxists) {                                                        //  determine if directory is empty
      dirempty = 1;
      dirf = opendir(BJdirk);
      if (dirf) {
         while (true) {
            ppd = readdir(dirf);
            if (! ppd) break;
            if (ppd->d_name[0] == '.') continue;
            dirempty = 0;
            break;
         }
         closedir(dirf);
      }
   }

   if (direxists) {                                                        //  directory exists
      if (*BJdev) {                                                        //  if device is specified,
         for (ii = 0; ii < Ndisk; ii++)                                    //    find where it is mounted
            if (strEqu(BJdev,diskdev[ii])) break;
         if (ii == Ndisk || *diskmp[ii] != '/') {                          //  device not mounted
            if (dirempty) {
               wprintf(mLog,"target is valid and not mounted \n");         //  mount to existing empty
               return 0;                                                   //    directory is allowed
            }
            else return "target directory is not an empty directory";      //  directory is not empty
         }
         else {                                                            //  device is mounted
            cc = strlen(diskmp[ii]);
            if (! strnEqu(diskmp[ii],BJdirk,cc))
               return "target directory not on target device";             //  somewhere else
            ch = BJdirk[cc];
            if (ch && ch != '/')
               return "target directory not on target device";
            devMounted = 1;                                                //  device mounted at directory
            strcpy(mountdev,BJdev);                                        //  save for later unmount()
            strcpy(mountdirk,BJdirk);
            wprintf(mLog,"target is valid and mounted \n");
            return 0;
         }
      }
      else {                                                               //  device not specified
         wprintf(mLog,"target directory is valid \n");
         return 0;
      }
   }
   else {                                                                  //  directory does not exist
      if (*BJdev) {
         wprintf(mLog,"target is valid and not mounted \n");               //  can be created at mount time
         return 0;
      }
      else {
         yn = zmessageYN(mWin,"target directory does not exist\n"          //  offer to create if missing
                              "create target directory?");                 //  v.4.1
         if (! yn) return "target directory does not exist";
         err = mkdir(BJdirk,0751);
         if (err) return strerror(errno);
         else return "target directory created";
      }
   }
}


//  Delete backup files exceeding age and version limits.
//  Copy new and modified disk files to backup location.

int Backup(cchar *menu)
{
   char        message[100];
   int         vmode = 0, terrs = 0, ii, jj, yn;
   int         upvers = 0, deleted = 0;
   char        disp, *dfile = 0;
   const char  *errmess = 0;
   double      bsecs, bbytes, bspeed;
   double      time0;

   if (! BJvalid) {
      if (Fgui) zmessageACK(mWin,0,"backup job has errors (open or edit)");
      else  wprintf(mLog,"backup job has errors \n");
      return 0;
   }

   if (! mount(0)) return 0;                                               //  validate and mount target  v.3.2

   Report("diffs summary");                                                //  refresh all file data, report diffs

   if (Fgui) {
      yn = zmessageYN(mWin,"backup target: %s %s \n"
                   "%d files (%s) will be copied to \n"
                   "(or deleted from) backup media \n" "continue?",
                     BJdev,BJdirk,Mfiles,formatKBMB(Mbytes,3));            //  confirm backup target   v.23
      if (! yn) return 0;
   }

   snprintf(message,99,"\n""begin backup \n");
   wprintx(mLog,0,message,boldfont);
   wprintf(mLog," files: %d  bytes: %s \n",Mfiles,formatKBMB(Mbytes,3));   //  files and bytes to copy

   if (Mfiles == 0) {
      wprintf(mLog," *** nothing to back-up \n");
      return 0;
   }

   wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk);

   if (strEqu(menu,"backup only")) vmode = 0;                              //  backup command, no auto verify
   if (strEqu(menu,"run job")) vmode = BJvmode;

   wprintf(mLog," assign new version numbers to modified backup files \n"
                " and purge expired versions from backup location \n\n");

   gdk_window_set_cursor(mLogwin,watchcursor);                             //  busy cursor         v.4.1

   for (ii = 0; ii < Bnf; ii++)                                            //  scan files at backup location
   {
      disp = Brec[ii].disp;
      dfile = Brec[ii].file;

      errmess = null;

      if (disp == 'm' || disp == 'd') {                                    //  modified or deleted,
         errmess = setnextVersion(Brec[ii]);                               //  rename to next version number
         Brec[ii].err = -1;                                                //  mark file gone
         if (disp =='m') upvers++;                                         //  update counts
         if (disp =='d') deleted++;
      }

      if (! errmess) errmess = purgeVersions(Brec[ii],1);                  //  purge expired file versions

      if (errmess) {
         wprintf(mLog," %s \n *** %s \n",dfile,errmess);                   //  log error                 v.4.3
         terrs++;
         if (terrs > 100) goto backup_fail;
      }
   }

   wprintf(mLog," %d backup files were assigned new versions \n",upvers);
   wprintf(mLog," %d backup files were deleted \n",deleted);
   wprintf(mLog," %d expired versions (%s) were purged \n\n",Pfiles,formatKBMB(Pbytes,3));
   Pbytes = Pfiles = 0;

   start_timer(time0);                                                     //  start timer
   bbytes = Mbytes;

   BJstore(TFjobfile);                                                     //  copy job file to temp file
   writeDT();                                                              //  create date-time temp file

   wprintf(mLog,-2," %s \n",BD_JOBFILE);
   errmess = copyFile(TFjobfile,BD_JOBFILE,2);                             //  copy job file to backup location
   if (errmess) goto backup_fail;

   wprintf(mLog,-2," %s \n",BD_DATETIME);
   errmess = copyFile(TFdatetime,BD_DATETIME,2);                           //  copy date-time file
   if (errmess) goto backup_fail;

   wprintf(mLog," copying new and modified files from disk to backup location \n\n");

   for (ii = 0; ii < Dnf; ii++)                                            //  scan all disk files
   {
      disp = Drec[ii].disp;
      dfile = Drec[ii].file;
      Drec[ii].finc = 0;                                                   //  not included yet

      if (disp == 'n' || disp == 'm')                                      //  new or modified file
      {
         wprintf(mLog," %s \n",dfile);
         errmess = copyFile(dfile,dfile,2);                                //  copy disk file to backup     v.4.3
         if (errmess) {
            Drec[ii].err = 1;                                              //  copy failed
            wprintf(mLog," *** %s \n",errmess);                            //  log error                    v.4.3
            terrs++;
            if (terrs > 100) goto backup_fail;
         }
         else {                                                            //  copy OK
            Drec[ii].finc = 1;                                             //  set included file flag
         }

         if (checkKillPause()) goto backup_fail;                           //  killed by user
      }

      jj = Drec[ii].bindx;                                                 //  purge last version now
      if (jj >= 0) purgeVersions(Brec[jj],0);                              //  bugfix     v.4.1
   }

   if (terrs) wprintf(mLog," *** %d files had backup errors \n",terrs);

   synch_poop("backup");                                                   //  synch owner and permissions data

   bsecs = get_timer(time0);                                               //  output perf. statistics
   wprintf(mLog," backup time: %.1f secs \n",bsecs);
   bspeed = bbytes/mega/bsecs;
   wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed);
   wprintf(mLog," backup complete \n");

   if (vmode)                                                              //  do verify if requested
   {
      wprintf(mLog,"\n");
      sleep(2);

      if (vmode == 1) Verify("incr");
      else if (vmode == 2) Verify("full");
      else if (vmode == 3) Verify("comp");

      sprintf(message," %d files had backup errors \n",terrs);              //  repeat backup status      v.4.5
      wprintx(mLog,0,message,boldfont);

      terrs = fverrs + fcerrs;
      sprintf(message," %d files had verify errors \n",terrs);              //  add verify status      v.4.5
      wprintx(mLog,0,message,boldfont);
   }

   wprintf(mLog," ready \n");

   if (ukoppMounted) unmount(0);                                           //  leave unmounted

   gdk_window_set_cursor(mLogwin,0);                                       //  normal cursor
   return 0;

backup_fail:
   if (terrs > 100) wprintf(mLog," too many errors, giving up \n");
   else if (errmess) wprintf(mLog," %s \n",errmess);
   wprintx(mLog,0," *** BACKUP FAILED \n",boldfont);

   bFilesReset();
   killFlag = 0;
   gdk_window_set_cursor(mLogwin,0);                                       //  normal cursor
   return 0;
}


//  synchronize disk and backup files                                      //  v.25
//  bi-directional copy of new and newer files

int Synch(cchar *menu)
{
   int         ii, yn, dii, bii, comp;
   char        disp, *dfile = 0;
   time_t      btime, dtime;
   const char  *errmess = 0;

   if (! BJvalid) {
      wprintf(mLog," *** job data has errors \n");
      return 0;
   }

   if (! mount(0)) return 0;                                               //  validate and mount target  v.3.2

   if (Fgui) {
      yn = zmessageYN(mWin,"backup target: %s %s \n continue?",BJdev,BJdirk);   //  confirm backup target
      if (! yn) return 0;
   }
   wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk);

   dGetFiles();                                                            //  get disk files of backup job
   if (bGetFiles() < 0) goto synch_exit;                                   //  get files in backup location
   setFileDisps();                                                         //  compare and set dispositions

   wprintf(mLog,"\n begin synchronize \n");

   gdk_window_set_cursor(mLogwin,watchcursor);                             //  busy cursor         v.4.1

   BJstore(TFjobfile);                                                     //  copy job file to temp file
   writeDT();                                                              //  create date-time temp file

   wprintf(mLog,-2," %s \n",BD_JOBFILE);
   errmess = copyFile(TFjobfile,BD_JOBFILE,2);                             //  copy job file to backup location
   if (errmess) goto synch_exit;

   wprintf(mLog,-2," %s \n",BD_DATETIME);
   errmess = copyFile(TFdatetime,BD_DATETIME,2);                           //  copy date-time file
   if (errmess) goto synch_exit;

   for (ii = 0; ii < Dnf; ii++)                                            //  copy new disk files >> backup loc.
   {
      disp = Drec[ii].disp;
      dfile = Drec[ii].file;
      if (disp != 'n') continue;
      wprintf(mLog," disk >> backup: %s \n",dfile);
      errmess = copyFile(dfile,dfile,2);
      if (errmess) wprintf(mLog," *** %s \n",errmess);
      else Drec[ii].finc = 1;
      if (checkKillPause()) goto synch_exit;                               //  killed by user
   }

   for (ii = 0; ii < Bnf; ii++)                                            //  copy new backup files >> disk
   {                                                                       //  (aka "deleted" disk files)
      disp = Brec[ii].disp;
      dfile = Brec[ii].file;
      if (disp != 'd') continue;
      wprintf(mLog," backup >> disk: %s \n",dfile);
      errmess = copyFile(dfile,dfile,1);
      if (errmess) wprintf(mLog," *** %s \n",errmess);
      else Brec[ii].finc = 1;
      if (checkKillPause()) goto synch_exit;
   }

   dii = bii = 0;

   while ((dii < Dnf) || (bii < Bnf))                                      //  scan disk and backup files parallel
   {
      if ((dii < Dnf) && (bii == Bnf)) comp = -1;
      else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
      else comp = strcmp(Drec[dii].file, Brec[bii].file);

      if (comp < 0) { dii++; continue; }                                   //  next disk file
      if (comp > 0) { bii++; continue; }                                   //  next backup file

      disp = Drec[dii].disp;
      dfile = Drec[dii].file;

      if (disp == 'm')                                                     //  screen for modified status
      {
         btime = int(Brec[bii].mtime);
         dtime = int(Drec[dii].mtime);

         if (btime > dtime) {                                              //  copy newer backup file >> disk
            wprintf(mLog," backup >> disk: %s \n",dfile);
            errmess = copyFile(dfile,dfile,1);
            if (errmess) wprintf(mLog," *** %s \n",errmess);
            else Brec[bii].finc = 1;
         }

         else {                                                            //  copy newer disk file >> backup
            wprintf(mLog," disk >> backup: %s \n",dfile);
            errmess = copyFile(dfile,dfile,2);
            if (errmess) wprintf(mLog," *** %s \n",errmess);
            else Drec[dii].finc = 1;
         }
      }

      dii++;                                                               //  next disk and backup files
      bii++;

      if (checkKillPause()) goto synch_exit;                               //  killed by user
   }

   errmess = null;
   synch_poop("synch");                                                    //  synch owner and permissions data

   Verify("incremental");                                                  //  verify all files copied

synch_exit:
   if (errmess) wprintf(mLog," *** %s \n",errmess);
   wprintf(mLog," ready \n");                                              //  v.3.6
   killFlag = 0;
   gdk_window_set_cursor(mLogwin,0);                                       //  normal cursor
   return 0;
}


//  verify integrity of backup files

int Verify(cchar *menu)
{
   int            ii, vers, comp, vfiles;
   int            dfiles1 = 0, dfiles2 = 0;
   char           filespec[maxfcc];
   const char     *errmess = 0;
   double         secs, dcc1, vbytes, vspeed;
   double         mtime, diff;
   double         time0;
   struct stat64  filestat;

   vfiles = fverrs = fcerrs = 0;
   vbytes = 0.0;
   if (! mount(0)) return 0;                                               //  validate and mount target  v.3.2

   start_timer(time0);

   gdk_window_set_cursor(mLogwin,watchcursor);                             //  busy cursor         v.4.1

   if (strnEqu(menu,"incremental",4))                                      //  verify new/modified files only
   {
      wprintx(mLog,0,"\n""Verify files copied in prior backup or synch \n",boldfont);

      for (ii = 0; ii < Dnf; ii++)                                         //  scan disk file list
      {
         if (! Drec[ii].finc) continue;                                    //  file included in last backup
         strncpy0(filespec,Drec[ii].file,maxfcc-1);
         wprintf(mLog,"  %s \n",filespec);                                 //  output filespec

         errmess = checkFile(filespec,1,dcc1);                             //  compare disk/backup files, get length
         if (errmess) {
            wprintf(mLog,"  *** %s \n\n",errmess);                         //  log and count errors
            if (strstr(errmess,"compare")) fcerrs++;                       //  backup - disk compare failure
            else  fverrs++;
         }

         vfiles++;                                                         //  count files and bytes
         vbytes += dcc1;
         if (fverrs + fcerrs > 100) goto verify_exit;                      //  v.3.7

         if (checkKillPause()) goto verify_exit;                           //  killed by user
      }

      for (ii = 0; ii < Bnf; ii++)                                         //  scan backup file list     v.25
      {
         if (! Brec[ii].finc) continue;                                    //  file included in last backup
         strncpy0(filespec,Brec[ii].file,maxfcc-1);
         wprintf(mLog,"  %s \n",filespec);                                 //  output filespec

         errmess = checkFile(filespec,1,dcc1);                             //  compare disk/backup files, get length
         if (errmess) {
            wprintf(mLog,"  *** %s \n\n",errmess);                         //  log and count errors
            if (strstr(errmess,"compare")) fcerrs++;                       //  backup - disk compare failure
            else  fverrs++;
         }

         vfiles++;                                                         //  count files and bytes
         vbytes += dcc1;
         if (fverrs + fcerrs > 100) goto verify_exit;                      //  v.3.7

         if (checkKillPause()) goto verify_exit;                           //  killed by user
      }
   }

   if (strEqu(menu,"full"))                                                //  verify all files are readable
   {
      wprintx(mLog,0,"\n""Read and verify ALL backup files \n\n",boldfont);

      bGetFiles();                                                         //  get all files at backup location
      wprintf(mLog," %d backup files \n",Bnf);
      if (! Bnf) goto verify_exit;

      for (ii = 0; ii < Bnf; ii++)                                         //  scan backup file list
      {
         strncpy0(filespec,Brec[ii].file,maxfcc-10);                       //  /directory.../filename

         if (Brec[ii].err == 0)
         {                                                                 //  check current file
            wprintf(mLog,-2," %s \n",filespec);
            errmess = checkFile(filespec,0,dcc1);                          //  verify file, get length
            if (errmess) {
               wprintf(mLog,-1," *** %s \n",errmess);                      //  log and count error
               wprintf(mLog,"\n");
               fverrs++;
            }
            vfiles++;                                                      //  count files and bytes
            vbytes += dcc1;
         }

         if (Brec[ii].lover)
         for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++)       //  check previous versions
         {
            setFileVersion(filespec,vers);                                 //  append version if > 0
            wprintf(mLog,-2," %s \n",filespec);
            errmess = checkFile(filespec,0,dcc1);                          //  verify file, get length
            if (errmess) {
               wprintf(mLog,-1," *** %s \n",errmess);                      //  log and count error
               wprintf(mLog,"\n");
               fverrs++;
            }
            vfiles++;                                                      //  count files and bytes
            vbytes += dcc1;
         }

         if (fverrs + fcerrs > 100) goto verify_exit;                      //  v.3.7

         if (checkKillPause()) goto verify_exit;                           //  killed by user
      }
   }

   if (strnEqu(menu,"compare",4))                                          //  compare backup files to disk files
   {
      wprintx(mLog,0,"\n Read and verify ALL backup files. \n",boldfont);
      wprintf(mLog," Compare to correspending disk files (if present). \n\n");

      bGetFiles();                                                         //  get all files at backup location
      wprintf(mLog," %d backup files \n",Bnf);
      if (! Bnf) goto verify_exit;

      for (ii = 0; ii < Bnf; ii++)                                         //  scan backup file list
      {
         strncpy0(filespec,Brec[ii].file,maxfcc-10);                       //  /directory.../filename

         if (Brec[ii].err == 0)
         {                                                                 //  check current file
            comp = 0;
            if (lstat64(filespec,&filestat) == 0) {                        //  corresponding disk file exists
               mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano;
               diff = fabs(mtime - Brec[ii].mtime);                        //  compare disk and backup mod times
               if (diff < MODTIMETOLR) comp = 1;                           //  equal within file system resolution
               dfiles1++;                                                  //  count matching disk names
               dfiles2 += comp;                                            //  count matching mod times
            }

            wprintf(mLog,-2," %s \n",filespec);
            errmess = checkFile(filespec,comp,dcc1);                       //  verify, get length, compare disk
            if (errmess) {
               wprintf(mLog,-1," *** %s \n",errmess);                      //  log and count error
               wprintf(mLog,"\n");
               if (strstr(errmess,"compare")) fcerrs++;                    //  backup - disk compare failure
               else  fverrs++;
            }
            vfiles++;                                                      //  count files and bytes
            vbytes += dcc1;
         }

         if (Brec[ii].lover)
         for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++)       //  check previous versions
         {
            setFileVersion(filespec,vers);                                 //  append version if > 0
            wprintf(mLog,-2," %s \n",filespec);
            errmess = checkFile(filespec,0,dcc1);                          //  verify file, get length
            if (errmess) {
               wprintf(mLog,-1," *** %s \n",errmess);                      //  log and count error
               wprintf(mLog,"\n");
               fverrs++;
            }
            vfiles++;                                                      //  count files and bytes
            vbytes += dcc1;
         }

         if (checkKillPause()) goto verify_exit;                           //  killed by user
         if (fverrs + fcerrs > 100) goto verify_exit;                      //  v.3.7
      }
   }

   wprintf(mLog," backup files: %d  (%s) \n",vfiles,formatKBMB(vbytes,3));
   wprintf(mLog," backup file read errors: %d \n",fverrs);

   if (strnEqu(menu,"incremental",4))
      wprintf(mLog," compare failures: %d \n",fcerrs);

   if (strnEqu(menu,"compare",4)) {
      wprintf(mLog," matching disk names: %d  mod times: %d \n",dfiles1,dfiles2);
      wprintf(mLog," compare failures: %d \n",fcerrs);
   }

   secs = get_timer(time0);
   wprintf(mLog," verify time: %.1f secs \n",secs);
   vspeed = vbytes/mega/secs;
   wprintf(mLog," verify speed: %.2f MB/sec \n",vspeed);

verify_exit:
   if (fverrs + fcerrs) wprintx(mLog,0," *** THERE WERE ERRORS *** \n",boldfont);
   else wprintx(mLog,0," NO ERRORS \n",boldfont);                          //  v.3.9
   wprintf(mLog," ready \n");                                              //  v.3.6
   killFlag = 0;
   gdk_window_set_cursor(mLogwin,0);                                       //  normal cursor
   return 0;
}


//  various kinds of reports

int Report(cchar *menu)
{
   char           *fspec1;
   char           fspec2[200], bfile[maxfcc];
   char           *pslash, *pdirk, ppdirk[maxfcc];
   char           header[100];
   int            ii, kfiles, knew, kdel, kmod;
   int            dii, bii, comp, err;
   double         nbytes, mb1, mb2, fage;
   int            vers, lover, hiver, nexpv;
   int            age, loage, hiage;
   struct tm      tmdt;
   time_t         btime, dtime;
   char           bmod[20], dmod[20];
   const char     *copy;
   struct stat64  filestat;

   //  get all disk files in backup job
   //  report file and byte counts per include and exclude record

   if (strEqu(menu, "get disk files"))
   {
      dGetFiles();                                                         //  get all files on disk

      wprintx(mLog,0,"\n""  files    bytes    filespec    retention (days, vers) \n",boldfont);

      for (ii = 0; ii < BJnnx; ii++) {                                     //  formatted report
         if (BJfspec[ii]) {
            if (BJfiles[ii]) {
               if (BJrtype[ii] == 2)                                       //  include: add retention    v.4.0
                  wprintf(mLog," %6d %9s   %s  (%d, %d) \n",
                     BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii],BJretND[ii],BJretNV[ii]);
               else
                  wprintf(mLog," %6d %9s   %s \n",BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii]);
            }
            else if (BJrtype[ii] > 1) {
               wprintx(mLog,0,"   NO FILES",boldfont);
               wprintf(mLog,"         %s \n",BJfspec[ii]);
            }
            else
               wprintf(mLog,"                    %s \n",BJfspec[ii]);
         }
      }

      wprintf(mLog," %6d %9s   TOTALS \n", Dnf, formatKBMB(Dbytes,3));
      goto report_exit;
   }

   //  report disk / backup differences: new, modified, and deleted files

   if (strEqu(menu, "diffs summary"))
   {
      dGetFiles();
      if (bGetFiles() < 0) goto report_exit;
      setFileDisps();

      wprintf(mLog,"\n disk files: %d  backup files: %d \n",Dnf,Bnf);
      wprintx(mLog,0,"\n Differences between files on disk and backup files: \n",boldfont);
      wprintf(mLog," %6d  disk files not found on backup (new files) \n",nnew);
      wprintf(mLog," %6d  files with different data (modified files) \n",nmod);
      wprintf(mLog," %6d  backup files not found on disk (deleted files) \n",ndel);
      wprintf(mLog," %6d  files with identical data (unchanged files) \n",nunc);
      wprintf(mLog," Total differences: %d files (%s new + modified) \n\n",Mfiles,formatKBMB(Mbytes,3));
      goto report_exit;
   }

   //  report disk / backup differences per directory level

   if (strEqu(menu, "diffs by directory"))
   {
      dGetFiles();
      if (bGetFiles() < 0) goto report_exit;
      setFileDisps();

      SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D');                //  re-sort, directories first
      SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D');

      wprintx(mLog,0,"\n differences by directory \n",boldfont);

      wprintx(mLog,0,"   new   mod   del   bytes   directory \n",boldfont);

      nbytes = kfiles = knew = kmod = kdel = 0;
      dii = bii = 0;

      while ((dii < Dnf) || (bii < Bnf))                                   //  scan disk and backup files parallel
      {
         if ((dii < Dnf) && (bii == Bnf)) comp = -1;
         else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
         else comp = filecomp(Drec[dii].file, Brec[bii].file);

         if (comp > 0) pdirk = Brec[bii].file;                             //  get disk or backup file
         else pdirk = Drec[dii].file;

         pslash = (char *) strrchr(pdirk,'/');                             //  isolate directory
         if (pslash) *pslash = 0;
         if (strNeq(pdirk,ppdirk)) {                                       //  if directory changed, output
            if (kfiles > 0)                                                //    totals from prior directory
               wprintf(mLog," %5d %5d %5d %8s  %s \n",
                       knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk);
            nbytes = kfiles = knew = kmod = kdel = 0;                      //  reset totals
            strcpy(ppdirk,pdirk);                                          //  start new directory
         }
         if (pslash) *pslash = '/';

         if (comp < 0) {                                                   //  unmatched disk file: new
            knew++;                                                        //  count new files
            kfiles++;
            nbytes += Drec[dii].size;
            dii++;
         }

         else if (comp > 0) {                                              //  unmatched backup file
            if (Brec[bii].disp == 'd') {
               kdel++;                                                     //  count deleted files
               kfiles++;
            }
            bii++;
         }

         else if (comp == 0) {                                             //  file present on disk and backup
            if (Drec[dii].disp == 'm') kmod++;                             //  count modified files
            if (Drec[dii].disp == 'n') knew++;                             //  count new files (backup disp is 'v')
            if (Drec[dii].disp != 'u') {
               kfiles++;                                                   //  count unless unchanged
               nbytes += Drec[dii].size;
            }
            dii++;
            bii++;
         }
      }

      if (kfiles > 0) wprintf(mLog," %5d %5d %5d %s  %s \n",               //  totals from last directory
                              knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk);

      SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A');                //  restore straight ascii sort
      SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A');
      goto report_exit;
   }

   //  report disk / backup differences by file status and directory

   if (strEqu(menu, "diffs by file status"))                               //  new v.4.1
   {
      dGetFiles();
      if (bGetFiles() < 0) goto report_exit;
      setFileDisps();

      SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D');                //  re-sort, directories first
      SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D');

      wprintx(mLog,0,"\n new files by directory \n",boldfont);             //  report new files
      wprintx(mLog,0,"  files   bytes   directory \n",boldfont);

      nbytes = knew = 0;
      dii = bii = 0;

      while ((dii < Dnf) || (bii < Bnf))                                   //  scan disk and backup files parallel
      {
         if ((dii < Dnf) && (bii == Bnf)) comp = -1;
         else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
         else comp = filecomp(Drec[dii].file, Brec[bii].file);

         if (comp > 0) pdirk = Brec[bii].file;                             //  get disk or backup file
         else pdirk = Drec[dii].file;

         pslash = (char *) strrchr(pdirk,'/');                             //  isolate directory
         if (pslash) *pslash = 0;
         if (strNeq(pdirk,ppdirk)) {                                       //  if directory changed, output
            if (knew > 0)                                                  //    totals from prior directory
               wprintf(mLog," %6d %8s  %s \n",
                       knew,formatKBMB(nbytes,3),ppdirk);
            nbytes = knew = 0;                                             //  reset totals
            strcpy(ppdirk,pdirk);                                          //  start new directory
         }
         if (pslash) *pslash = '/';

         if (comp < 0) {                                                   //  unmatched disk file: new
            knew++;                                                        //  count new files
            nbytes += Drec[dii].size;
            dii++;
         }

         else if (comp > 0)                                                //  unmatched backup file: deleted
            bii++;

         else if (comp == 0) {                                             //  file present on disk and backup
            if (Drec[dii].disp == 'n') {                                   //  count new files (backup disp is 'v')
               knew++;
               nbytes += Drec[dii].size;
            }
            dii++;
            bii++;
         }
      }

      if (knew > 0) wprintf(mLog," %6d %8s  %s \n",                        //  totals from last directory
                            knew,formatKBMB(nbytes,3),ppdirk);

      wprintx(mLog,0,"\n modified files by directory \n",boldfont);        //  report modified files
      wprintx(mLog,0,"  files   bytes   directory \n",boldfont);

      nbytes = kmod = 0;
      dii = bii = 0;

      while ((dii < Dnf) || (bii < Bnf))                                   //  scan disk and backup files parallel
      {
         if ((dii < Dnf) && (bii == Bnf)) comp = -1;
         else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
         else comp = filecomp(Drec[dii].file, Brec[bii].file);

         if (comp > 0) pdirk = Brec[bii].file;                             //  get disk or backup file
         else pdirk = Drec[dii].file;

         pslash = (char *) strrchr(pdirk,'/');                             //  isolate directory
         if (pslash) *pslash = 0;
         if (strNeq(pdirk,ppdirk)) {                                       //  if directory changed, output
            if (kmod > 0)                                                  //    totals from prior directory
               wprintf(mLog," %6d %8s  %s \n",
                       kmod,formatKBMB(nbytes,3),ppdirk);
            nbytes = kmod = 0;                                             //  reset totals
            strcpy(ppdirk,pdirk);                                          //  start new directory
         }
         if (pslash) *pslash = '/';

         if (comp < 0)                                                     //  unmatched disk file: new
            dii++;

         else if (comp > 0)                                                //  unmatched backup file: deleted
            bii++;

         else if (comp == 0) {                                             //  file present on disk and backup
            if (Drec[dii].disp == 'm') {                                   //  count modified files
               kmod++;
               nbytes += Drec[dii].size;
            }
            dii++;
            bii++;
         }
      }

      if (kmod > 0) wprintf(mLog," %6d %8s  %s \n",                        //  totals from last directory
                            kmod,formatKBMB(nbytes,3),ppdirk);

      wprintx(mLog,0,"\n deleted files by directory \n",boldfont);         //  report deleted files
      wprintx(mLog,0,"  files   bytes   directory \n",boldfont);

      nbytes = kdel = 0;
      dii = bii = 0;

      while ((dii < Dnf) || (bii < Bnf))                                   //  scan disk and backup files parallel
      {
         if ((dii < Dnf) && (bii == Bnf)) comp = -1;
         else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
         else comp = filecomp(Drec[dii].file, Brec[bii].file);

         if (comp > 0) pdirk = Brec[bii].file;                             //  get disk or backup file
         else pdirk = Drec[dii].file;

         pslash = (char *) strrchr(pdirk,'/');                             //  isolate directory
         if (pslash) *pslash = 0;
         if (strNeq(pdirk,ppdirk)) {                                       //  if directory changed, output
            if (kdel > 0)                                                  //    totals from prior directory
               wprintf(mLog," %6d %8s  %s \n",
                       kdel,formatKBMB(nbytes,3),ppdirk);
            nbytes = kdel = 0;                                             //  reset totals
            strcpy(ppdirk,pdirk);                                          //  start new directory
         }
         if (pslash) *pslash = '/';

         if (comp < 0)                                                     //  unmatched disk file: new
            dii++;

         else if (comp > 0) {                                              //  unmatched backup file: deleted
            if (Brec[bii].disp == 'd') {
               kdel++;                                                     //  count deleted files
               nbytes += Brec[bii].size;
            }
            bii++;
         }

         else if (comp == 0) {                                             //  file present on disk and backup
            dii++;
            bii++;
         }
      }

      if (kdel > 0) wprintf(mLog," %6d %8s  %s \n",                        //  totals from last directory
                            kdel,formatKBMB(nbytes,3),ppdirk);

      SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A');                //  restore straight ascii sort
      SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A');
      goto report_exit;
   }

   //  report disk / backup differences by file

   if (strEqu(menu, "diffs by file"))
   {
      dGetFiles();
      if (bGetFiles() < 0) goto report_exit;
      setFileDisps();

      wprintx(mLog,0,"\n Detailed list of disk:backup differences: \n",boldfont);

      snprintf(header,99,"\n %d disk files not found on backup \n",nnew);
      wprintx(mLog,0,header,boldfont);

      for (ii = 0; ii < Dnf; ii++)
      {
         if (Drec[ii].disp != 'n') continue;
         wprintf(mLog,"  %s \n",Drec[ii].file);
      }

      snprintf(header,99,"\n %d backup files not found on disk \n",ndel);
      wprintx(mLog,0,header,boldfont);

      for (ii = 0; ii < Bnf; ii++)
      {
         if (Brec[ii].disp != 'd') continue;
         wprintf(mLog,"  %s \n",Brec[ii].file);
      }

      snprintf(header,99,"\n %d files with different data \n",nmod);
      wprintx(mLog,0,header,boldfont);

      wprintx(mLog,0,"  backup mod date   copy  disk mod date     filespec \n",boldfont);

      dii = bii = 0;

      while ((dii < Dnf) || (bii < Bnf))                                   //  scan disk and backup files parallel
      {                                                                    //  revised       v.25
         if ((dii < Dnf) && (bii == Bnf)) comp = -1;
         else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
         else comp = strcmp(Drec[dii].file, Brec[bii].file);

         if (comp < 0) { dii++; continue; }                                //  next disk file
         if (comp > 0) { bii++; continue; }                                //  next backup file

         if (Drec[dii].disp == 'm')                                        //  screen for modified status
         {
            btime = int(Brec[bii].mtime);                                  //  mod time on backup
            dtime = int(Drec[dii].mtime);                                  //  mod time on disk

            copy = "<<<<";                                                 //  copy direction, disk to backup
            if (btime > dtime) copy = "!!!!";                              //  flag if backup to disk

            tmdt = *localtime(&btime);
            snprintf(bmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900,
                     tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min);

            tmdt = *localtime(&dtime);
            snprintf(dmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900,
                     tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min);

            wprintf(mLog,"  %s  %s  %s  %s \n",bmod,copy,dmod,Drec[dii].file);
         }

         dii++;                                                            //  next disk and backup files
         bii++;
      }

      goto report_exit;
   }

   //  report versions and expired versions per file

   if (strEqu(menu, "file versions"))
   {
      Report("diffs summary");
      if (Bnf < 1) goto report_exit;

      wprintx(mLog,0,"\n  lover hiver  nxver  loage hiage   bytes   expired  filespec \n",boldfont);

      for (ii = 0; ii < Bnf; ii++)
      {
         lover = Brec[ii].lover;
         hiver = Brec[ii].hiver;
         nexpv = Brec[ii].nexpv;
         if (! lover) continue;

         strcpy(bfile,BJdirk);
         strcat(bfile,Brec[ii].file);
         loage = hiage = -1;
         mb1 = mb2 = 0.0;

         for (vers = lover; vers <= hiver; vers++)                         //  loop file versions
         {
            setFileVersion(bfile,vers);
            err = lstat64(bfile,&filestat);                                //  check file exists on backup
            if (err) continue;

            fage = (time(0)-filestat.st_mtime)/24.0/3600.0;                //  file age in days
            age = fage;                                                    //  remove fraction
            if (loage < 0) loage = hiage = age;
            if (age < loage) loage = age;
            if (age > hiage) hiage = age;

            mb1 += filestat.st_size;                                       //  accumulate total bytes
            if (vers < lover + nexpv)                                      //  v.4.0
               mb2 += filestat.st_size;                                    //  and expired version bytes
         }

         wprintf(mLog," %5d %5d %5d   %5d %5d  %8s %8s  %s \n",
                 lover,hiver,nexpv,loage,hiage,formatKBMB(mb1,3),formatKBMB(mb2,3),Brec[ii].file);
      }

      goto report_exit;
   }

   //  report expired file versions (will be purged)

   if (strEqu(menu, "expired versions"))
   {
      Report("diffs summary");
      if (Bnf < 1) goto report_exit;

      wprintx(mLog,0,"\n  expired files (will purge from backup location) \n",boldfont);
      wprintx(mLog,0,"\n  vers   age    bytes   filespec \n",boldfont);

      for (ii = 0; ii < Bnf; ii++)
      {
         lover = Brec[ii].lover;
         hiver = Brec[ii].hiver;
         nexpv = Brec[ii].nexpv;
         if (! nexpv) continue;

         strcpy(bfile,BJdirk);
         strcat(bfile,Brec[ii].file);
         mb1 = 0.0;

         for (vers = lover; vers < lover + nexpv; vers++)                  //  loop expired file versions   v.4.0
         {
            setFileVersion(bfile,vers);
            err = lstat64(bfile,&filestat);                                //  check file exists on backup
            if (err) continue;
            fage = (time(0)-filestat.st_mtime)/24.0/3600.0;                //  age in days, size in MB
            age = fage;
            mb1 = filestat.st_size;
            wprintf(mLog," %5d %5d %8s   %s \n",vers,age,formatKBMB(mb1,3),Brec[ii].file);
         }
      }

      goto report_exit;
   }

   //  list all files in backup job set

   if (strEqu(menu, "list disk files"))
   {
      wprintx(mLog,0," List all files in backup file set: \n",boldfont);

      dGetFiles();
      wprintf(mLog,"   %d files found \n",Dnf);

      for (ii = 0; ii < Dnf; ii++)
         wprintf(mLog," %s \n",Drec[ii].file);

      goto report_exit;
   }

   //  list all files on backup

   if (strEqu(menu, "list backup files"))
   {
      wprintx(mLog,0," List all files at backup location \n",boldfont);
      if (bGetFiles() < 0) goto report_exit;

      for (ii = 0; ii < Bnf; ii++)
      {
         if (Brec[ii].lover) wprintf(mLog," %s (+ vers %d-%d) \n",
                     Brec[ii].file, Brec[ii].lover, Brec[ii].hiver);
         else  wprintf(mLog," %s \n",Brec[ii].file);
      }

      goto report_exit;
   }

   //  search disk and backup file list for match with wild search pattern

   if (strEqu(menu, "find files"))
   {
      wprintx(mLog,0," Find files matching wildcard pattern \n",boldfont);

      dGetFiles();
      bGetFiles();
      if (!(Dnf + Bnf)) goto report_exit;

      fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:","/dir*/file* ");
      if (! fspec1) goto report_exit;
      strncpy0(fspec2,fspec1,199);
      free(fspec1);
      strTrim(fspec2);
      if (! *fspec2) goto report_exit;

      wprintf(mLog,"\n matching disk files: \n");

      for (ii = 0; ii < Dnf; ii++)
         if (MatchWild(fspec2,Drec[ii].file) == 0)
               wprintf(mLog," %s \n",Drec[ii].file);

      wprintf(mLog,"\n matching backup files: \n");

      for (ii = 0; ii < Bnf; ii++)
      {
         if (MatchWild(fspec2,Brec[ii].file) == 0) {
            if (Brec[ii].hiver) wprintf(mLog," %s (+ vers %d-%d) \n",
                        Brec[ii].file, Brec[ii].lover, Brec[ii].hiver);
            else  wprintf(mLog," %s \n",Brec[ii].file);
         }
      }

      goto report_exit;
   }

report_exit:
   wprintf(mLog," ready \n");                                              //  v.3.6
   return 0;
}


//  edit dialog for file restore

int  RJedit_fchooser(cchar *dirk);
zdialog *RJedit_fchooser_zd = 0;

int RJedit(cchar *menu)
{
   int RJedit_dialog_event(zdialog *zd, cchar *event);

   zdialog        *zd;

   wprintf(mLog,"\n Restore files from backup \n");

   if (bGetFiles() < 0) return 0;                                          //  get files in backup location
   wprintf(mLog,"   %d backup files found \n",Bnf);
   if (! Bnf) return 0;

   zd = zdialog_new("restore files from backup",mWin,"browse","done","cancel",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5|expand");
   zdialog_add_widget(zd,"label","labfrom","vb1","copy-from backup");      //  copy-from backup    [_____________]
   zdialog_add_widget(zd,"label","labto","vb1","copy-to disk");            //  copy-to disk        [_____________]
   zdialog_add_widget(zd,"entry","entfrom","vb2",RJfrom);
   zdialog_add_widget(zd,"entry","entto","vb2",RJto);
   zdialog_add_widget(zd,"hsep","hsep1","dialog");
   zdialog_add_widget(zd,"label","labf","dialog","files to restore");      //  files to restore
   zdialog_add_widget(zd,"frame","framef","dialog",0,"expand");            //  scrolling edit window
   zdialog_add_widget(zd,"scrwin","scrf","framef");
   zdialog_add_widget(zd,"edit","editf","scrf");

   editwidget = zdialog_widget(zd,"editf");

   for (int ii = 0; ii < RJnnx; ii++)                                      //  get restore include/exclude recs,
   {                                                                       //   pack into file selection edit box
      if (RJrtype[ii] == 2)
         wprintf(editwidget,"include %s\n",RJfspec[ii]);
      if (RJrtype[ii] == 3)
         wprintf(editwidget,"exclude %s\n",RJfspec[ii]);
   }

   zdialog_resize(zd,400,400);
   zdialog_run(zd,RJedit_dialog_event,"20/20");                            //  run dialog    posn v.4.1
   zdialog_wait(zd);
   return 0;
}


//  dialog completion function
//  get restore job data from dialog widgets and validate

int RJedit_dialog_event(zdialog *zd, cchar *event)
{
   DIR         *pdirk;
   char        *pp, *fspec, rdirk[300];
   int         ftf = 1, cc, rtype, nerrs = 0;
   int         zstat, days, vers;
   const char  *errmess = 0;

   zstat = zd->zstat;
   if (! zstat) return 0;                                                  //  wait for dialog end

   zd->zstat = 0;                                                          //  this dialog continues

   if (RJedit_fchooser_zd)                                                 //  kill file chooser dialog if active
      zdialog_free(RJedit_fchooser_zd);

   if (zstat == 1)
   {                                                                       //  browse button, file-chooser dialog
      zdialog_fetch(zd,"entfrom",RJfrom,299);                              //  copy-from location /dirk/xxx/.../
      strTrim(RJfrom);
      strcpy(rdirk,BJdirk);                                                //  start at /media/xxx/dirk/xxx/
      strncat(rdirk,RJfrom,299);
      RJedit_fchooser(rdirk);                                              //  do file chooser dialog
      return 0;
   }

   if (zstat != 1 && zstat != 2) {                                         //  cancel or destroy
      zdialog_free(zd);
      return 0;
   }

   RJreset();                                                              //  edit done, reset job data

   zdialog_fetch(zd,"entfrom",RJfrom,299);                                 //  copy-from location /dirk/xxx/.../
   strTrim(RJfrom);

   strcpy(rdirk,BJdirk);                                                   //  validate copy-from location
   strncat(rdirk,RJfrom,299);                                              //  /media/xxx/dirk/...
   pdirk = opendir(rdirk);
   if (! pdirk) {
      zmessageACK(0,0,"invalid copy-from location");                       //  v.4.1
      nerrs++;
   }
   else closedir(pdirk);

   cc = strlen(RJfrom);                                                    //  insure '/' at end
   if (RJfrom[cc-1] != '/') strcat(RJfrom,"/");

   zdialog_fetch(zd,"entto",RJto,299);                                     //  copy-to location  /dirk/yyy/.../
   strTrim(RJto);

   pdirk = opendir(RJto);                                                  //  validate copy-to location
   if (! pdirk) {
      zmessageACK(0,0,"invalid copy-to location");                         //  v.4.1
      nerrs++;
   }
   else closedir(pdirk);

   cc = strlen(RJto);                                                      //  insure '/' at end
   if (RJto[cc-1] != '/') strcat(RJto,"/");

   for (RJnnx = 0; RJnnx < maxnx; RJnnx++)                                 //  include/exclude recs from edit box
   {
      pp = wscanf(editwidget,ftf);                                         //  next record from edit widget
      if (! pp) break;
      wprintf(mLog," %s \n",pp);

      errmess = parseNXrec(pp,rtype,fspec,days,vers);                      //  validate include/exclude rec.
      if (errmess) {
         zmessageACK(0,0,"%s \n %s",pp,errmess);                           //  v.4.1
         nerrs++;
      }

      RJrtype[RJnnx] = rtype;                                              //  save job record
      RJfspec[RJnnx] = fspec;
   }

   if (RJnnx == maxnx) {
      zmessageACK(0,0,"max job records exceeded");
      nerrs++;
   }

   if (nerrs == 0) RJvalid = 1;

   if (RJvalid) {                                                          //  all OK
      rGetFiles();                                                         //  get files to restore
      zdialog_free(zd);                                                    //  destroy dialog
   }
   else zd->zstat = 0;                                                     //  errors, keep dialog open     v.4.1

   return 0;
}


//  file chooser dialog for restore job edit

int RJedit_fchooser(cchar *dirk)                                           //  v.3.5
{
   int RJedit_fchooser_event(zdialog *zd, const char *event);

   RJedit_fchooser_zd = zdialog_new("Choose Files to Restore",mWin,"Done",null);
   zdialog *zd = RJedit_fchooser_zd;

   zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand");
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","space","hb1",0,"expand");
   zdialog_add_widget(zd,"check","hidden","hb1","show hidden","space=5");
   zdialog_add_widget(zd,"button","incl","hb1","include","space=5");
   zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5");

   fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN);
   GtkWidget *frame = zdialog_widget(zd,"fr1");
   gtk_container_add(GTK_CONTAINER(frame),fc_widget);

   gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk);
   gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1);

   zdialog_resize(zd,550,500);
   zdialog_run(zd,RJedit_fchooser_event,"50/50");                          //  posn v.4.1
   zdialog_wait(zd);
   zdialog_free(zd);
   RJedit_fchooser_zd = 0;
   return 0;
}


int RJedit_fchooser_event(zdialog *zd, const char *event)
{
   GSList            *flist = 0;
   struct stat64     filestat;
   char              *file1, *file2, rdirk[300];
   int               ii, rdcc, err;

   if (strEqu(event,"hidden")) {                                           //  show/hide hidden files  v.3.7.1
      zdialog_fetch(zd,"hidden",ii);
      gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),ii);
   }

   if (strEqu(event,"incl") || strEqu(event,"excl"))                       //  include or exclude
   {
      strcpy(rdirk,BJdirk);                                                //  copy-from location              v.3.9
      strncat(rdirk,RJfrom,299);                                           //  /media/xxx/dirk/...
      rdcc = strlen(rdirk);

      flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget));

      for (ii = 0; ; ii++)                                                 //  process selected files
      {
         file1 = (char *) g_slist_nth_data(flist,ii);
         if (! file1) break;

         if (! strnEqu(rdirk,file1,rdcc)) {                                //  check file in backup location   v.3.9
            zmessageACK(0,0,"not in copy-from directory: \n %s",file1);    //  v.4.1
            continue;
         }

         err = lstat64(file1,&filestat);
         if (err) {
            zmessageACK(0,0,"error: %s  file: \n %s",strerror(errno),file1);
            continue;
         }

         file2 = strdupz(file1,2);                                         //  extra space for wildcard
         g_free(file1);

         if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*");                //  if directory, append wildcard

         if (strEqu(event,"incl"))
            wprintf(editwidget,"include %s""\n",file2 + BJdcc);            //  omit backup mount point
         if (strEqu(event,"excl"))
            wprintf(editwidget,"exclude %s""\n",file2 + BJdcc);
         free(file2);
      }

      gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget));
      g_slist_free(flist);
   }

   return 0;
}


//  list and validate backup files to be restored

int RJlist(cchar *menu)
{
   int       cc1, cc2, errs = 0;
   char     *file1, file2[maxfcc];

   if (! RJvalid) wprintf(mLog," *** restore job has errors \n");
   if (! Rnf) goto rjlist_exit;

   wprintf(mLog,"\n copy %d files from backup: %s \n",Rnf, RJfrom);
   wprintf(mLog,"    to directory: %s \n",RJto);
   wprintf(mLog,"\n resulting files will be the following: \n");

   cc1 = strlen(RJfrom);                                                   //  from: /dirk/xxx/.../
   cc2 = strlen(RJto);                                                     //    to: /dirk/yyy/.../

   for (int ii = 0; ii < Rnf; ii++)
   {
      file1 = Rrec[ii].file;

      if (! strnEqu(file1,RJfrom,cc1)) {
         wprintf(mLog," *** not within copy-from: %s \n",file1);
         errs++;
         continue;
      }

      strcpy(file2,RJto);
      strcpy(file2+cc2,file1+cc1);
      wprintf(mLog," %s \n",file2);
   }

   if (errs) {
      wprintf(mLog," *** %d errors \n",errs);
      RJvalid = 0;
   }

rjlist_exit:
   wprintf(mLog," ready \n");                                              //  v.3.6
   return 0;
}


//  restore files based on data from restore dialog

int Restore(cchar *menu)
{
   int         ii, nn, ccf;
   char        dfile[maxfcc];
   const char  *errmess = 0;

   if (! RJvalid || ! Rnf) {
      wprintf(mLog," *** restore job has errors \n");
      goto restore_exit;
   }

   nn = zmessageYN(mWin,"Restore %d files from: %s%s \n     to: %s \n"
                   "Proceed with file restore ?",Rnf,BJdirk,RJfrom,RJto);
   if (! nn) goto restore_exit;

   snprintf(dfile,maxfcc-2,"\n""begin restore of %d files to: %s \n",Rnf,RJto);
   wprintx(mLog,0,dfile,boldfont);

   gdk_window_set_cursor(mLogwin,watchcursor);                             //  busy cursor         v.4.1

   ccf = strlen(RJfrom);                                                   //  from: /media/xxx/filespec

   for (ii = 0; ii < Rnf; ii++)
   {
      strcpy(dfile,RJto);                                                  //  to: /destination/filespec
      strcat(dfile,Rrec[ii].file + ccf);
      wprintf(mLog," %s \n",dfile);
      errmess = copyFile(Rrec[ii].file,dfile,1);
      if (errmess) wprintf(mLog," *** %s \n",errmess);
      else Rrec[ii].finc = 1;
      if (checkKillPause()) goto restore_exit;
   }

   synch_poop("restore");                                                  //  synch owner and permissions data

restore_exit:
   wprintf(mLog," ready \n");                                              //  v.3.6
   killFlag = 0;
   gdk_window_set_cursor(mLogwin,0);                                       //  normal cursor
   return 0;
}


//  format disk backup device with vfat or ext2 file system
//  uses existing partitions only - no changes to partition table          //  v.3.3.1

int Format(cchar *menu)
{
   int         ii, jj, zstat, yn, contx = 0;
   char        text[200], device[20], filesys[20], label[20], *crec;
   zdialog     *zd;
   FILE        *fid;

   wprintf(mLog,"\n Format a backup device \n");

   zd = zdialog_new("format backup device",mWin,"start","cancel",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand");             //   backup device   [________][v]
   zdialog_add_widget(zd,"label","labdev","vb1"," backup device");         //   device label    [________]
   zdialog_add_widget(zd,"comboE","entdev","vb2");                         //   file system     [________][v]
   zdialog_add_widget(zd,"label","lablab","vb1","    device label");
   zdialog_add_widget(zd,"entry","entlab","vb2","ukopp");
   zdialog_add_widget(zd,"label","labfs","vb1","      file system");
   zdialog_add_widget(zd,"comboE","entfs","vb2","ext2");

   unmount(0);                                                             //  unmount mounted device
   BDpoop();                                                               //  refresh available devices

   for (ii = 0; ii < Ndisk; ii++)                                          //  load combo box with device
   {
      strcpy(text,diskdev[ii]);                                            //  /dev/xxxx  description
      strncatv(text,199,"  ",diskdesc[ii],null);
      zdialog_cb_app(zd,"entdev",text);
   }

   zdialog_cb_app(zd,"entfs","ext2");                                      //  load combo box with file systems
   zdialog_cb_app(zd,"entfs","vfat");

   zdialog_resize(zd,300,0);
   zdialog_run(zd);                                                        //  run dialog
   zstat = zdialog_wait(zd);
   zdialog_free(zd);
   if (zstat != 1) return 0;

   zdialog_fetch(zd,"entdev",device,19);                                   //  get chosen device and file system
   zdialog_fetch(zd,"entfs",filesys,19);
   zdialog_fetch(zd,"entlab",label,19);

   for (ii = 1; device[ii] > ' '; ii++);                                   //  strip off device description
   if (ii > 19) ii = 19;
   device[ii] = 0;

   yn = zmessageYN(mWin,"device: %s  label: %s  file sys: %s \n"
                   "WARNING: all data will be lost! \n"
                   "Proceed with formatting?",device,label,filesys);
   if (! yn) goto format_exit;

   wprintf(mLog," formatting %s with file system %s \n",device,filesys);

   fid = fopen(TFformatscript,"w");
   if (! fid) {
      wprintf(mLog," *** cannot create format script file \n");
      goto format_exit;
   }

   gdk_window_set_cursor(mLogwin,watchcursor);                             //  busy cursor         v.4.1

   fprintf(fid,"umount %s \n",device);                                     //  unmount /dev/xxxx
   fprintf(fid,"sleep 2 \n");
   if (*filesys == 'v')
      fprintf(fid,"mkfs -t vfat -F 32 -n %s %s \n",label,device);          //  make vfat file system
   if (*filesys == 'e')
      fprintf(fid,"mkfs -t ext2 -L %s %s \n",label,device);                //  or ext2 file system
   fprintf(fid,"exit 0 \n");
   fclose(fid);
   chmod(TFformatscript,0744);

   while ((crec = command_output(contx,TFformatscript)))                   //  v.3.3.1
   {
      zsleep(0.1);                                                         //  throttle a little
      for (ii = jj = 0; crec[jj]; jj++)
      {                                                                    //  get rid of weird characters
         if (crec[jj] < ' ') continue;                                     //    in mkfs output
         crec[ii] = crec[jj];
         ii++;
      }
      crec[ii] = 0;
      wprintf(mLog," format: %s \n",crec);                                 //  print command output
   }

format_exit:
   wprintf(mLog," ready \n");                                              //  v.3.6
   gdk_window_set_cursor(mLogwin,0);                                       //  normal cursor
   return 0;
}


//  display help/about or help/contents

int helpFunc(cchar *menu)
{
   if (strEqu(menu,"about")) {
      wprintf(mLog," %s \n",ukopp_title);
      wprintf(mLog," free software: %s \n",ukopp_license);
   }

   if (strEqu(menu,"contents")) showz_userguide();
   return 0;
}


//  Mount target device. Return 1 if success, else 0.
//  menu caller: menu arg is present
//  internal caller: menu arg is 0

int mount(cchar *menu)                                                     //  more error checking  v.3.5
{
   int            ii, err, cc;
   char           ch, work[300];
   const char     *errmess;
   struct stat    statb;

   bFilesReset();                                                          //  clear file data at backup location
   BDpoop();                                                               //  refresh device data

   snprintf(work,299,"%s %s",BJdev,BJdirk);
   errmess = parseTarget(work);                                            //  target device and directory
   if (errmess) {                                                          //    in conflict with current
      wprintf(mLog," *** %s \n",errmess);                                  //      mount status
      return 0;
   }

   for (ii = 0; ii < Ndisk; ii++)                                          //  see if device is mounted
      if (strEqu(BJdev,diskdev[ii])) break;
   if (ii < Ndisk && *diskmp[ii] == '/') {                                 //  yes
      cc = strlen(diskmp[ii]);
      if (strnEqu(diskmp[ii],BJdirk,cc)) {
         ch = BJdirk[cc];
         if (! ch || ch == '/') {
            devMounted = 1;                                                //  target directory is on device
            if (menu) wprintf(mLog," already mounted \n");
            return 1;
         }
      }
      wprintf(mLog," *** target directory not on device \n");
   }

   err = stat(BJdirk,&statb);                                              //  directory exists?
   if (err && *BJdev) {                                                    //  device but no directory
      snprintf(work,299,"mkdir -p %s",BJdirk);                             //  create mount point
      err = do_shell("mkdir",work);
      if (err) return 0;
      ukoppMpoint++;                                                       //  remember created by me
   }

   if (! err && ! *BJdev) return 1;                                        //  no device, directory OK, use it

   snprintf(work,299,"mount -noatime %s %s",BJdev,BJdirk);                 //  mount device at target directory
   err = do_shell("mount",work);
   if (err) return 0;

   ukoppMounted++;                                                         //  remember mounted by me
   devMounted = 1;
   strcpy(mountdev,BJdev);                                                 //  save mount poop
   strcpy(mountdirk,BJdirk);
   return 1;
}


//  unmount target device

int unmount(cchar *menu)                                                   //  revised   v.3.5.1
{
   int            err;
   char           work[200];
   struct stat    statb;

   bFilesReset();                                                          //  no files at backup location

   sleep(1);
   if (*mountdirk) snprintf(work,199,"umount %s",mountdirk);               //  unmount unconditionally
   else  snprintf(work,199,"umount %s",BJdev);
   do_shell("umount",work);

   sleep(1);
   err = stat(mountdirk,&statb);                                           //  remove directory
   if (! err && ukoppMpoint) {                                             //    only if it exists
      snprintf(work,199,"rmdir %s",mountdirk);                             //      and created by me
      do_shell("rmdir",work);
   }

   devMounted = ukoppMounted = ukoppMpoint = 0;
   *mountdev = *mountdirk = 0;
   BDpoop();                                                               //  refresh device data
   return 0;
}


//  save logging window as text file

int saveScreen(cchar *menu)
{
   wfilesave(mLog);
   return 0;
}


//  backup helper function
//  write date and time to temp file

int writeDT()
{
   time_t      dt1;
   char        *dt2;
   FILE        *fid;
   int         cc;

   time(&dt1);
   dt2 = ctime(&dt1);                                                      //  get string date-time
   cc = strlen(dt2);
   if (cc && (dt2[cc-1] == '\n')) dt2[cc-1] = 0;                           //  save without trailing \n

   fid = fopen(TFdatetime,"w");
   if (! fid) zappcrash("cannot open scratch file %s",TFdatetime);

   fprintf(fid,"%s \n",dt2);
   fclose(fid);
   return 0;
}


//  synchronize owner and permissions data using poopfile at backup location        v.26
//   - for files copied backup >> disk, set owner and permissions from poopfile
//   - refresh poopfile data from disk files
//  mode is "backup" "restore" or "synch"

int synch_poop(const char *mode)
{
   int            ii, err, nn, uid, gid, perms;
   int            cc, ccf, cct;
   char           file[maxfcc], file2[maxfcc];
   char           dirk[maxfcc], pdirk[maxfcc];
   char           *pp, poopfile[100];
   const char     *errmess = 0;
   FILE           *fid;
   struct stat64  dstat;

   if (strEqu(mode,"synch"))                                               //  set poop for updated disk files
   {
      strcpy(poopfile,BJdirk);
      strcat(poopfile,BD_POOPFILE);
      fid = fopen(poopfile,"r");                                           //  open poopfile
      if (! fid) {
         wprintf(mLog," *** no owner/permissions file: %s \n",poopfile);
         return 0;
      }

      ii = 0;

      while (true)                                                         //  read poopfile records
      {
         nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file);         //  uid, gid, perms, file or directory
         if (nn == EOF) break;
         if (nn != 4) continue;

         cc = strlen(file);

         while (ii < Bnf)                                                  //  match poopfile file or directory
         {                                                                 //    to backup files copied to disk
            nn = strncmp(Brec[ii].file,file,cc);                           //  (logic assumes ascii sort)
            if (nn >= 0) break;
            ii++;
         }

         if (ii == Bnf) break;                                             //  EOL
         if (nn > 0) continue;                                             //  file not in backup file list
         if (Brec[ii].finc == 0) continue;                                 //  file not copied to disk

         wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file);
         err = chown(file,uid,gid);
         if (err) wprintf(mLog," *** error: %s \n",strerror(errno));
         err = chmod(file,perms);
         if (err) wprintf(mLog," *** error: %s \n",strerror(errno));
      }

      fclose(fid);
   }

   if (strEqu(mode,"restore"))                                             //  set poop for restored disk files
   {
      strcpy(poopfile,BJdirk);
      strcat(poopfile,BD_POOPFILE);
      fid = fopen(poopfile,"r");
      if (! fid) {
         wprintf(mLog," *** no owner/permissions file: %s \n",poopfile);
         return 0;
      }

      ccf = strlen(RJfrom);
      cct = strlen(RJto);
      ii = 0;

      while (true)
      {
         nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file);
         if (nn == EOF) break;
         if (nn != 4) continue;

         cc = strlen(file);
         if (cc <= ccf) continue;

         while (ii < Rnf)
         {
            nn = strncmp(Rrec[ii].file,file,cc);
            if (nn >= 0) break;
            ii++;
         }

         if (ii == Rnf) break;
         if (nn > 0) continue;
         if (Rrec[ii].finc == 0) continue;

         strcpy(file2,RJto);                                               //  offset restore 'from' and 'to' paths
         strcpy(file2 + cct, file + ccf);

         wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file2);
         err = chown(file2,uid,gid);
         if (err) wprintf(mLog," *** error: %s \n",strerror(errno));
         err = chmod(file2,perms);
         if (err) wprintf(mLog," *** error: %s \n",strerror(errno));
      }

      fclose(fid);
   }

   if (strEqu(mode,"backup") || strEqu(mode,"synch"))                      //  make new poop file from disk files
   {
      fid = fopen(TFpoopfile,"w");
      if (! fid) zappcrash("cannot open temp file %s",TFpoopfile);

      *pdirk = 0;                                                          //  no prior directory

      for (ii = 0; ii < Dnf; ii++)
      {
         strcpy(dirk,Drec[ii].file);                                       //  next file on disk
         pp = dirk;

         while (true)                                                      //  set directory owner & permissions
         {
            pp = strchr(pp+1,'/');                                         //  next (last) directory level
            if (! pp) break;
            cc = pp - dirk + 1;                                            //  cc incl. '/'
            if (strncmp(dirk,pdirk,cc) == 0) continue;                     //  matches prior, skip

            *pp = 0;                                                       //  terminate this directory level

            err = lstat64(dirk,&dstat);                                    //  get owner and permissions   v.3.0
            if (err) {
               wprintf(mLog," *** error: %s  file: %s \n",strerror(errno),dirk);
               break;
            }

            dstat.st_mode = dstat.st_mode & 0777;

            fprintf(fid,"%4d:%4d %3o %s/\n",                               //  output uid:gid perms directory/
                    dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk);

            *pp = '/';                                                     //  restore '/'
         }

         strcpy(pdirk,dirk);                                               //  prior = this directory

         strcpy(file,Drec[ii].file);                                       //  disk file, again

         err = lstat64(file,&dstat);                                       //  get owner and permissions    v.3.0
         if (err) {
            wprintf(mLog," *** error: %s  file: %s \n",strerror(errno),file);
            continue;
         }

         dstat.st_mode = dstat.st_mode & 0777;

         fprintf(fid,"%4d:%4d %3o %s\n",                                   //  output uid:gid perms file
                 dstat.st_uid, dstat.st_gid, dstat.st_mode, file);
      }

      fclose(fid);

      errmess = copyFile(TFpoopfile,BD_POOPFILE,2);                        //  copy file owner/permissions file
      if (errmess) wprintf(mLog," *** poopfile error: %s \n",errmess);
   }

   return 0;
}


//  get all disk files specified by include/exclude records
//  save in Drec[] array

int dGetFiles()
{
   const char     *fsp, *psep2;
   char           *fspec, *pp, *psep1;
   int            ftf, wstat, cc, err, dups;
   int            rtype, ii, jj, st, nfiles;
   int            fcc, vers;
   double         nbytes;
   struct stat64  filestat;

   dFilesReset();
   wprintx(mLog,0,"\n""generating backup file set \n",boldfont);

   for (ii = 0; ii < BJnnx; ii++)                                          //  process include/exclude recs
   {
      BJfiles[ii] = 0;                                                     //  initz. include/exclude rec stats
      BJbytes[ii] = 0.0;

      if (! BJfspec[ii]) continue;                                         //  v.4.6

      if (*BJfspec[ii] == '"') {                                           //  v.4.1
         fspec = strdup(BJfspec[ii]+1);                                    //  unquote quoted filespec
         pp = strrchr(fspec,'"');
         if (pp) *pp = 0;
      }
      else fspec = strdup(BJfspec[ii]);

      rtype = BJrtype[ii];

      if (rtype == 2 || rtype == 3)                                        //  include or exclude filespec
      {
         err = stat64(fspec,&filestat);
         if (! err && S_ISDIR(filestat.st_mode)) {                         //  if directory, append /*   v.4.1
            cc = strlen(fspec) - 1;                                        //  (BJfspec has the extra length)
            if (fspec[cc] != '/') cc++;
            strcpy(fspec+cc,"/*");
         }
      }

      if (rtype == 2)                                                      //  include filespec
      {
         ftf = 1;

         while (1)
         {
            fsp = SearchWild(fspec,ftf);                                   //  find matching files
            if (! fsp) break;

            Drec[Dnf].file = strdup(fsp);

            err = lstat64(fsp,&filestat);                                  //  check accessibility
            if (! err) {
               Drec[Dnf].err = 0;
               if (! S_ISREG(filestat.st_mode) &&                          //  reg. files + symlinks only  v.3.0
                   ! S_ISLNK(filestat.st_mode)) continue;
            }
            else Drec[Dnf].err = errno;                                    //  save file error status

            fcc = strlen(fsp);
            psep1 = (char *) strstr(fsp+fcc-10,VSEP1);                     //  look for file version   v.3.2
            if (psep1) {                                                   //  (char *) fix gcc error  v.3.4.1
               vers = 0;
               st = convSI(psep1+2,vers,&psep2);                           //  if format not valid, take
               if (st < 2) vers = 1;                                       //    as non-versioned file
               if (strNeq(psep2,VSEP2)) vers = 0;
               if (*(psep2+1)) vers = 0;                                   //  VSEP2 must be at end
               if (vers) {
                  wprintf(mLog," *** omit versioned file: %s \n",fsp);
                  continue;
               }
            }

            Drec[Dnf].jindx = ii;                                          //  save pointer to include record
            Drec[Dnf].size = filestat.st_size;                             //  save file size
            Drec[Dnf].mtime = filestat.st_mtime                            //  save last mod time
                            + filestat.st_mtim.tv_nsec * nano;             //    (nanosec resolution)
            if (err) Drec[Dnf].size = Drec[Dnf].mtime = 0;                 //  inaccessible file
            Drec[Dnf].finc = 0;                                            //  not copied yet
            Drec[Dnf].bindx = -1;                                          //  no link to backup record yet v.4.0

            BJfiles[ii]++;                                                 //  count included files and bytes
            BJbytes[ii] += Drec[Dnf].size;

            if (++Dnf == maxfs) {
               wprintf(mLog," *** max files exceeded \n");
               goto errret;                                                //  v.4.4
            }
         }
      }

      if (rtype == 3)                                                      //  exclude filespec
      {
         for (jj = 0; jj < Dnf; jj++)                                      //  check all included files (SO FAR)
         {
            if (! Drec[jj].file) continue;
            wstat = MatchWild(fspec,Drec[jj].file);
            if (wstat != 0) continue;
            BJfiles[ii]--;                                                 //  un-count excluded file and bytes
            BJbytes[ii] -= Drec[jj].size;
            free(Drec[jj].file);                                          //  clear file data entry
            Drec[jj].file = 0;
            Drec[jj].err = 0;
         }
      }

      free(fspec);                                                         //  v.4.1
   }                                                                       //  end of include/exclude recs

   for (ii = 0; ii < Dnf; ii++)                                            //  list and remove error files
   {                                                                       //  (after excluded files removed)
      if (Drec[ii].err) {
         wprintf(mLog," *** %s  omit: %s \n",strerror(Drec[ii].err),Drec[ii].file);
         jj = Drec[ii].jindx;
         BJfiles[jj]--;                                                    //  un-count file and bytes
         BJbytes[jj] -= Drec[ii].size;
         free(Drec[ii].file);
         Drec[ii].file = 0;
      }
   }

   ii = jj = 0;                                                            //  repack file arrays after deletions
   while (ii < Dnf)
   {
      if (Drec[ii].file == 0) ii++;
      else {
         if (ii > jj) {
            if (Drec[jj].file) free(Drec[jj].file);
            Drec[jj] = Drec[ii];
            Drec[ii].file = 0;
         }
         ii++;
         jj++;
      }
   }

   Dnf = jj;                                                               //  final file count in backup set

   Dbytes = 0.0;
   for (ii = 0; ii < Dnf; ii++) Dbytes += Drec[ii].size;                   //  compute total bytes from files

   nfiles = 0;
   nbytes = 0.0;

   for (ii = 0; ii < BJnnx; ii++)                                          //  compute total files and bytes
   {                                                                       //    from include/exclude recs
      nfiles += BJfiles[ii];
      nbytes += BJbytes[ii];
   }

   wprintf(mLog," disk files: %d  %s \n",nfiles,formatKBMB(nbytes,3));

   if ((nfiles != Dnf) || (Dbytes != nbytes)) {                            //  must match
      wprintf(mLog," *** bug: nfiles: %d  Dnf: %d \n",nfiles,Dnf);
      wprintf(mLog,"          nbytes: %.0f  Dbytes: %.0f \n",nbytes,Dbytes);
      goto errret;
   }

   SortFileList((char *) Drec,sizeof(dfrec),Dnf,'A');                      //  sort Drec[Dnf] by Drec[].file

   for (ii = dups = 0; ii < Dnf-1; ii++)                                   //  look for duplicate files
      if (strEqu(Drec[ii].file,Drec[ii+1].file)) {
         wprintf(mLog," *** duplicate file: %s \n",Drec[ii].file);
         dups++;
      }

   if (dups) goto errret;
   return 0;

errret:
   BJvalid = 0;
   dFilesReset();
   return 0;
}


//  get existing files at backup location, save in Brec[] array
//  return -1 if error, else count of backup files
//
//  Linux sort command:
//    '.' sorts before ' ' (0x2E < 0x20, which is crazy)
//    Workaround implemented.

int bGetFiles()
{
   int            gcc, fcc, err, vers, vfound, jj;
   int            bb, bbp, rtype, noret = 0;
   int            lover, hiver, retND, retNV;
   char           command[300], *pp, *psep1;
   char           bfile[maxfcc], *bfile2;
   double         fage;
   const char     *psep2;
   FILE           *fid;
   struct stat64  filestat;

   bFilesReset();                                                          //  reset backup file list
   if (! mount(0)) return 0;                                               //  validate and mount target  v.3.2

   wprintx(mLog,0,"\n""find all files at backup location \n",boldfont);

   sprintf(command,"find %s -type f -or -type l >%s",BJdirk,TFbakfiles);   //  backup filespecs to temp file   v.3.0
   err = do_shell("find",command);
   if (err) return -1;

   //  read filespecs into memory and use memory sort instead of linux sort utility
   //  (apparently cannot do a straight ascii sort, even with LC_ALL=C)

   gcc = strlen(BD_UKOPPDIRK);                                             //  directory for ukopp special files

   fid = fopen(TFbakfiles,"r");                                            //  read file list
   if (! fid) zappcrash("cannot open scratch file %s",TFbakfiles);

   for (bb = 0; bb < maxfs; )                                              //  loop all files at backup location
   {
      pp = fgets_trim(bfile,maxfcc-1,fid);                                 //  next file
      if (! pp) break;                                                     //  eof

      bfile2 = bfile + BJdcc;                                              //  remove backup mount point
      if (strnEqu(bfile2,BD_UKOPPDIRK,gcc)) continue;

      fcc = strlen(bfile2);
      if (fcc > maxfcc-BJdcc-10) {                                         //  cannot handle files near limit
         wprintf(mLog," *** filespec too big, omit: %s...",bfile2);
         wprintf(mLog,"\n");
         continue;
      }

      err = lstat64(bfile,&filestat);                                      //  check accessibility
      if (err) {
         wprintf(mLog," *** %s, omit: %s",strerror(errno),bfile2);
         wprintf(mLog,"\n");
         continue;
      }
      else  if (! S_ISREG(filestat.st_mode) &&                             //  reg. files and symlinks only    v.3.0
                ! S_ISLNK(filestat.st_mode)) continue;

      //  build memory record for file data

      Brec[bb].file = strdup(bfile2);                                      //  filespec
      Brec[bb].err = 0;
      Brec[bb].size = filestat.st_size;                                    //  file size
      Brec[bb].mtime = filestat.st_mtime                                   //  last mod time
                      + filestat.st_mtim.tv_nsec * nano;
      Brec[bb].lover = Brec[bb].hiver = Brec[bb].nexpv = 0;                //  no versions yet
      Brec[bb].finc = 0;                                                   //  no backup yet
      bb++;
   }

   fclose (fid);

   Bnf = bb;
   wprintf(mLog," %6d backup files \n",Bnf);

   if (Bnf == maxfs) {
      wprintf(mLog," *** max files exceeded \n");
      bFilesReset();
      return -1;
   }

   SortFileList((char *) Brec,sizeof(bfrec),Bnf,'A');                      //  sort Brec[Bnf] by Brec[].file

   for (bb = 0, bbp = -1; bb < Bnf; bb++)                                  //  loop all files
   {
      bfile2 = Brec[bb].file;
      fcc = strlen(bfile2);

      vers = 0;
      psep1 = strstr(bfile2+fcc-10,VSEP1);                                 //  look for file version
      if (psep1) {
         err = convSI(psep1+2,vers,1,9999,&psep2);                         //  if format not valid,
         if (err > 1) vers = 0;                                            //    assume a current file (vers 0)
         if (strNeq(psep2,VSEP2)) vers = 0;
         if (*(psep2+1)) vers = 0;                                         //  VSEP2 must be at end   v.3.2
         if (vers) *psep1 = 0;                                             //  remove version from file name
      }

      if (! vers)                                                          //  a current file, not prior version
      {
         bbp++;                                                            //  add new file record
         Brec[bbp] = Brec[bb];                                             //  copy all data
      }

      if (vers)                                                            //  a prior version, 1-9999
      {
         if (bbp > -1 && strEqu(Brec[bbp].file,bfile2)) {                  //  look back for match with prior file
            if (Brec[bbp].lover == 0) Brec[bbp].lover = vers;              //  set first version found
            if (vers < Brec[bbp].lover) Brec[bbp].lover = vers;            //  (10) sorts before (9)
            if (vers > Brec[bbp].hiver) Brec[bbp].hiver = vers;            //  track lowest and highest vers. found
            free(bfile2);                                                  //  free duplicate filespec
         }
         else  {                                                           //  version present, but no curr. file
            bbp++;                                                         //  add new file record
            Brec[bbp] = Brec[bb];                                          //  copy all data
            Brec[bbp].err = -1;                                            //  mark file (vers 0) not present
            Brec[bbp].size = Brec[bbp].mtime = 0;
            Brec[bbp].lover = Brec[bbp].hiver = vers;                      //  track prior versions present
         }
      }
   }

   Bnf = bbp + 1;

   for (bb = 0; bb < Bnf; bb++)                                            //  loop all files at backup location
   {
      strcpy(bfile,BJdirk);
      strcat(bfile,Brec[bb].file);
      bfile2 = bfile + BJdcc;

      if (BJnnx > 0) {
         for (jj = 0; jj < BJnnx; jj++) {                                  //  find matching backup include rec.
            rtype = BJrtype[jj];
            if (rtype != 2) continue;
            if (MatchWild(BJfspec[jj],bfile2) == 0) break;
         }
         if (jj == BJnnx) {                                                //  this file not in backup set
            Brec[bb].retND = Brec[bb].retNV = 0;                           //  no retention specs
            noret++;
         }
         else {
            Brec[bb].retND = BJretND[jj];                                  //  get corresp. retention specs  v.3.5
            Brec[bb].retNV = BJretNV[jj];
         }
      }

      if (Brec[bb].err == 0) {
         Cfiles++;                                                         //  count curr. version files
         Cbytes += Brec[bb].size;                                          //    and total bytes
      }

      if (Brec[bb].lover == 0) continue;                                   //  no versions present

      lover = Brec[bb].lover;                                              //  version range found
      hiver = Brec[bb].hiver;
      retND = Brec[bb].retND;                                              //  retention days
      retNV = Brec[bb].retNV;                                              //  retention versions

      if (! retND) retND = -1;                                             //  zero days retention, defeat test
      vfound = 0;                                                          //  versions found

      for (vers = hiver; vers >= lover; vers--)                            //  loop file version, high to low
      {                                                                    //  v.4.0
         setFileVersion(bfile,vers);
         err = lstat64(bfile,&filestat);                                   //  check file exists on backup
         if (err) {
            wprintf(mLog," *** version %d missing: %s \n",vers,bfile2);
            continue;
         }

         vfound++;                                                         //  this file, versions found
         Vfiles++;                                                         //  total versioned files and bytes
         Vbytes += filestat.st_size;

         fage = (time(0)-filestat.st_mtime)/24.0/3600.0;                   //  file version age in days
         if (fage <= retND || vfound <= retNV) continue;                   //  this version is to be retained
         Brec[bb].nexpv++;                                                 //  count expired versions    v.4.0
         Pfiles++;                                                         //  total expired files and bytes
         Pbytes += filestat.st_size;                                       //  (to be purged)
      }
   }

   wprintf(mLog," %6d files not in backup set (unknown retention) \n",noret);
   wprintf(mLog," %6d (%s) curr. file versions \n",Cfiles,formatKBMB(Cbytes,3));
   wprintf(mLog," %6d (%s) prior file versions \n",Vfiles,formatKBMB(Vbytes,3));
   wprintf(mLog," %6d (%s) expired prior versions \n",Pfiles,formatKBMB(Pbytes,3));

   sprintf(command,"df -h %s",mountdirk);                                  //  v.4.2
   do_shell("target space",command);

   return Bnf;
}


//  get all restore files specified by include/exclude records
//  save in Rrec[] array

int rGetFiles()
{
   int         ii, jj, cc, rtype, wstat, ninc, nexc;
   char       *fspec;

   if (! RJvalid) return 0;
   rFilesReset();                                                          //  clear restore files
   if (bGetFiles() < 1) return 0;                                          //  get backup files

   wprintf(mLog,"\n generating restore file set \n");

   for (ii = 0; ii < RJnnx; ii++)                                          //  process include/exclude recs
   {
      rtype = RJrtype[ii];
      fspec = RJfspec[ii];

      if (rtype == 2)                                                      //  include filespec
      {
         wprintf(mLog," include %s \n",fspec);

         for (ninc = jj = 0; jj < Bnf; jj++)                               //  screen all files in backup loc.
         {
            wstat = MatchWild(fspec,Brec[jj].file);
            if (wstat != 0) continue;
            if (Brec[jj].err) continue;
            Rrec[Rnf].file = strdup(Brec[jj].file);                        //  add matching files
            Rrec[Rnf].finc = 0;
            Rnf++; ninc++;
            if (Rnf == maxfs) {
               wprintf(mLog," *** max files exceeded \n");
               rFilesReset();                                              //  v.4.4
               return 0;
            }
         }

         wprintf(mLog,"  %d files added \n",ninc);
      }

      if (rtype == 3)                                                      //  exclude filespec
      {
         wprintf(mLog," exclude %s \n",fspec);

         for (nexc = jj = 0; jj < Rnf; jj++)                               //  check all included files (SO FAR)
         {
            if (! Rrec[jj].file) continue;

            wstat = MatchWild(fspec,Rrec[jj].file);
            if (wstat != 0) continue;
            free(Rrec[jj].file);                                           //  remove matching files
            Rrec[jj].file = 0;
            nexc++;
         }

         wprintf(mLog,"  %d files removed \n",nexc);
      }
   }

   ii = jj = 0;                                                            //  repack after deletions
   while (ii < Rnf)
   {
      if (Rrec[ii].file == 0) ii++;
      else
      {
         if (ii > jj)
         {
            if (Rrec[jj].file) free(Rrec[jj].file);
            Rrec[jj].file = Rrec[ii].file;
            Rrec[ii].file = 0;
         }
         ii++;
         jj++;
      }
   }

   Rnf = jj;
   wprintf(mLog," total file count: %d \n",Rnf);

   cc = strlen(RJfrom);                                                    //  copy from: /dirk/.../

   for (ii = 0; ii < Rnf; ii++)                                            //  get selected backup files to restore
   {
      if (! strnEqu(Rrec[ii].file,RJfrom,cc)) {
         wprintf(mLog," *** not within copy-from; %s \n",Rrec[ii].file);
         RJvalid = 0;                                                      //  mark restore job invalid
         continue;
      }
   }

   SortFileList((char *) Rrec,sizeof(rfrec),Rnf,'A');                      //  sort Rrec[Rnf] by Rrec[].file
   return 0;
}


//  helper function for backups and reports
//
//  compare disk and backup files, set disp in Drec[] and Brec[] arrays:
//       n  new         on disk, not on backup
//       d  deleted     on backup, not on disk
//       m  modified    on both, but not equal
//       u  unchanged   on both, and equal
//       v  versions    on backup, only prev. versions are present

int setFileDisps()
{
   int            dii, bii, comp;
   char           disp;
   double         diff;

   dii = bii = 0;
   nnew = nmod = nunc = ndel = comp = 0;
   Mbytes = 0.0;                                                           //  total bytes, new and modified files

   while ((dii < Dnf) || (bii < Bnf))                                      //  scan disk and backup files parallel
   {
      if ((dii < Dnf) && (bii == Bnf)) comp = -1;
      else if ((dii == Dnf) && (bii < Bnf)) comp = +1;
      else comp = strcmp(Drec[dii].file, Brec[bii].file);

      if (comp < 0) {                                                      //  unmatched disk file
         Drec[dii].disp = 'n';                                             //  new
         nnew++;                                                           //  count new files
         Mbytes += Drec[dii].size;                                         //  accumulate Mbytes
         Drec[dii].bindx = -1;                                             //  no matching backup file
         dii++;
      }

      else if (comp > 0) {                                                 //  unmatched backup file
         if (Brec[bii].err == 0) {                                         //  if current version is present,
            Brec[bii].disp = 'd';                                          //    file was deleted from disk
            ndel++;                                                        //  count deleted files
         }
         else Brec[bii].disp = 'v';                                        //  only old versions on backup
         bii++;
      }

      else if (comp == 0) {                                                //  file present on disk and backup
         Drec[dii].bindx = bii;                                            //  link disk to backup record   v.4.0
         if (Brec[bii].err == 0) {
            diff = Drec[dii].mtime - Brec[bii].mtime;                      //  check if equal mod times
            if (fabs(diff) > MODTIMETOLR) disp = 'm';
            else disp = 'u';                                               //  yes, assume unchanged
            Drec[dii].disp = Brec[bii].disp = disp;
            if (disp == 'u') nunc++;                                       //  count unchanged files
            if (disp == 'm') nmod++;                                       //  count modified files
            if (disp == 'm') Mbytes += Drec[dii].size;                     //    and accumulate Mbytes
         }
         else {
            Brec[bii].disp = 'v';                                          //  only old versions on backup
            Drec[dii].disp = 'n';                                          //  disk file is logically new
            nnew++;                                                        //  count new files
            Mbytes += Drec[dii].size;                                      //  accumulate Mbytes
         }
         dii++;
         bii++;
      }
   }

   Mfiles = nnew + nmod + ndel;
   return 0;
}


//  Sort file list in memory (disk files, backup files, restore files).
//  Sort ascii sequence, or sort subdirectories in a directory before files.

int SortFileList(char *recs, int RL, int NR, char sort)
{
   HeapSortUcomp fcompA, fcompD;                                           //  filespec compare funcs
   if (sort == 'A') HeapSort(recs,RL,NR,fcompA);                           //  ascii compare
   if (sort == 'D') HeapSort(recs,RL,NR,fcompD);                           //  special compare (directories first)
   return 0;
}

int fcompA(cchar *rec1, cchar *rec2)                                       //  ascii comparison
{                                                                          //  current file (no version) sorts first
   dfrec  *r1 = (dfrec *) rec1;
   dfrec  *r2 = (dfrec *) rec2;
   return strcmp(r1->file,r2->file);
}

int fcompD(cchar *rec1, cchar *rec2)                                       //  special compare filenames
{                                                                          //  subdirectories in a directory compare
   dfrec  *r1 = (dfrec *) rec1;                                            //    less than files in the directory
   dfrec  *r2 = (dfrec *) rec2;
   return filecomp(r1->file,r2->file);
}

int filecomp(cchar *file1, cchar *file2)                                   //  special compare filenames
{                                                                          //  subdirectories compare before files
   cchar       *pp1, *pp10, *pp2, *pp20;
   cchar       slash = '/';
   int         cc1, cc2, comp;

   pp1 = file1;                                                            //  first directory level or file
   pp2 = file2;

   while (true)
   {
      pp10 = strchr(pp1,slash);                                            //  find next slash
      pp20 = strchr(pp2,slash);

      if (pp10 && pp20) {                                                  //  both are directories
         cc1 = pp10 - pp1;
         cc2 = pp20 - pp2;
         if (cc1 < cc2) comp = strncmp(pp1,pp2,cc1);                       //  compare the directories
         else comp = strncmp(pp1,pp2,cc2);
         if (comp) return comp;
         else if (cc1 != cc2) return (cc1 - cc2);
         pp1 = pp10 + 1;                                                   //  equal, check next level
         pp2 = pp20 + 1;
         continue;
      }

      if (pp10 && ! pp20) return -1;                                       //  only one is a directory,
      if (pp20 && ! pp10) return 1;                                        //    the directory is first

      comp = strcmp(pp1,pp2);                                              //  both are files, compare
      return comp;
   }
}


//  reset all backup job data and free allocated memory

int BJreset()
{
   for (int ii = 0; ii < BJnnx; ii++)
      if (BJfspec[ii]) free(BJfspec[ii]);

   BJnnx = BJvalid = 0;
   BJvmode = 0;
   dFilesReset();                                                          //  reset dependent disk file data
   return 0;
}


//  reset all restore job data and free allocated memory

int RJreset()
{
   for (int ii = 0; ii < RJnnx; ii++)
      if (RJfspec[ii]) free(RJfspec[ii]);

   RJvalid = RJnnx = 0;
   rFilesReset();                                                          //  reset dependent disk file data
   return 0;
}


//  reset all file data and free allocated memory

int dFilesReset()
{                                                                          //  disk files data
   for (int ii = 0; ii < Dnf; ii++)
   {
      free(Drec[ii].file);
      Drec[ii].file = 0;
   }

   Dnf = 0;
   Dbytes = Mbytes = 0.0;
   return 0;
}

int bFilesReset()
{                                                                          //  backup files data
   for (int ii = 0; ii < Bnf; ii++)
   {
      free(Brec[ii].file);
      Brec[ii].file = 0;
   }

   Bbytes = Bnf = 0;
   Cbytes = Cfiles = 0;
   Mbytes = Mfiles = 0;
   Vbytes = Vfiles = 0;
   Pbytes = Pfiles = 0;
   return 0;
}

int rFilesReset()
{                                                                          //  restore files data
   for (int ii = 0; ii < Rnf; ii++)
   {
      free(Rrec[ii].file);
      Rrec[ii].file = 0;
   }

   Rnf = 0;
   return 0;
}


//  Helper function to copy a file between disk and backup location.
//  Owner and permissions are transferred for copied files and directories,
//  but this will do nothing in case target is VFAT (Microsoft) file system.

cchar * copyFile(cchar *sfile, cchar *dfile, int mpf)
{
   char              file1[maxfcc], file2[maxfcc];
   int               fid1, fid2, err, rcc, dlevs;
   char              *pp1, *pp2, buff[BIOCC];
   const char        *errmess = 0;
   struct stat64     fstat1, fstat2;
   struct timeval    ftimes[2];

   *file1 = *file2 = 0;
   if (mpf == 1) strcpy(file1,BJdirk);                                     //  prepend mount point if req.
   strcat(file1,sfile);
   if (mpf == 2) strcpy(file2,BJdirk);
   strcat(file2,dfile);

   pp2 = file2;
   dlevs = 0;

   while (true) {                                                          //  v.25
      pp2 = strchr(pp2+1,'/');                                             //  create missing directory levels
      if (! pp2) break;                                                    //  (check and create from top down)
      *pp2 = 0;
      err = stat64(file2,&fstat2);
      if (err) {
         err = mkdir(file2,0731);
         if (err) return strerror(errno);
         dlevs++;
      }
      *pp2 = '/';
   }

   while (dlevs) {                                                         //  v.25
      pp1 = (char *) strrchr(file1,'/');                                   //  for created output directories,
      if (! pp1) break;                                                    //   copy owner and permissions from
      pp2 = (char *) strrchr(file2,'/');                                   //    corresponding input directory
      if (! pp2) break;                                                    //     (measured from bottom up)
      *pp1 = *pp2 = 0;                                                     //  (possibly top levels not set)
      err = stat64(file1,&fstat1);
      if (err) return strerror(errno);
      chmod(file2,fstat1.st_mode);
      err = chown(file2,fstat1.st_uid,fstat1.st_gid);
      if (err) wprintf(mLog,"error: %s \n",wstrerror(err));
      dlevs--;
   }

   *file1 = *file2 = 0;
   if (mpf == 1) strcpy(file1,BJdirk);                                     //  refresh filespecs
   strcat(file1,sfile);
   if (mpf == 2) strcpy(file2,BJdirk);
   strcat(file2,dfile);

   err = lstat64(file1,&fstat1);                                           //  get input file attributes  v.3.0
   if (err) return strerror(errno);

   if (S_ISLNK(fstat1.st_mode)) {                                          //  input file is symlink
      rcc = readlink(file1,buff,maxfcc);
      if (rcc < 0 || rcc > maxfcc-2) return strerror(errno);
      buff[rcc] = 0;
      err = symlink(buff,file2);                                           //  create output symlink
      if (err) return strerror(errno);
      ftimes[0].tv_sec = fstat1.st_atime;                                  //  get input file access time  v.3.0
      ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000;                   //    in microsecs.
      ftimes[1].tv_sec = fstat1.st_mtime;
      ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000;
      lutimes(file2,ftimes);                                               //  set output file access time
      return 0;
   }

   fid1 = open(file1,O_RDONLY+O_NOATIME+O_LARGEFILE);                      //  open input file
   if (fid1 == -1) return strerror(errno);

   fid2 = open(file2,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700);           //  open output file
   if (fid2 == -1) {
      errmess = strerror(errno);
      close(fid1);
      return errmess;
   }

   while (true)
   {
      rcc = read(fid1,buff,BIOCC);                                         //  read huge blocks
      if (rcc == 0) break;
      if (rcc == -1) {
         errmess = strerror(errno);
         close(fid1);
         close(fid2);
         return errmess;
      }

      rcc = write(fid2,buff,rcc);                                          //  write blocks
      if (rcc == -1) {
         errmess = strerror(errno);
         close(fid1);
         close(fid2);
         return errmess;
      }

      if (checkKillPause()) break;
   }

   close(fid1);                                                            //  close input file
   err = fsync(fid2);                                                      //  flush output file         v.4.4
   if (err) return strerror(errno);
   err = close(fid2);                                                      //  close output file
   if (err) return strerror(errno);

   err = lstat64(file1,&fstat1);                                           //  get input file attributes
   if (err) return strerror(errno);

   chmod(file2,fstat1.st_mode);                                            //  copy owner and permissions
   err = chown(file2,fstat1.st_uid,fstat1.st_gid);                         //    from input to output file
   if (err) wprintf(mLog,"error: %s \n",wstrerror(err));

   ftimes[0].tv_sec = fstat1.st_atime;                                     //  get input file access time
   ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000;                      //    in microsecs.
   ftimes[1].tv_sec = fstat1.st_mtime;
   ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000;
   utimes(file2,ftimes);                                                   //  set output file access time

   return 0;
}


//  Verify helper function
//  Verify that file on backup medium is readable, return its length.
//  Optionally compare backup file to disk file, byte for byte.
//  returns error message or null if OK.

cchar * checkFile(cchar *dfile, int compf, double &tcc)
{
   int            vfid = 0, dfid = 0;
   int            err, vcc, dcc, cmperr = 0;
   char           vfile[maxfcc], *vbuff = 0, *dbuff = 0;
   const char     *errmess = 0;
   double         dtime, vtime;
   int            open_flagsV = O_RDONLY+O_NOATIME+O_LARGEFILE+O_DIRECT;   //  bypass cache           v.3.5.2
   int            open_flagsD = O_RDONLY+O_NOATIME+O_LARGEFILE;            //  use cache normally     v.3.5.3
   struct stat64  filestat;

   tcc = 0.0;

   strcpy(vfile,BJdirk);                                                   //  prepend mount point
   strcat(vfile,dfile);

   lstat64(vfile,&filestat);                                               //  if symlink, check readable   v.3.1
   if (S_ISLNK(filestat.st_mode)) {
      vbuff = (char *) malloc(maxfcc);
      vcc = readlink(vfile,vbuff,maxfcc);
      if (vcc == -1) errmess = strerror(errno);
      goto cleanup;
   }

   if (compf) goto comparefiles;

   vfid = open(vfile,open_flagsV);                                         //  open for read, large blocks, direct I/O
   if (vfid == -1) goto checkerr;

   err = posix_memalign((void**) &vbuff,512,BIOCC);                        //  use aligned buffer
   if (err) zappcrash("memory allocation failure");

   while (1)
   {
      vcc = read(vfid,vbuff,BIOCC);
      if (vcc == 0) break;
      if (vcc == -1) { errmess = strerror(errno); break; }
      tcc += vcc;                                                          //  accumulate length
      if (checkKillPause()) break;
   }
   goto cleanup;

comparefiles:

   vfid = open(vfile,open_flagsV);                                         //  open for read, large blocks, direct I/O
   if (vfid == -1) goto checkerr;

   dfid = open(dfile,open_flagsD);                                         //  disk files, use cached I/O
   if (dfid == -1) goto checkerr;

   err = posix_memalign((void**) &vbuff,512,BIOCC);                        //  use aligned buffers
   if (err) zappcrash("memory allocation failure");
   err = posix_memalign((void**) &dbuff,512,BIOCC);
   if (err) zappcrash("memory allocation failure");

   while (1)
   {
      vcc = read(vfid,vbuff,BIOCC);                                        //  read two files
      if (vcc == -1) { errmess = strerror(errno); goto cleanup; }

      dcc = read(dfid,dbuff,BIOCC);
      if (dcc == -1) { errmess = strerror(errno); goto cleanup; }

      if (vcc != dcc) cmperr++;                                            //  compare buffers
      if (memcmp(vbuff,dbuff,vcc)) cmperr++;

      tcc += vcc;                                                          //  accumulate length
      if (vcc == 0) break;
      if (dcc == 0) break;

      if (checkKillPause()) break;
   }

   if (vcc != dcc) cmperr++;

   if (cmperr) {                                                           //  compare error
      lstat64(dfile,&filestat);                                            //                                  v.3.0
      dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano;         //  file modified since snapshot?
      lstat64(vfile,&filestat);                                            //                                  v.3.0
      vtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano;
      if (fabs(dtime-vtime) < MODTIMETOLR) errmess = "compare error";      //  no, a real compare error
   }

cleanup:
   if (vfid) close(vfid);
   if (dfid) close(dfid);
   if (vbuff) free(vbuff);
   if (dbuff) free(dbuff);
   return errmess;

checkerr:                                                                  //  v.3.7
   errmess = strerror(errno);
   if (errno == EINVAL || errno == ENOTSUP) {
      if (Fgui) zmessageACK(mWin,0,"large block direct I/O not allowed \n %s",errmess);
      else wprintf(mLog,"large block direct I/O not allowed \n %s",errmess);
   }
   if (errno == EPERM) {
      if (Fgui) zmessageACK(mWin,0,"permission denied \n %s",errmess);
      else wprintf(mLog,"permission denied \n %s",errmess);
   }
   goto cleanup;
}


//  modify filespec to have a specified version
//  0 = no version = current version, +N = previous version
//  returns cc of resulting filespec
//  warning: filespec must have space for version numbers

int setFileVersion(char *filespec, int vers)
{
   int         fcc, overs, err;
   char        *psep1;
   const char  *psep2;

   fcc = strlen(filespec);
   psep1 = strstr(filespec+fcc-10,VSEP1);                                  //  look for file version   v.3.2
   if (psep1) {
      overs = 0;
      err = convSI(psep1+2,overs,&psep2);                                  //  if format not valid, take
      if (err < 2) overs = 1;                                              //    as non-versioned file
      if (strNeq(psep2,VSEP2)) overs = 0;
      if (*(psep2+1)) overs = 0;                                           //  VSEP2 must be at end
      if (overs) *psep1 = 0;
      fcc = psep1 - filespec;
   }

   if (vers == 0) return fcc;

   if (! psep1) psep1 = filespec + fcc;
   strcpy(psep1,VSEP1);
   sprintf(psep1+2,"%d",vers);
   strcat(psep1+2,VSEP2);

   return fcc + strlen(psep1);
}


//  rename a backup file to assign the next version number
//  update the passed backup file data record, bakrec
//  returns error message or null if all OK

const char * setnextVersion(bfrec &bakrec)
{
   char     fspec1[maxfcc], fspec2[maxfcc];
   int      vers, err;

   strcpy(fspec1,BJdirk);
   strcat(fspec1,bakrec.file);
   strcpy(fspec2,fspec1);

   vers = bakrec.hiver + 1;
   setFileVersion(fspec2,vers);

   err = rename(fspec1,fspec2);
   if (err) return strerror(errno);

   bakrec.hiver = vers;
   if (! bakrec.lover) bakrec.lover = vers;                                //  v.4.0
   return null;
}


//  purge expired file versions in backup location
//  bakrec: backup file data record to use and update
//  returns error message or null if all OK
//  fkeep: if true, keep last version unless disk file was deleted         //  v.4.0

const char * purgeVersions(bfrec &bakrec, int fkeep)
{
   int            lover, hiver, loretver, vers;
   int            err, vfound, vpurged;
   int            retND, retNV;
   double         fage;
   char           fspec[maxfcc];
   const char     *mess = null;
   struct stat64  filestat;

   strcpy(fspec,BJdirk);                                                   //  prepend backup location
   strcat(fspec,bakrec.file);

   retND = bakrec.retND;
   retNV = bakrec.retNV;
   lover = bakrec.lover;
   hiver = bakrec.hiver;
   if (! hiver) return 0;                                                  //  no versions present

   if (! retND) retND = -1;                                                //  zero days retention, defeat test

   if (bakrec.disp == 'd') fkeep = 0;                                      //  file no longer in backup job
   if (bakrec.disp == 'v') fkeep = 0;                                      //    or disk file deleted

   loretver = lover;                                                       //  lowest retained version
   vfound = 0;                                                             //  actual versions found
   vpurged = 0;                                                            //  versions purged

   for (vers = hiver; vers >= lover; vers--)                               //  loop file versions, high to low
   {                                                                       //                            v.4.0
      setFileVersion(fspec,vers);
      err = lstat64(fspec,&filestat);                                      //  check file exists on backup
      if (err) continue;
      vfound++;                                                            //  count versions found

      fage = (time(0)-filestat.st_mtime)/24.0/3600.0;                      //  file age in days

      if (fage <= retND || vfound <= retNV) {                              //  retain this version
         loretver = vers;                                                  //  remember lowest retained version
         continue;
      }

      if ((vers == hiver) && fkeep) {                                      //  retain last version    v.4.0
         loretver = vers;
         continue;
      }

      mess = deleteFile(fspec);                                            //  purge this version
      if (mess) break;
      vpurged++;
   }

   bakrec.nexpv = fkeep;                                                   //  set 0 or 1 expired versions  v.4.0
   bakrec.lover = loretver;                                                //  set new low version          v.4.0
   if (vpurged == vfound)
      bakrec.lover = bakrec.hiver = 0;                                     //  no versions remaining        v.4.1
   return mess;
}


//  helper function to delete a file from backup location.
//  delete parent directories if they are now empty.

cchar * deleteFile(cchar *file)
{
   int      err;
   char     dfile[maxfcc], *pp;

   strcpy(dfile,file);

   err = remove(dfile);
   if (err) return strerror(errno);

   while ((pp = (char *) strrchr(dfile,'/')))                              //  delete empty directory
   {
      *pp = 0;
      err = rmdir(dfile);
      if (! err) continue;                                                 //  and parents ...
      if (errno == ENOTEMPTY) return 0;
      if (errno == EBUSY) return 0;                                        //  see https://lkml.org/lkml/2013/8/28/654  v.4.8
      return strerror(errno);
   }

   return 0;
}


//  do shell command (subprocess) and echo outputs to log window
//  returns command status: 0 = OK, +N = error

int do_shell(cchar *pname, cchar *command)
{
   char     buff[500], *crec;
   int      err, contx = 0;

   snprintf(buff,499,"\n""shell: %s \n",command);
   wprintx(mLog,0,buff,boldfont);

   while ((crec = command_output(contx,command)))                          //  bug fix: remove colon   v.3.2
   {
      wprintf(mLog," %s: %s \n",pname,crec);
      zsleep(0.1);                                                         //  throttle output a little
   }

   err = command_status(contx);
   if (err == 32) err = 0;                                                 //  ignore Linux "broken pipe" crap
   if (err) wprintf(mLog," %s status: %s \n", pname, strerror(err));
   else wprintf(mLog," OK \n");
   return err;
}


//  supply unused zdialog callback function

void KBstate(GdkEventKey *event, int state)
{ return; }




