/**************************************************************************
   dkopp   copy files to / restore files from BD/DVD media

   Copyright 2006 2007 2008 2009 2010 2011 2012 2013 Michael Cornelison
   source URL:  kornelix.com
   contact: kornelix2@gmail.com
   
   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/>.

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

#define dkopp_title "dkopp v.6.3.1"                                        //  version
#define dkopp_license "GNU General Public License v.3"
#define debug 0

#include <fcntl.h>
#include <dirent.h>
#include <sys/ioctl.h>
#include <sys/statfs.h>                                                    //  v.6.2
#if defined __linux__                                                      //  v.5.9.3
   #include <linux/cdrom.h>
#endif
#include "zfuncs.h"                                                        //  order important

//  parameters and limits

#define normfont "monospace 9"                                             //  report fonts   v.5.2
#define boldfont "monospace bold 9"
#define vrcc  512*1024                                                     //  verify read I/O size
#define maxnx 1000                                                         //  max no. include/exclude recs.
#define maxfs 200000                                                       //  max no. disk or DVD/BD files
#define maxhist 200                                                        //  max no. history files
#define giga  1000000000.0                                                 //  gigabyte (not 1024**3)
#define dvdcapmin 0.5                                                      //  DVD/BD capacity sanity limits, GB
#define dvdcapmax 50.0                                                     //  (Blue-Ray dual layer = 50 GB)      v.5.3
#define modtimetolr 1.0                                                    //  tolerance, file equal mod times    v.5.1
#define nano 0.000000001                                                   //  nanosecond
#define gforce "-use-the-force-luke=tty -use-the-force-luke=notray"        //  growisofs hidden options

//  special control files on DVD/BD

#define V_DKOPPDIRK  "/dkopp-data/"                                        //  dkopp special files on DVD/BD
#define V_FILEPOOP  V_DKOPPDIRK "filepoop"                                 //  directory data file
#define V_JOBFILE   V_DKOPPDIRK "jobfile"                                  //  backup job data file
#define V_DATETIME  V_DKOPPDIRK "datetime"                                 //  date-time file

//  GTK GUI widgets

GtkWidget      *mWin, *mVbox, *mScroll, *mLog;                             //  main window
GtkTextBuffer  *logBuff;
GtkWidget      *fc_dialogbox, *fc_widget;                                  //  file-chooser dialog
GtkWidget      *editwidget;                                                //  edit box in file selection dialogs

//  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      commFail;                                                         //  command failure flag
int      Fdialog;                                                          //  dialog in progress
int      clrun;                                                            //  flag, command line 'run' command
int      Fgui;                                                             //  flag, GUI or command line
char     subprocName[20];                                                  //  name of created subprocess
char     scriptParam[200];                                                 //  parameter from script file
char     mbmode[20], mvmode[20];                                           //  actual backup, verify modes
double   pctdone;                                                          //  % done from growisofs
char     scrFile[maxfcc];                                                  //  command line script file
char     backupDT[16];                                                     //  nominal backup date: yyyymmdd-hhmm

char     userdir[200];                                                     //  /home/user/.dkopp
char     TFdiskfiles[200], TFdvdfiles[200];                                //  scratch files in userdir
char     TFjobfile[200], TFfilepoop[200], TFdatetime[200];
char     TFrestorefiles[200], TFrestoredirks[200];

//  available DVD/BD devices

int      ndvds, maxdvds = 8;
char     dvddevs[8][20];                                                   //  DVD/BD devices, /dev/sr0 etc.
char     dvddesc[8][40];                                                   //  DVD/BD device descriptions
char     dvddevdesc[8][60];                                                //  combined device and description

//  backup job data

char     BJfile[maxfcc];                                                   //  backup job file
char     BJdvd[20];                                                        //  DVD/BD device: /dev/hdb
double   BJcap;                                                            //  DVD/BD capacity (GB)
int      BJspeed;                                                          //  write speed, x 1.38 MB/sec  v.4.5
char     BJbmode[20];                                                      //  backup: full/incremental/accumulate
char     BJvmode[20];                                                      //  verify: full/incremental/thorough
char     BJdatefrom[12];                                                   //  mod date selection, yyyy.mm.dd
time_t   BJtdate;                                                          //  binary mod date selection
int      BJndvd;                                                           //  no. DVD/BD media required
int      BJval;                                                            //  backup job data validated
int      BJmod;                                                            //  backup job data modified
char    *BJinex[maxnx];                                                    //  backup include/exclude records
int      BJfiles[maxnx];                                                   //  corresp. file count per rec
double   BJbytes[maxnx];                                                   //  corresp. byte count per rec
int      BJdvdno[maxnx];                                                   //  corresp. DVD/BD sequence no. (1,2...)
int      BJnx;                                                             //  actual record count < maxnx

//  DVD/BD medium data

char     dvdmp[100];                                                       //  mount point, /media/xxxxx
int      dvdmpcc;                                                          //  mount point cc
int      dvdmtd;                                                           //  DVD/BD mounted
char     mediumDT[16];                                                     //  DVD/BD medium last use date-time
time_t   dvdtime;                                                          //  DVD/BD device mod time
int      dvdnum;                                                           //  DVD/BD medium sequence no.
char     dvdlabel[32];                                                     //  DVD/BD label

//  current files for backup

struct dfrec {                                                             //  disk file record
   char     *file;                                                         //    directory/filename
   double   size;                                                          //    byte count
   double   mtime;                                                         //    mod time
   int      stat;                                                          //    fstat() status
   int      dvd;                                                           //    assigned DVD/BD number
   int      inclx;                                                         //    include rec for this file
   char     disp;                                                          //    status: new modified unchanged
   char     ivf;                                                           //    flag for incr. verify
};

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

//  DVD/BD file data 

struct vfrec {                                                             //  DVD/BD file record
   char     *file;                                                         //    directory/file (- /media/xxx)
   double   size;                                                          //    byte count
   double   mtime;                                                         //    mod time
   int      stat;                                                          //    fstat() status
   char     disp;                                                          //    status: deleted modified unchanged
};

vfrec    Vrec[maxfs];                                                      //  DVD/BD file data records
int      Vnf;                                                              //  actual file count < maxfs
double   Vbytes;                                                           //  DVD/BD files, total bytes

//  disk:DVD/BD comparison data

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

//  restore job data

char    *RJinex[maxnx];                                                    //  file restore include/exclude recs.
int      RJnx;                                                             //  actual RJinex count < maxnx
int      RJval;                                                            //  restore job data validated
char     RJfrom[maxfcc];                                                   //  restore copy-from: /home/.../
char     RJto[maxfcc];                                                     //  restore copy-to: /home/.../

struct   rfrec {                                                           //  restore file record
   char     *file;                                                         //  restore filespec: /home/.../file.ext 
   double   size;                                                          //  byte count
};

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


//  dkopp local functions    

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

int getroot(cchar *);                                                      //  get root privileges
int quit_dkopp(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 fileOpen(cchar *);                                                     //  file open dialog
int fileSave(cchar *);                                                     //  file save dialog
int BJload(cchar *fspec);                                                  //  backup job data <<< file
int BJstore(cchar *fspec, int ndvd = 0);                                   //  backup job data >>> file
int BJvload(cchar *);                                                      //  load job file from DVD/BD

int BJedit(cchar *);                                                       //  backup job edit dialog
int BJedit_event(zdialog *zd, cchar *event);                               //  dialog event function
int BJedit_stuff(zdialog * zd);                                            //  stuff dialog widgets with job data
int BJedit_fetch(zdialog * zd);                                            //  set job data from dialog widgets

int Backup(cchar *);                                                       //  backup menu function
int FullBackup(cchar *vmode);                                              //  full backup + verify
int IncrBackup(cchar *bmode, cchar *vmode);                                //  incremental / accumulate + verify
int Verify(cchar *);                                                       //  verify functions

int Report(cchar *);                                                       //  report functions
int get_current_files(cchar *);                                            //  file stats. per include/exclude
int report_summary_diffs(cchar *);                                         //  disk:DVD/BD differences summary
int report_directory_diffs(cchar *);                                       //  disk:DVD/BD differences by directory
int report_file_diffs(cchar *);                                            //  disk:DVD/BD differences by file
int list_current_files(cchar *);                                           //  list all disk files for backup
int list_DVD_files(cchar *);                                               //  list all files on DVD/BD
int find_files(cchar *);                                                   //  find files on disk, DVD/BD, backup hist
int view_backup_hist(cchar *);                                             //  view backup history files

int RJedit(cchar *);                                                       //  restore job edit dialog
int RJedit_event(zdialog*, cchar *event);                                  //  RJedit response
int RJlist(cchar *);                                                       //  list DVD/BD files to be restored
int Restore(cchar *);                                                      //  file restore function

int getDVDs();                                                             //  get avail. DVD/BD devices, mount points
int setDVDdevice(cchar *);                                                 //  set DVD/BD device and mount point  
int setDVDlabel(cchar *);                                                  //  set new DVD/BD label
int mountDVD(cchar *);                                                     //  mount DVD/BD, echo outputs, status
int unmountDVD(cchar *);                                                   //  unmount DVD/BD + echo outputs
int ejectDVD(cchar *);                                                     //  eject DVD/BD + echo outputs
int resetDVD(cchar *);                                                     //  hardware reset
int eraseDVD(cchar *);                                                     //  fill DVD/BD with zeros (long time)
int formatDVD(cchar *);                                                    //  quick format DVD/BD

int saveScreen(cchar *);                                                   //  save logging window to file
int helpFunc(cchar *);                                                     //  help function

int fc_dialog(cchar *dirk);                                                //  file chooser dialog
int fc_response(GtkDialog *, int, void *);                                 //  fc_dialog response

int writeDT();                                                             //  write date-time to temp file
int save_filepoop();                                                       //  save file owner & permissions data
int restore_filepoop();                                                    //  restore file owner & perm. data
int createBackupHist();                                                    //  create backup history file

int inexParse(char *rec, char *&rtype, char *&fspec);                      //  parse include/exclude record
int BJvalidate(cchar *);                                                   //  validate backup job data
int RJvalidate();                                                          //  validate restore job data
int nxValidate(char **recs, int nr);                                       //  validate include/exclude recs

int dGetFiles();                                                           //  generate file list from job
int vGetFiles();                                                           //  find all DVD/BD files
int rGetFiles();                                                           //  generate restore job file list
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 directories before files

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

cchar * checkFile(char *dfile, int compf, double &tcc);                    //  validate file on BD/DVD medium
cchar * copyFile(cchar *vfile, char *dfile);                               //  copy file from DVD/BD to disk

int track_filespec(cchar *filespec);                                       //  track filespec on screen, no scroll
int track_filespec_err(cchar *filespec, cchar *errmess);                   //  error logger for track_filespec()
cchar * kleenex(cchar *name);                                              //  clean exotic file names for output 

int do_shell(cchar *pname, cchar *command);                                //  shell command + output to window
int track_growisofs_files(char *buff);                                     //  convert %done to filespec, output


//  dkopp 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  45
struct menuent menus[nmenu] = {
//  top-menu    sub-menu                lock    menu-function
{  "button",   "root",                    1,    getroot        },
{  "button",   "edit job",                1,    BJedit         },
{  "button",   "clear",                   0,    clearScreen    },
{  "button",   "run job",                 1,    Backup         },
{  "button",   "run DVD/BD",              1,    Backup         },
{  "button",   "pause",                   0,    signalFunc     },
{  "button",   "resume",                  0,    signalFunc     },
{  "button",   "kill job",                0,    signalFunc     },
{  "button",   "quit",                    0,    quit_dkopp     },
{  "File",     "open job",                1,    fileOpen       },
{  "File",     "open DVD/BD",             1,    BJvload        },
{  "File",     "edit job",                1,    BJedit         },
{  "File",     "show job",                0,    BJvalidate     },
{  "File",     "save job",                0,    fileSave       },
{  "File",     "run job",                 1,    Backup         },
{  "File",     "run DVD/BD",              1,    Backup         },
{  "File",     "quit",                    0,    quit_dkopp     },
{  "Backup",   "full",                    1,    Backup         },
{  "Backup",   "incremental",             1,    Backup         },
{  "Backup",   "accumulate",              1,    Backup         },
{  "Verify",   "full",                    1,    Verify         },
{  "Verify",   "incremental",             1,    Verify         },
{  "Verify",   "thorough",                1,    Verify         },
{  "Report",   "get files for backup",    1,    Report         },
{  "Report",   "diffs summary",           1,    Report         },
{  "Report",   "diffs by directory",      1,    Report         },
{  "Report",   "diffs by file",           1,    Report         },
{  "Report",   "list files for backup",   1,    Report         },
{  "Report",   "list DVD/BD files",       1,    Report         },
{  "Report",   "find files",              1,    Report         },
{  "Report",   "view backup hist",        1,    Report         },
{  "Report",   "save screen",             0,    saveScreen     },
{  "Restore",  "setup DVD/BD restore",    1,    RJedit         },
{  "Restore",  "list restore files",      1,    RJlist         },
{  "Restore",  "restore files",           1,    Restore        },
{  "DVD/BD",   "set DVD/BD device",       1,    setDVDdevice   },
{  "DVD/BD",   "set DVD/BD label",        1,    setDVDlabel    },
{  "DVD/BD",   "erase DVD/BD",            1,    eraseDVD       },
{  "DVD/BD",   "format DVD/BD",           1,    formatDVD      },
{  "DVD/BD",   "mount DVD/BD",            1,    mountDVD       },
{  "DVD/BD",   "unmount DVD/BD",          1,    unmountDVD     },
{  "DVD/BD",   "eject DVD/BD",            1,    ejectDVD       },
{  "DVD/BD",   "reset DVD/BD",            0,    resetDVD       },
{  "Help",     "about",                   0,    helpFunc       },
{  "Help",     "contents",                0,    helpFunc       }  };


//  dkopp main program

int main(int argc, char *argv[])
{
   PangoFontDescription    *deffont;                                       //  main window default font
   GtkWidget   *mbar, *tbar;                                               //  menubar and toolbar
   GtkWidget   *mFile, *mBackup, *mVerify, *mReport, *mRestore;
   GtkWidget   *mDVD, *mHelp;
   int         ii;

   zinitapp("dkopp",null);                                                 //  get install directories

   Fgui = 1;                                                               //  assume GUI
   clrun = 0;                                                              //  no command line run command
   *scrFile = 0;                                                           //  no script file
   *BJfile = 0;                                                            //  no backup job file
   
   main_argc = argc;                                                       //  save command line arguments
   main_argv = argv;

   for (ii = 1; ii < argc; ii++)                                           //  get command line options
   {
      if (strEqu(argv[ii],"-nogui")) Fgui = 0;                             //  command line operation       v.5.0
      else if (strEqu(argv[ii],"-job") && argc > ii+1)                     //  -job jobfile  (load job)
            strcpy(BJfile,argv[++ii]);
      else if (strEqu(argv[ii],"-run") && argc > ii+1)                     //  -run jobfile  (load and run job)
          { strcpy(BJfile,argv[++ii]); clrun++; }
      else if (strEqu(argv[ii],"-script") && argc > ii+1)                  //  -script scriptfile  (execute script)
            strcpy(scrFile,argv[++ii]);
      else  strcpy(BJfile,argv[ii]);                                       //  assume a job file and load it
   }

   if (! Fgui) {                                                           //  no GUI                       v.5.0
      mLog = mWin = 0;                                                     //  output goes to STDOUT
      initfunc(0);                                                         //  start job or script
      unmountDVD(0);                                                       //  unmount DVD/BD 
      ejectDVD(0);                                                         //  eject DVD/BD (may not work)
      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),dkopp_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

   deffont = pango_font_description_from_string(normfont);                 //  default font
   if (! deffont) {
      printf("monospace font not found \n");                               //  v.5.9
      return 0;
   }
   gtk_widget_modify_font(mLog,deffont);

   logBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));                //  get related text buffer
   gtk_text_buffer_set_text(logBuff,"", -1);

   mbar = create_menubar(mVbox);                                           //  create menu bar and menus
      mFile = add_menubar_item(mbar,"File",menufunc);                      
         add_submenu_item(mFile,"open job",menufunc);
         add_submenu_item(mFile,"open DVD/BD",menufunc);
         add_submenu_item(mFile,"edit job",menufunc);
         add_submenu_item(mFile,"show job",menufunc);
         add_submenu_item(mFile,"save job",menufunc);
         add_submenu_item(mFile,"run job",menufunc);
         add_submenu_item(mFile,"run DVD/BD",menufunc);
         add_submenu_item(mFile,"quit",menufunc);
      mBackup = add_menubar_item(mbar,"Backup",menufunc);
         add_submenu_item(mBackup,"full",menufunc);
         add_submenu_item(mBackup,"incremental",menufunc);
         add_submenu_item(mBackup,"accumulate",menufunc);
      mVerify = add_menubar_item(mbar,"Verify",menufunc);
         add_submenu_item(mVerify,"full",menufunc);
         add_submenu_item(mVerify,"incremental",menufunc);
         add_submenu_item(mVerify,"thorough",menufunc);
      mReport = add_menubar_item(mbar,"Report",menufunc);
         add_submenu_item(mReport,"get files for backup",menufunc);
         add_submenu_item(mReport,"diffs summary",menufunc);
         add_submenu_item(mReport,"diffs by directory",menufunc);
         add_submenu_item(mReport,"diffs by file",menufunc);
         add_submenu_item(mReport,"list files for backup",menufunc);
         add_submenu_item(mReport,"list DVD/BD files",menufunc);
         add_submenu_item(mReport,"find files",menufunc);
         add_submenu_item(mReport,"view backup hist",menufunc);
         add_submenu_item(mReport,"save screen",menufunc);
      mRestore = add_menubar_item(mbar,"Restore",menufunc);
         add_submenu_item(mRestore,"setup DVD/BD restore",menufunc);
         add_submenu_item(mRestore,"list restore files",menufunc);
         add_submenu_item(mRestore,"restore files",menufunc);
      mDVD = add_menubar_item(mbar,"DVD/BD",menufunc);
         add_submenu_item(mDVD,"set DVD/BD device",menufunc);
         add_submenu_item(mDVD,"set DVD/BD label",menufunc);
         add_submenu_item(mDVD,"mount DVD/BD",menufunc);
         add_submenu_item(mDVD,"unmount DVD/BD",menufunc);
         add_submenu_item(mDVD,"eject DVD/BD",menufunc);
         add_submenu_item(mDVD,"reset DVD/BD",menufunc);
         add_submenu_item(mDVD,"erase DVD/BD",menufunc);
         add_submenu_item(mDVD,"format DVD/BD",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

   add_toolbar_button(tbar,"root","become root","gtk-dialog-authentication",buttonfunc);
   add_toolbar_button(tbar,"edit job","edit backup job","editjob.png",buttonfunc);
   add_toolbar_button(tbar,"run job","run backup job","burn.png",buttonfunc);
   add_toolbar_button(tbar,"run DVD/BD","run job on DVD/BD","burn.png",buttonfunc);
   add_toolbar_button(tbar,"pause","pause running job","gtk-media-pause",buttonfunc);
   add_toolbar_button(tbar,"resume","resume running job","gtk-media-play",buttonfunc);
   add_toolbar_button(tbar,"kill job","kill running job","gtk-stop",buttonfunc);
   add_toolbar_button(tbar,"clear","clear screen","gtk-clear",buttonfunc);
   add_toolbar_button(tbar,"quit","quit dkopp","gtk-quit",buttonfunc);

   gtk_widget_show_all(mWin);                                              //  show all widgets

   G_SIGNAL(mWin,"destroy",quit_dkopp,0);                                  //  connect window destroy event
   
   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 *)
{
   int         ii;
   char        *home;
   time_t      datetime;
   
   menufunc(null,"Help");                                                  //  show version and license
   menufunc(null,"about");
   
   strcpy(userdir,get_zuserdir());                                         //  get temp file names
   sprintf(TFdiskfiles,"%s/diskfiles",userdir);
   sprintf(TFdvdfiles,"%s/dvdfiles",userdir);
   sprintf(TFfilepoop,"%s/filepoop",userdir);
   sprintf(TFjobfile,"%s/jobfile",userdir);
   sprintf(TFdatetime,"%s/datetime",userdir);
   sprintf(TFrestorefiles,"%s/restorefiles.sh",userdir);
   sprintf(TFrestoredirks,"%s/restoredirks.sh",userdir);

   datetime = time(0);
   printf("dkopp errlog %s \n",ctime(&datetime));

   menuLock = Fdialog = 0;                                                 //  initialize controls
   killFlag = pauseFlag = commFail = 0;
   strcpy(subprocName,"");
   strcpy(scriptParam,"");

   getDVDs();                                                              //  get available DVD/BD devices

   strcpy(BJdvd,dvddevs[0]);                                               //  DVD/BD device
   strcpy(dvdmp,"/media/dkopp");                                           //  default mount point    v.5.1
   dvdmpcc = strlen(dvdmp);                                                //  mount point cc
   BJcap = 4.0;                                                            //  default DVD/BD capacity, 4 GB    v.5.3
   BJspeed = 0;                                                            //  speed (0 = default)    v.4.5
   strcpy(dvdlabel,"dkopp");                                               //  default DVD/BD label      v.5.1
   strcpy(BJbmode,"full");                                                 //  backup mode
   strcpy(BJvmode,"full");                                                 //  verify mode
   BJval = 0;                                                              //  not validated
   BJmod = 0;                                                              //  not modified
   strcpy(BJdatefrom,"1970.01.01");                                        //  file age exclusion default   v.4.8
   BJtdate = 0;

   BJnx = 4;                                                               //  backup job include/exclude recs
   for (ii = 0; ii < BJnx; ii++) 
      BJinex[ii] = zmalloc(50);
   
   home = getenv("HOME");                                                  //  get "/home/username"
   if (! home) home = (char *) "/home/xxx";
   strcpy(BJinex[0],"# dkopp default backup job");                         //  initz. default backup specs
   sprintf(BJinex[1],"include %s/*",home);                                 //  include /home/username/*
   sprintf(BJinex[2],"exclude %s/.Trash/*",home);                          //  exclude /home/username/.Trash/*
   sprintf(BJinex[3],"exclude %s/.thumbnails/*",home);                     //  exclude /home/username/.thumbnails/*

   Dnf = Vnf = Rnf = Mfiles = 0;                                           //  file counts = 0
   Dbytes = Dbytes2 = Vbytes = Mbytes = 0.0;                               //  byte counts = 0

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

   strcpy(mediumDT,"unknown");                                             //  DVD/BD medium last backup date-time
   dvdtime = -1;                                                           //  DVD/BD device mod time
   dvdmtd = 0;                                                             //  DVD/BD not mounted

   if (*BJfile) {                                                          //  command line job file
      BJload(BJfile);
      if (commFail) return 0;
   }

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

   if (*scrFile) script_func(0);                                           //  command line script file

   return 0;
}


//  process toolbar button events (simulate menu selection)

void buttonfunc(GtkWidget *item, cchar *button)
{
   char     button2[20], *pp;
   
   strncpy0(button2,button,19);
   pp = strchr(button2,'\n');                                              //  replace \n with blank
   if (pp) *pp = ' ';

   menufunc(item,"button");                                                //  use menu function for button
   menufunc(item,button2);
   return;
}


//  process menu selection event

void menufunc(GtkWidget *, cchar *menu)
{
   static int     ii;
   static char    menu1[20] = "", menu2[40] = "";
   int            kk;
   char           command[100];

   for (ii = 0; ii < nmenu; ii++) 
         if (strEqu(menu,menus[ii].menu1)) break;                          //  mark top-menu selection
   if (ii < nmenu) { strcpy(menu1,menu); return;  }
   
   for (ii = 0; ii < nmenu; ii++) 
         if (strEqu(menu1,menus[ii].menu1) && 
             strEqu(menu,menus[ii].menu2)) break;                          //  mark sub-menu selection
   if (ii < nmenu) strcpy(menu2,menu);

   else {                                                                  //  no match to menus
      wprintf(mLog," *** bad command: %s \n",menu);
      commFail++;
      return;
   }
   
   if (menuLock && menus[ii].lock) {                                       //  no lock funcs can run parallel
      if (Fgui) 
         zmessageACK(mWin,0,"wait for current function to complete");
      return;
   }
   
   if (! menuLock) {   
      killFlag = pauseFlag = 0;                                            //  reset controls
      *subprocName = 0;
      commFail = 0;                                                        //  start with no errors
   }
   
   if (! *scrFile) {                                                       //  if not a script file,
      snprintf(command,99,"\n""command: %s > %s \n",menu1,menu2);          //    echo command to window
      wprintx(mLog,0,command,boldfont);
   }
   
   kk = ii;                                                                //  move to non-static memory    v.6.0

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

   return;
}


//  function to execute menu commands from a script file 

void script_func(void *)
{
   FILE     *fid;
   int      cc, Nth;
   char     buff[200], menu1[20], menu2[40];
   cchar    *pp;
   char     *bb;
   
   fid = fopen(scrFile,"r");                                               //  open file
   if (! fid) {
      wprintf(mLog," *** can't open script file: %s \n",scrFile);
      commFail++;
      *scrFile = 0;
      return;
   }

   while (true)
   {
      if (checkKillPause()) break;                                         //  exit script
      if (commFail) break;
      
      pp = fgets_trim(buff,199,fid,1);                                     //  read next record
      if (! pp) break;                                                     //  EOF

      wprintf(mLog,"\n""Script: %s \n",buff);                              //  write to log
      
      bb = strchr(buff,'#');                                               //  get rid of comments
      if (bb) *bb = 0;
      cc = strTrim(buff);                                                  //  and trailing blanks
      if (cc < 2) continue;

      *menu1 = *menu2 = 0;
      *scriptParam = 0;

      Nth = 1;                                                             //  parse menu1 > menu2 > parameter
      pp = strField(buff,'>',Nth++);
      if (pp) strncpy0(menu1,pp,20);
      pp = strField(buff,'>',Nth++);
      if (pp) strncpy0(menu2,pp,40);
      pp = strField(buff,'>',Nth++);
      if (pp) strncpy0(scriptParam,pp,200);
      
      strTrim(menu1);                                                      //  get rid of trailing blanks
      strTrim(menu2);

      if (strEqu(menu1,"exit")) break;
      
      menufunc(null,menu1);                                                //  simulate menu entries
      menufunc(null,menu2);

      while (Fdialog) sleep(1);                                            //  if dialog, wait for compl.
   }
   
   wprintf(mLog,"script exiting \n");
   fclose(fid);
   *scrFile = 0;
   return;
}


//  get root privileges if password is OK

int getroot(cchar * menu)                                                  //  v.5.4
{
   beroot(main_argc-1,main_argv+1);                                        //  does not return
   return 0;
}


//  quit dkopp, with last chance to save edits to backup job data

int quit_dkopp(cchar * menu)
{
   int      yn;
   
   if (! Fgui) return 0;                                                   //  v.5.5

   if (BJmod) {                                                            //  job data was modified
      yn = zmessageYN(mWin,"SAVE changes to dkopp job?");                  //  give user a chance to save mods
      if (yn) fileSave(null);
   }

   if (dvdmtd) {
      unmountDVD(0);                                                       //  unmount DVD/BD 
      ejectDVD(0);                                                         //  eject DVD/BD (may not work)
   }

   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");                                     //  already dead
         return 0;
      }
      
      if (killFlag) {                                                      //  redundant kill
         if (*subprocName) {
            wprintf(mLog," *** kill again: %s \n",subprocName);
            signalProc(subprocName,"kill");                                //  kill subprocess
         }
         else wprintf(mLog," *** waiting for function to quit \n");        //  or wait for function to die
         return 0;
      }

      wprintf(mLog," *** KILL current function \n");                       //  initial kill
      pauseFlag = 0;
      killFlag = 1;

      if (*subprocName) {
         signalProc(subprocName,"resume");
         signalProc(subprocName,"kill");
      }

      return 0;
   }

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

   if (strEqu(menu,"resume")) {
      pauseFlag = 0;
      if (*subprocName) signalProc(subprocName,"resume");
      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
   }

   if (killFlag) return 1;                                                 //  return true = stop now
   return 0;                                                               //  return false = continue
}


//  file open dialog - get backup job data from a file

int fileOpen(cchar * menu)
{
   char        *file;
   int         err = 0;
   
   if (*scriptParam) {                                                     //  get file from script
      strcpy(BJfile,scriptParam);
      *scriptParam = 0;
      err = BJload(BJfile);
      return err;
   }

   ++Fdialog;

   file = zgetfile1("open backup job","open",userdir,"hidden");            //  get file from user
   if (file) {
      if (strlen(file) > maxfcc-2) zappcrash("pathname too big");
      strcpy(BJfile,file);
      zfree(file);
      err = BJload(BJfile);                                                //  get job data from file
   }
   else err = 1;

   --Fdialog;
   return err;
}


//  file save dialog - save backup job data to a file

int fileSave(cchar * menu)
{
   char        *file;
   int         nstat, err = 0;
   
   if (*scriptParam) {                                                     //  get file from script
      strcpy(BJfile,scriptParam);
      *scriptParam = 0;
      BJstore(BJfile);
      return 0;
   }

   if (! BJval) {
      nstat = zmessageYN(mWin,"Job data not valid, save anyway?");
      if (! nstat) return 0;
   }

   ++Fdialog;

   if (! *BJfile) strcpy(BJfile,"dkopp.job");                              //  if no job file, use default
   file = zgetfile1("save backup job","save",BJfile,"hidden");
   if (file) {
      if (strlen(file) > maxfcc-2) zappcrash("pathname too big");
      strcpy(BJfile,file);
      zfree(file);
      err = BJstore(BJfile);
      if (! err) BJmod = 0;                                                //  job not modified
   }
   
   --Fdialog;
   return 0;
}


//  backup job data <<< file
//  errors not checked here are checked in BJvalidate()

int BJload(cchar * fspec)
{
   FILE           *fid;
   char           buff[1000];
   cchar          *fgs, *rtype, *rdata;
   char           rtype2[20];
   int            cc, Nth, nerrs;

   BJreset();                                                              //  clear old job from memory
   nerrs = 0;

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

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

   while (true)                                                            //  read file
   {
      fgs = fgets_trim(buff,998,fid,1);
      if (! fgs) break;                                                    //  EOF
      cc = strlen(buff);
      if (cc > 996) { 
         wprintf(mLog," *** input record too big \n");
         nerrs++;
         continue;
      }

      Nth = 1;
      rtype = strField(buff,' ',Nth++);                                    //  parse 1st field, record type
      if (! rtype) rtype = "#";                                            //  blank record is comment
      strncpy0(rtype2,rtype,19);
      strToLower(rtype2);

      if (strEqu(rtype2,"device")) {
         rdata = strField(buff,' ',Nth++);                                 //  DVD/BD device: /dev/dvd
         if (rdata) strncpy0(BJdvd,rdata,19);
         continue;
      }

      if (strEqu(rtype2,"dvdcap")) {
         rdata = strField(buff,' ',Nth++);                                 //  DVD/BD capacity, GB
         if (rdata) convSD(rdata,BJcap);
         rdata = strField(buff,' ',Nth++);                                 //  DVD/BD write speed (x 1.38 MB/sec)
         convSI(rdata,BJspeed);                                            //  (0 = default)         v.4.5
         continue;
      }

      if (strEqu(rtype2,"backup")) {
         rdata = strField(buff,' ',Nth++);                                 //  backup mode
         if (rdata) {
            strncpy0(BJbmode,rdata,19);
            strToLower(BJbmode);
         }
         continue;
      }

      if (strEqu(rtype2,"verify")) {
         rdata = strField(buff,' ',Nth++);                                 //  verify mode
         if (rdata) {
            strncpy0(BJvmode,rdata,19);
            strToLower(BJvmode);
         }
         continue;
      }
      
      if (strEqu(rtype2,"datefrom")) {
         rdata = strField(buff,' ',Nth++);                                 //  file mod date selection    v.4.6
         if (rdata) strncpy0(BJdatefrom,rdata,11);
         continue;
      }
      
      if (strcmpv(rtype2,"include","exclude","#",null)) {
         BJinex[BJnx] = strdupz(buff);                                     //  include/exclude or comment rec. 
         if (++BJnx >= maxnx) {
            wprintf(mLog," *** exceed %d include/exclude recs \n",maxnx);
            nerrs++;
            break;
         }
         continue;
      }
      
      wprintf(mLog," *** unrecognized record: %s \n",buff);
      continue;
   }
   
   fclose(fid);                                                            //  close file
   BJmod = 0;                                                              //  new job, not modified

   BJvalidate(0);                                                          //  validation checks, set BJval
   if (! nerrs && BJval) return 0;
   BJval = 0;
   commFail++;
   return 1;
}


//  backup job data >>> file

int BJstore(cchar * fspec, int ndvd)
{
   FILE     *fid;
   int      ii;

   fid = fopen(fspec,"w");                                                 //  open file
   if (! fid) { 
      wprintf(mLog," *** cannot open file: %s \n",fspec); 
      commFail++;
      return 1; 
   }
   
   fprintf(fid,"device %s \n",BJdvd);                                      //  device /dev/dvd
   fprintf(fid,"dvdcap %.1f %d \n",BJcap,BJspeed);                         //  dvdcap N.N N (DVD/BD capacity, speed)
   fprintf(fid,"backup %s \n",BJbmode);                                    //  backup full/incremental/accumulate
   fprintf(fid,"verify %s \n",BJvmode);                                    //  verify full/incremental/thorough
   fprintf(fid,"datefrom %s \n",BJdatefrom);                               //  file mod date selection   v.4.6

   if (! ndvd)
      for (ii = 0; ii < BJnx; ii++)                                        //  output all include/exclude recs
         fprintf(fid,"%s \n",BJinex[ii]);

   else {                                                                  //  output only recs for one DVD/BD medium 
      for (ii = 0; ii < BJnx && BJdvdno[ii] < ndvd; ii++);                 //    of multi-volume set
      for ( ; ii < BJnx; ii++)
         if (BJdvdno[ii] <= ndvd)                                          //  matching includes (dvdno = ndvd)
            fprintf(fid,"%s \n",BJinex[ii]);                               //   + all following excludes (dvdno = 0)
   }

   fclose(fid);
   return 0;
}   


//  backup job data <<< DVD/BD job file
//  get job file from prior backup to this same medium

int BJvload(cchar * menu)
{
   char     vjfile[100];
   
   BJreset();                                                              //  reset job data   

   mountDVD(0);                                                            //  (re) mount DVD/BD
   if (! dvdmtd) { 
      commFail++; 
      return 1; 
   }

   strcpy(vjfile,dvdmp);                                                   //  dvd mount point
   strcat(vjfile,V_JOBFILE);                                               //  + dvd job file
   BJload(vjfile);                                                         //  load job file (BJval set)
   if (BJval) return 0;
   commFail++;
   return 1;
}


//  edit dialog for backup job data

int BJedit(cchar * menu)
{
   zdialog        *zd;
   
   ++Fdialog;

   zd = zdialog_new("edit backup job",mWin,"browse","done","clear","cancel",null);

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=8");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb3","hb1",0,"homog");
   zdialog_add_widget(zd,"vbox","vb4","hb1",0,"homog");
   zdialog_add_widget(zd,"hbox","hb2","vb1");
   zdialog_add_widget(zd,"vbox","vb21","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb22","hb2",0,"homog|space=5");
   zdialog_add_widget(zd,"hbox","hb3","vb1",0,"space=8");

   zdialog_add_widget(zd,"label","labdev","vb21","DVD/BD device ");        //  DVD/BD device   [______________][v]
   zdialog_add_widget(zd,"label","labcap","vb21","capacity GB");           //  capacity GB  [___]
   zdialog_add_widget(zd,"label","labspeed","vb21","write speed");         //  write speed  [__]  (x 1.38 MB/sec)
   zdialog_add_widget(zd,"comboE","entdvd","vb22",BJdvd);
   zdialog_add_widget(zd,"hbox","hb21","vb22");
   zdialog_add_widget(zd,"entry","entcap","hb21",0,"scc=5");
   zdialog_add_widget(zd,"hbox","hb22","vb22");
   zdialog_add_widget(zd,"entry","entspeed","hb22",0,"scc=3");
   zdialog_add_widget(zd,"label","labxmb","hb22","(x 1.38 MB/sec)","space=5");

   zdialog_add_widget(zd,"button","bopen","hb3","open job file");          //  [open job] [DVD/BD job] [save as]
   zdialog_add_widget(zd,"button","bdvd","hb3","open DVD/BD job");
   zdialog_add_widget(zd,"button","bsave","hb3"," save as ");

   zdialog_add_widget(zd,"label","labbmode","vb3","Backup Mode        ");
   zdialog_add_widget(zd,"label","labvmode","vb4","Verify Mode        ");
   zdialog_add_widget(zd,"radio","bmrb1","vb3","full");                    //  Backup Mode         Verify Mode
   zdialog_add_widget(zd,"radio","bmrb2","vb3","incremental");             //  (o) full            (o) full
   zdialog_add_widget(zd,"radio","bmrb3","vb3","accumulate");              //  (o) incremental     (o) incremental
   zdialog_add_widget(zd,"radio","vmrb1","vb4","full");                    //  (o) accumulate      (o) thorough
   zdialog_add_widget(zd,"radio","vmrb2","vb4","incremental");             //  file date from:     [ yyyy.mm.dd ]
   zdialog_add_widget(zd,"radio","vmrb3","vb4","thorough");
   zdialog_add_widget(zd,"label","labdate","vb3","file date from:");
   zdialog_add_widget(zd,"entry","entdate","vb4","yyyy.mm.dd","scc=10");

   zdialog_add_widget(zd,"label","space","vb3",0,"expand");
   zdialog_add_widget(zd,"label","space","vb4",0,"expand");

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

   BJedit_stuff(zd);                                                       //  stuff dialog widgets with job data

   zdialog_resize(zd,0,500);
   zdialog_run(zd,BJedit_event);                                           //  run dialog
   return 0;
}


//  edit dialog event function

int BJedit_event(zdialog *zd, cchar *event)
{
   int      zstat, err = 0;
   
   zstat = zd->zstat;
   zd->zstat = 0;                                                          //  dialog may continue
   
   if (zstat)
   {
      if (zstat == 1) {                                                    //  browse, do file-chooser dialog
         fc_dialog("/home");
         return 0;
      }

      if (zstat == 2) {                                                    //  done
         BJedit_fetch(zd);                                                 //  get all job data from dialog widgets
         if (! BJval) commFail++;
         zdialog_free(zd);                                                 //  destroy dialog
         --Fdialog;
         return 0;
      }

      if (zstat == 3) {
         wclear(editwidget);                                               //  clear include/exclude recs
         return 0;
      }

      zdialog_free(zd);                                                    //  cancel
      --Fdialog;
      return 0;
   }

   if (strEqu(event,"bopen")) {
      err = fileOpen("");                                                  //  get job file from user
      if (! err) BJedit_stuff(zd);                                         //  stuff dialog widgets
   }

   if (strEqu(event,"bdvd")) {
      err = BJvload("");                                                   //  get job file on DVD/BD
      if (! err) BJedit_stuff(zd);                                         //  stuff dialog widgets
   }

   if (strEqu(event,"bsave")) {
      BJedit_fetch(zd);                                                    //  get job data from dialog widgets
      fileSave("");                                                        //  save to file
   }

   return 0;
}


//  backup job data in memory >>> job edit dialog widgets

int BJedit_stuff(zdialog * zd)
{
   int      ii;

   for (ii = 0; ii < ndvds; ii++)
      zdialog_cb_app(zd,"entdvd",dvddevdesc[ii]);
                                                                           //  remove mount point get/stuff   v.5.3
   zdialog_stuff(zd,"entcap",BJcap);
   if (BJspeed > 0) zdialog_stuff(zd,"entspeed",BJspeed);                  //  v.4.5

   if (strEqu(BJbmode,"full")) zdialog_stuff(zd,"bmrb1",1);
   if (strEqu(BJbmode,"incremental")) zdialog_stuff(zd,"bmrb2",1);
   if (strEqu(BJbmode,"accumulate")) zdialog_stuff(zd,"bmrb3",1);
   if (strEqu(BJvmode,"full")) zdialog_stuff(zd,"vmrb1",1);
   if (strEqu(BJvmode,"incremental")) zdialog_stuff(zd,"vmrb2",1);
   if (strEqu(BJvmode,"thorough")) zdialog_stuff(zd,"vmrb3",1);
   zdialog_stuff(zd,"entdate",BJdatefrom);                                 //  file mod date selection   v.4.6

   editwidget = zdialog_widget(zd,"edinex");
   wclear(editwidget);
   for (int ii = 0; ii < BJnx; ii++) 
      wprintf(editwidget,"%s""\n",BJinex[ii]);
   
   return 0;
}


//  job edit dialog widgets  >>>  backup job data in memory

int BJedit_fetch(zdialog * zd)
{
   int            ii, ftf;
   char           text[40], *pp;

   BJreset();                                                              //  reset job data

   zdialog_fetch(zd,"entdvd",text,19);                                     //  get DVD/BD device
   strncpy0(BJdvd,text,19);
   pp = strchr(BJdvd,' ');
   if (pp) *pp = 0;
                                                                           //  remove mount point fetch/save    v.5.3
   zdialog_fetch(zd,"entcap",BJcap);                                       //  capacity GB
   zdialog_fetch(zd,"entspeed",BJspeed);                                   //  speed, x 1.38 MB/sec

   zdialog_fetch(zd,"bmrb1",ii); if (ii) strcpy(BJbmode,"full");           //  backup mode
   zdialog_fetch(zd,"bmrb2",ii); if (ii) strcpy(BJbmode,"incremental");
   zdialog_fetch(zd,"bmrb3",ii); if (ii) strcpy(BJbmode,"accumulate");

   zdialog_fetch(zd,"vmrb1",ii); if (ii) strcpy(BJvmode,"full");           //  verify mode
   zdialog_fetch(zd,"vmrb2",ii); if (ii) strcpy(BJvmode,"incremental");
   zdialog_fetch(zd,"vmrb3",ii); if (ii) strcpy(BJvmode,"thorough");
   
   zdialog_fetch(zd,"entdate",BJdatefrom,11);                              //  file mod date selection    v.4.6

   for (ftf = 1;;)
   {
      pp = wscanf(editwidget,ftf);                                         //  include/exclude recs.
      if (! pp) break;
      strTrim(pp);                                                         //  remove trailing blanks
      BJinex[BJnx] = strdupz(pp);                                          //  copy new record
      if (++BJnx >= maxnx) {
         wprintf(mLog," *** exceed %d include/exclude recs \n",maxnx); 
         break;
      }
   }
   
   BJmod++;                                                                //  job modified
   BJvalidate(0);                                                          //  check for errors, set BJval   
   return 0;
}


//  perform DVD/BD backup using growisofs utility

int Backup(cchar *menu)
{
   strcpy(mbmode,"");
   strcpy(mvmode,"");
   
   if (strcmpv(menu,"full","incremental","accumulate",null))               //  backup only
      strcpy(mbmode,menu);

   if (strEqu(menu,"run DVD/BD")) BJvload(null);                           //  load job file from DVD/BD if req.

   if (strcmpv(menu,"run job","run DVD/BD",null)) {                        //  if run job or job on DVD/BD,
      if (BJval) {                                                         //   and valid job file,
         strcpy(mbmode,BJbmode);                                           //    use job file backup & verify modes
         strcpy(mvmode,BJvmode);
      }
   }

   if (! BJval) {                                                          //  check for errors
      wprintf(mLog," *** no valid backup job \n");
      goto backup_done;
   }

   if (strEqu(mbmode,"full")) FullBackup(mvmode);                          //  full backup (+ verify)
   else  IncrBackup(mbmode,mvmode);                                        //  incremental / accumulate (+ verify)

backup_done:
   if (Fgui) wprintf(mLog,"ready \n");                                     //  v.5.0
   return 0;
}


//  full backup using multiple DVD/BD media if required 

int FullBackup(cchar * BJvmode)
{
   FILE        *fid = 0;
   int         gerr, ii, zstat;
   char        command[200], Nspeed[20] = "";
   char        *dfile, vfile[maxfcc], *mbytes;
   double      secs, bspeed, time0;

   dGetFiles();                                                            //  get files for backup
   if (Dnf == 0) {
      wprintf(mLog," *** nothing to back-up \n");
      goto backup_fail;
   }

   vFilesReset();                                                          //  reset DVD/BD files data

   wprintx(mLog,0,"\n""begin full backup \n",boldfont);
   wprintf(mLog," files: %d  bytes: %.0f \n",Dnf,Dbytes);                  //  files and bytes to copy
   
   if (! *dvdlabel) strcpy(dvdlabel,"dkopp");                              //  if no label, default "dkopp"    v.5.1

   for (dvdnum = 1; dvdnum <= BJndvd; dvdnum++)                            //  loop for each DVD/BD
   {
      if (dvdnum > 1) ejectDVD(0);                                         //  eject prior DVD/BD
      
      if (! *scrFile || dvdnum > 1)                                        //  if script file avoid dialog  v.4.8
      if (Fgui) zmessageACK(mWin,0,
                "Insert DVD/BD medium no. %d \n"                           //  ask for next DVD/BD
                "and wait for desktop icon", dvdnum);                      //    if GUI mode                v.5.0
      unmountDVD(0);                                                       //  no mount for full backup     v.4.7

      BJstore(TFjobfile,dvdnum);                                           //  copy job file (this DVD/BD) to temp file
      save_filepoop();                                                     //  + owner and permissions to temp file
      writeDT();                                                           //  create date-time & usage temp file

      fid = fopen(TFdiskfiles,"w");                                        //  temp file for growisofs path-list
      if (! fid) {
         wprintf(mLog," *** cannot open /tmp scratch file \n");
         goto backup_fail;
      }

      fprintf(fid,"%s=%s\n",V_JOBFILE +1,TFjobfile);                       //  add job file to growisofs list
      fprintf(fid,"%s=%s\n",V_FILEPOOP +1,TFfilepoop);                     //  add directory poop file
      fprintf(fid,"%s=%s\n",V_DATETIME +1,TFdatetime);                     //  add date-time file

      Dbytes2 = 0.0;
      for (ii = 0; ii < Dnf; ii++)                                         //  process all files for backup
      {
         if (Drec[ii].dvd != dvdnum) continue;                             //  screen for DVD/BD no.
         dfile = Drec[ii].file;                                            //  add to growisofs path-list
         repl_1str(dfile,vfile,"=","\\\\=");                               //  replace "=" with "\\=" in file name
         fprintf(fid,"%s=%s\n",vfile+1,dfile);                             //  directories/file=/directories/file
         Dbytes2 += Drec[ii].size;
      }

      fclose(fid);

      if (BJspeed > 0) sprintf(Nspeed,"-speed=%d",BJspeed);                //  v.4.5

      mbytes = formatKBMB(Dbytes2,4);                                      //  v.5.2
      wprintf(mLog," writing DVD/BD medium %d of %d, %s \n", 
                                       dvdnum, BJndvd, mbytes);

      start_timer(time0);                                                  //  start timer for growisofs

      sprintf(command,                                                     //  build growisofs command line
         "/usr/bin/growisofs -Z %s %s -r -graft-points "
         "-iso-level 4 -gui -V \"%s\" %s -path-list %s 2>&1",              //  label in quotes     v.5.6
          BJdvd,Nspeed,dvdlabel,gforce,TFdiskfiles);

   backup_retry:

      gerr = do_shell("growisofs", command);                               //  do growisofs, echo outputs
      if (checkKillPause()) goto backup_fail;                              //  killed by user
      if (gerr) {
         if (! Fgui) goto backup_fail;
         zstat = zdialog_choose("choose",mWin,"growisofs error",           //  manual compensation for growisofs
                           "abort","retry","ignore (continue)",null);      //    and/or gnome bugs      v.5.9.1
         if (zstat == 1) goto backup_fail;
         if (zstat == 2) goto backup_retry;
      }

      secs = get_timer(time0);                                             //  output statistics
      wprintf(mLog," backup time: %.0f secs \n",secs);
      bspeed = Dbytes2/1000000.0/secs;
      wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed);
      wprintf(mLog," backup complete \n");
      if (BJndvd > 1) wprintf(mLog," (DVD/BD medium no. %d) \n",dvdnum);
      
      ejectDVD(0);                                                         //  DVD may be hung after growisofs      
      sleep(5);

   verify_retry:

      if (*BJvmode)                                                        //  do verify if requested 
      {
         mountDVD(0);                                                      //  test if DVD hung
         if (! dvdmtd) {
            zstat = zdialog_choose("choose",mWin,"DVD mount failure",
                           "abort","retry","ignore (continue)",null);
            if (zstat == 1) goto backup_fail;
            if (zstat == 2) goto verify_retry;
         }

         Verify(BJvmode);                                                  //  verify this DVD
         if (commFail) {
            zstat = zdialog_choose("choose",mWin,"verify error",
                           "abort","retry","ignore (continue)",null);
            if (zstat == 1) goto backup_fail;
            if (zstat == 2) goto verify_retry;

            wprintf(mLog," backup is being repeated \n",dvdnum);
            commFail = 0;
            --dvdnum;                                                      //  repeat same DVD, salvage job
            continue;
         }
      }

      createBackupHist();                                                  //  create backup history file
   }
   
   wprintf(mLog," backup job complete \n");
   return 0;

backup_fail:

   commFail++;
   secs = get_timer(time0);                                                //  output statistics even if failed   v.5.8
   wprintf(mLog," backup time: %.0f secs \n",secs);
   bspeed = Dbytes2/1000000.0/secs;
   wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed);
   wprintx(mLog,0," *** BACKUP FAILED \n",boldfont);
   wprintf(mLog," media may be OK: check with Verify \n");                 //  v.5.3
   return 0;
}


//  incremental / accumulate backup (one DVD/BD only)

int IncrBackup(cchar * BJbmode, cchar * BJvmode)
{
   FILE           *fid = 0;
   int            gerr, ii, zstat;
   char           command[200], Nspeed[20] = "";
   char           *dfile, vfile[maxfcc], disp;
   double         secs, bspeed;
   double         time0;

   mountDVD(0);                                                            //  requires successful mount
   if (! dvdmtd) goto backup_fail;

   dGetFiles();                                                            //  get files for backup
   vGetFiles();                                                            //  get DVD/BD files
   setFileDisps();                                                         //  file disps: new mod del unch

   if (! Dnf) {
      wprintf(mLog," *** no files for backup \n");
      goto backup_fail;
   }

   if (! Vnf) {
      wprintf(mLog," *** no DVD/BD files \n");
      goto backup_fail;
   }

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

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

   if (! *dvdlabel) strcpy(dvdlabel,"dkopp");                              //  if no label, default "dkopp"    v.5.1

   fid = fopen(TFdiskfiles,"w");                                           //  temp file for growisofs path-list
   if (! fid) {
      wprintf(mLog," *** cannot open /tmp scratch file \n");
      goto backup_fail;
   }

   BJstore(TFjobfile);                                                     //  copy job file to temp file
   save_filepoop();                                                        //  + file owner & permissions
   writeDT();                                                              //  create date-time & usage temp file

   fprintf(fid,"%s=%s\n",V_JOBFILE +1,TFjobfile);                          //  add job file to growisofs list
   fprintf(fid,"%s=%s\n",V_FILEPOOP +1,TFfilepoop);                        //  add directory poop file
   fprintf(fid,"%s=%s\n",V_DATETIME +1,TFdatetime);                        //  add date-time file

   for (ii = 0; ii < Dnf; ii++) {                                          //  process new and modified disk files
      disp = Drec[ii].disp;
      if ((disp == 'n') || (disp == 'm')) {                                //  new or modified file
         dfile = Drec[ii].file;                                            //  add to growisofs path-list
         repl_1str(dfile,vfile,"=","\\\\=");                               //  replace "=" with "\\=" in file name
         fprintf(fid,"%s=%s\n",vfile+1,dfile);                             //  directories/file=/directories/file
         Drec[ii].ivf = 1;                                                 //  set flag for incr. verify
      }
   }

   if (strEqu(BJbmode,"incremental")) {                                    //  incremental backup (not accumulate)
      for (ii = 0; ii < Vnf; ii++) {                                       //  process deleted files still on DVD/BD
         if (Vrec[ii].disp == 'd') {
            dfile = Vrec[ii].file;                                         //  add to growisofs path-list
            repl_1str(dfile,vfile,"=","\\\\=");                            //  replace "=" with "\\=" in file name
            fprintf(fid,"%s=%s\n",vfile+1,"/dev/null");                    //  directories/file=/dev/null
         }
      }
   }

   fclose(fid);

   if (BJspeed > 0) sprintf(Nspeed,"-speed=%d",BJspeed);                   //  v.4.5

   start_timer(time0);                                                     //  start timer for growisofs

   sprintf(command,"/usr/bin/growisofs -M %s %s -r -graft-points "         //  build growisofs command line
                     "-iso-level 4 -gui -V %s %s -path-list %s 2>&1",
                     BJdvd,Nspeed,dvdlabel,gforce,TFdiskfiles);

backup_retry:

   gerr = do_shell("growisofs", command);                                  //  do growisofs, echo outputs
   if (checkKillPause()) goto backup_fail;                                 //  killed by user
   if (gerr) {
      zstat = zdialog_choose("choose",mWin,"growisofs error",              //  manual compensation for growisofs
                        "abort","retry","ignore (continue)",null);         //    and/or gnome bugs      v.5.9.1
      if (zstat == 1) goto backup_fail;
      if (zstat == 2) goto backup_retry;
   }

   secs = get_timer(time0);                                                //  output statistics
   wprintf(mLog," backup time: %.0f secs \n",secs);
   bspeed = Mbytes/1000000.0/secs;
   wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed);
   wprintf(mLog," backup complete \n");

   vFilesReset();                                                          //  reset DVD/BD files

   ejectDVD(0);                                                            //  DVD may be hung after growisofs      
   sleep(5);

verify_retry:

   if (*BJvmode)                                                           //  do verify if requested 
   {
      mountDVD(0);                                                         //  test if DVD/BD hung
      if (! dvdmtd) {
         zstat = zdialog_choose("choose",mWin,"DVD mount failure",
                        "abort","retry","ignore (continue)",null);
         if (zstat == 1) goto backup_fail;
         if (zstat == 2) goto verify_retry;
      }

      Verify(BJvmode);                                                     //  verify new files on DVD/BD
      if (commFail) {
         zstat = zdialog_choose("choose",mWin,"verify error",
                        "abort","retry","ignore (continue)",null);
         if (zstat == 1) goto backup_fail;
         if (zstat == 2) goto verify_retry;
      }
   }

   createBackupHist();                                                     //  create backup history file

   return 0;

backup_fail:
   commFail++;
   wprintx(mLog,0," *** BACKUP FAILED \n",boldfont);
   vFilesReset();
   return 0;
}


//  verify DVD/BD disc data integrity

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

   vGetFiles();                                                            //  get DVD/BD files
   wprintf(mLog," %d files on DVD/BD \n",Vnf);
   if (! Vnf) goto verify_exit;

   vfiles = verrs = cerrs = 0;
   vbytes = 0.0;

   start_timer(time0);

   if (strEqu(menu,"full"))                                                //  verify all files are readable
   {
      wprintx(mLog,0,"\n""verify ALL files on DVD/BD \n",boldfont);
      if (Fgui) wprintf(mLog,"\n\n");                                      //  v.5.0

      for (ii = 0; ii < Vnf; ii++)
      {
         if (checkKillPause()) goto verify_exit;

         filespec = Vrec[ii].file;                                         //  /home/.../file.ext
         track_filespec(filespec);                                         //  track progress on screen
         errmess = checkFile(filespec,0,dcc1);                             //  check file, get length
         if (errmess) track_filespec_err(filespec,errmess);                //  log errors
         if (errmess) verrs++;
         vfiles++;
         vbytes += dcc1;

         if (verrs + cerrs > 100) {
            wprintx(mLog,0," *** OVER 100 ERRORS, GIVING UP *** \n",boldfont);
            goto verify_exit;
         }
      }
   }
   
   if (strEqu(menu,"incremental"))                                         //  verify files in prior incr. backup
   {
      wprintx(mLog,0,"\n""verify files in prior incremental backup \n",boldfont);

      for (ii = 0; ii < Dnf; ii++)
      {
         if (checkKillPause()) goto verify_exit;
         if (! Drec[ii].ivf) continue;                                     //  skip if not in prior incr. backup

         filespec = Drec[ii].file;
         wprintf(mLog,"  %s \n",kleenex(filespec));                        //  output filespec
         errmess = checkFile(filespec,0,dcc1);                             //  check file on DVD/BD, get length
         if (errmess) wprintf(mLog,"  *** %s \n",errmess);
         if (errmess) verrs++;
         vfiles++;
         vbytes += dcc1;

         if (verrs + cerrs > 100) {
            wprintx(mLog,0," *** OVER 100 ERRORS, GIVING UP *** \n",boldfont);
            goto verify_exit;
         }
      }
   }
   
   if (strEqu(menu,"thorough"))                                            //  compare DVD/BD to disk files
   {
      wprintx(mLog,0,"\n Read and verify ALL files on DVD/BD. \n",boldfont);
      wprintf(mLog," Compare to disk files with matching names and mod times.\n");
      if (Fgui) wprintf(mLog,"\n\n");                                      //  v.5.0
      
      for (ii = 0; ii < Vnf; ii++)                                         //  process DVD/BD files
      {
         if (checkKillPause()) goto verify_exit;

         filespec = Vrec[ii].file;                                         //  corresp. file name on disk
         track_filespec(filespec);                                         //  track progress on screen

         comp = 0;
         if (stat64(filespec,&filestat) == 0) {                            //  disk file exists?
            mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano;   //  yes, get file mod time
            diff = fabs(mtime - Vrec[ii].mtime);                           //  compare to DVD/BD file mod time
            if (diff < modtimetolr) comp = 1;                              //  equal
            dfiles1++;                                                     //  count matching disk names
            dfiles2 += comp;                                               //  count matching names and mod times
         }

         errmess = checkFile(filespec,comp,dcc1);                          //  check DVD/BD file, opt. compare to disk
         if (errmess) track_filespec_err(filespec,errmess);                //  log errors

         if (errmess) {
            if (strstr(errmess,"compare")) cerrs++;                        //  file compare error
            else  verrs++;
         }

         vfiles++;
         vbytes += dcc1;

         if (verrs + cerrs > 100) {
            wprintx(mLog,0," *** OVER 100 ERRORS, GIVING UP *** \n",boldfont);
            goto verify_exit;
         }
      }
   }

   wprintf(mLog," DVD/BD files: %d  bytes: %.0f \n",vfiles,vbytes);
   wprintf(mLog," DVD/BD read errors: %d \n",verrs);

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

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

   if (verrs + cerrs) wprintx(mLog,0," *** THERE WERE ERRORS *** \n",boldfont);
   else wprintf(mLog," NO ERRORS \n");

verify_exit:
   if (! Vnf) wprintf(mLog," *** no files on DVD/BD \n");
   if (! Vnf) commFail++;
   if (verrs + cerrs) commFail++;
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0
   return 0;
}


//  Reports menu function 

int Report(cchar * menu)
{
   if (strEqu(menu, "get files for backup")) get_current_files(0);
   if (strEqu(menu, "diffs summary")) report_summary_diffs(0);
   if (strEqu(menu, "diffs by directory")) report_directory_diffs(0);
   if (strEqu(menu, "diffs by file")) report_file_diffs(0);
   if (strEqu(menu, "list files for backup")) list_current_files(0);
   if (strEqu(menu, "list DVD/BD files")) list_DVD_files(0);
   if (strEqu(menu, "find files")) find_files(0);
   if (strEqu(menu, "view backup hist")) view_backup_hist(0);
   return 0;
}


//  refresh files for backup and report summary statistics per include/exclude statement

int get_current_files(cchar *menu)
{
   char     *bytes;
   int      ii;

   dFilesReset();                                                          //  force refresh
   dGetFiles();                                                            //  get disk files

   if (! BJval) {
      wprintf(mLog," *** backup job is invalid \n");
      goto report_exit;
   }

   wprintx(mLog,0,"\n  files    bytes  disk  include/exclude filespec \n",boldfont);

   for (ii = 0; ii < BJnx; ii++)
   {
      bytes = formatKBMB(BJbytes[ii],3);                                   //  v.5.2
      if (BJfiles[ii] > 0) wprintf(mLog," %6d %9s %3d", BJfiles[ii], bytes, BJdvdno[ii]);
      if (BJfiles[ii] < 0) wprintf(mLog," %6d %9s    ", BJfiles[ii], bytes);
      if (BJfiles[ii] == 0) wprintf(mLog,"                     ");
      wprintf(mLog,"   %s \n",BJinex[ii]);
   }

   bytes = formatKBMB(Dbytes,4);                                           //  v.5.2
   wprintf(mLog," %6d %9s       TOTAL   %d disks \n", Dnf, bytes, BJndvd);

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


//  report disk:DVD/BD differences summary

int report_summary_diffs(cchar *menu)
{
   char     *bytes;

   if (! BJval) {
      wprintf(mLog," *** backup job is invalid \n");
      goto report_exit;
   }

   dGetFiles();
   vGetFiles();
   setFileDisps();

   wprintf(mLog,"\n disk files: %d  DVD/BD files: %d \n",Dnf,Vnf);
   wprintf(mLog,"\n Differences between DVD/BD and files on disk: \n");
   wprintf(mLog," %7d  disk files not on DVD/BD - new \n",nnew);
   wprintf(mLog," %7d  files on disk and DVD/BD - unchanged \n",nunc);
   wprintf(mLog," %7d  files on disk and DVD/BD - modified \n",nmod);
   wprintf(mLog," %7d  DVD/BD files not on disk - deleted \n",ndel);
   
   bytes = formatKBMB(Mbytes,4);                                           //  v.5.2
   wprintf(mLog," Total differences: %d files  %s \n",nnew+ndel+nmod,bytes);

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


//  report disk:DVD/BD differences by directory, summary statistics

int report_directory_diffs(cchar *menu)
{
   int         kfiles, knew, kdel, kmod;
   int         dii, vii, comp;
   char        *pp, *pdirk, *bytes, ppdirk[maxfcc];
   double      nbytes;

   if (! BJval) {
      wprintf(mLog," *** backup job is invalid \n");
      goto report_exit;
   }

   dGetFiles();
   vGetFiles();
   setFileDisps();

   SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D');                   //  re-sort, directories first
   SortFileList((char *) Vrec, sizeof(vfrec), Vnf, 'D');

   wprintf(mLog,"\n Disk:DVD/BD differences by directory \n");

   wprintf(mLog,"   new   mod   del   bytes   directory \n");
   
   nbytes = kfiles = knew = kmod = kdel = 0;
   dii = vii = 0;

   while ((dii < Dnf) || (vii < Vnf))                                      //  scan disk and DVD/BD files in parallel
   {   
      if ((dii < Dnf) && (vii == Vnf)) comp = -1;
      else if ((dii == Dnf) && (vii < Vnf)) comp = +1;
      else comp = filecomp(Drec[dii].file, Vrec[vii].file);
      
      if (comp > 0) pdirk = Vrec[vii].file;                                //  get file on DVD/BD or disk
      else pdirk = Drec[dii].file;

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

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

      else if (comp > 0) {                                                 //  unmatched DVD/BD file: deleted
         kdel++;                                                           //  count deleted file
         kfiles++;
         vii++;
      }

      else if (comp == 0) {                                                //  file present on disk and DVD/BD
         if (Drec[dii].disp == 'm') {
            kmod++;                                                        //  count modified file
            nbytes += Drec[dii].size;
            kfiles++;
         }
         dii++;                                                            //  other: u = unchanged
         vii++;
      }
   }

   if (kfiles > 0) {
      bytes = formatKBMB(nbytes,3);                                        //  totals from last directory  v.5.2
      wprintf(mLog," %5d %5d %5d %8s  %s \n",knew,kmod,kdel,bytes,ppdirk);
   }

   SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A');                   //  restore ascii sort
   SortFileList((char *) Vrec, sizeof(vfrec), Vnf, 'A');

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


//  report disk:DVD/BD differences by file (new, modified, deleted)

int report_file_diffs(cchar *menu)
{
   int      dii, vii;

   if (! BJval) {
      wprintf(mLog," *** backup job is invalid \n");
      goto report_exit;
   }

   report_summary_diffs(0);                                                //  report summary first

   wprintf(mLog,"\n Detailed list of disk:DVD/BD differences: \n");

   wprintf(mLog,"\n %d new files (on disk, not on DVD/BD) \n",nnew);

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

   wprintf(mLog,"\n %d modified files (disk and DVD/BD files are different) \n",nmod);

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

   wprintf(mLog,"\n %d deleted files (on DVD/BD, not on disk) \n",ndel);

   for (vii = 0; vii < Vnf; vii++) 
   {
      if (Vrec[vii].disp != 'd') continue;
      wprintf(mLog,"  %s \n",kleenex(Vrec[vii].file));
      if (checkKillPause()) goto report_exit;
   }

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


//  list all files for backup

int list_current_files(cchar *menu)
{
   int      dii;

   if (! BJval) {
      wprintf(mLog," *** backup job is invalid \n");
      goto report_exit;
   }

   wprintf(mLog,"\n List all files for backup: \n");

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

   for (dii = 0; dii < Dnf; dii++)
   {
      if (checkKillPause()) break;
      wprintf(mLog," %s \n",kleenex(Drec[dii].file));
   }

report_exit:
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  list all files on mounted DVD/BD

int list_DVD_files(cchar *menu)
{
   int      vii;

   wprintf(mLog,"\n List all files on DVD/BD: \n");

   vGetFiles();
   wprintf(mLog,"   %d files found \n",Vnf);

   for (vii = 0; vii < Vnf; vii++)
   {
      if (checkKillPause()) break;
      wprintf(mLog," %s \n",kleenex(Vrec[vii].file));
   }

   return 0;
}


//  find desired files on disk, on mounted DVD/BD, and in history files

int find_files(cchar *menu)
{
   int            dii, vii, hii, ftf, nn;
   cchar          *fspec1, *hfile1;
   static char    fspec2[200] = "/home/*/file*";
   char           hfile[200], buff[1000], *pp;
   FILE           *fid;
   pvlist         *flist = 0;

   dGetFiles();                                                            //  get disk and DVD/BD files
   if (dvdmtd) vGetFiles();
   else wprintf(mLog," DVD/BD not mounted \n");

   wprintf(mLog,"\n find files matching wildcard pattern \n");             //  get search pattern
   fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:",fspec2);
   if (blank_null(fspec1)) goto report_exit;
   strncpy0(fspec2,fspec1,199);
   strTrim(fspec2);
   wprintf(mLog," search pattern: %s \n",fspec2);

   wprintx(mLog,0,"\n matching files on disk: \n",boldfont);

   for (dii = 0; dii < Dnf; dii++)                                         //  search disk files
   {
      if (checkKillPause()) goto report_exit;
      if (MatchWild(fspec2,Drec[dii].file) == 0) 
            wprintf(mLog,"  %s \n",kleenex(Drec[dii].file));
   }

   wprintx(mLog,0,"\n matching files on DVD/BD: \n",boldfont);

   for (vii = 0; vii < Vnf; vii++)                                         //  search DVD/BD files
   {
      if (checkKillPause()) goto report_exit;
      if (MatchWild(fspec2,Vrec[vii].file) == 0) 
            wprintf(mLog,"  %s \n",kleenex(Vrec[vii].file));
   }
   
   wprintx(mLog,0,"\n matching files in backup history: \n",boldfont);
   
   flist = pvlist_create(maxhist);
   snprintf(hfile,199,"%s/dkopp-hist-*",userdir);                          //  find all backup history files
   ftf = 1;                                                                //    /home/user/.dkopp/dkopp-hist-*
   nn = 0;

   while (true)
   {
      hfile1 = SearchWild(hfile,ftf);
      if (! hfile1) break;
      if (nn == maxhist) break;
      pvlist_append(flist,hfile1);                                         //  add to list
      nn++;
   }

   if (nn == 0) wprintf(mLog," no history files found \n");
   if (nn == maxhist) wprintf(mLog," *** too many history files, please purge");
   if (nn == 0 || nn == maxhist) goto report_exit;

   pvlist_sort(flist);                                                     //  sort list ascending
   
   for (hii = 0; hii < nn; hii++)                                          //  loop all history files
   {
      hfile1 = pvlist_get(flist,hii);
      wprintf(mLog,"  %s \n",hfile1);

      fid = fopen(hfile1,"r");                                             //  next history file
      if (! fid) {
         wprintf(mLog,"   *** file open error \n");
         continue;
      }

      while (true)                                                         //  read and search for match
      {
         if (checkKillPause()) break;
            pp = fgets_trim(buff,999,fid,1);
            if (! pp) break;
            if (MatchWild(fspec2,buff) == 0) 
                  wprintf(mLog,"    %s \n",buff);
      }
      
      fclose(fid);
   }

report_exit:
   if (flist) pvlist_free(flist);
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  list available backup history files, select one to view

int view_backup_hist(cchar *menu)
{
   cchar          *fspec1;
   char           fspec2[200], histfile[200];
   char           *pp;
   int            ii, jj, nn;
   int            zstat, ftf;
   zdialog        *zd;
   pvlist         *flist = 0;

   wprintf(mLog," available history files in %s \n",userdir);

   snprintf(fspec2,199,"%s/dkopp-hist-*",userdir);
   flist = pvlist_create(maxhist);
   ftf = 1;
   nn = 0;

   while (true)
   {
      fspec1 = SearchWild(fspec2,ftf);                                     //  file: dkopp-hist-yyyymmdd-hhmm-label
      if (! fspec1) break;
      pp = (char *) strrchr(fspec1,'/') + 12;                              //  get yyyymmdd-hhmm-label  v.4.7.1 gcc
      if (nn == maxhist) break;
      pvlist_append(flist,pp);                                             //  add to list
      nn++;
   }

   if (nn == 0) wprintf(mLog," no history files found \n");
   if (nn == maxhist) wprintf(mLog," *** too many history files, please purge");
   if (nn == 0 || nn == maxhist) goto report_exit;
   
   pvlist_sort(flist);                                                     //  sort list ascending
   
   for (ii = 0; ii < nn; ii++)                                             //  report sorted list
      wprintf(mLog," dkopp-hist-%s \n",pvlist_get(flist,ii));

   zd = zdialog_new("choose history file",mWin,"OK","cancel",null);
   zdialog_add_widget(zd,"label","lab1","dialog","history file date and label");
   zdialog_add_widget(zd,"comboE","hfile","dialog");

   jj = nn - 20;
   if (jj < 0) jj = 0;
   for (ii = jj; ii < nn; ii++)                                            //  stuff combo box list with
      zdialog_cb_app(zd,"hfile",pvlist_get(flist,ii));                     //    20 newest hist file IDs
   zdialog_stuff(zd,"hfile",pvlist_get(flist,nn-1));                       //  default entry is newest file
   
   zdialog_run(zd);                                                        //  run dialog
   zstat = zdialog_wait(zd);

   zdialog_fetch(zd,"hfile",histfile,199);                                 //  get user choice
   zdialog_free(zd);
   if (zstat != 1) goto report_exit;                                       //  cancelled

   shell_ack("xdg-open %s/%s-%s",userdir,"dkopp-hist",histfile);           //  view the file

report_exit:   
   if (flist) pvlist_free(flist);
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  file restore dialog - specify DVD/BD files to be restored

int RJedit(cchar * menu)
{
   zdialog        *zd;
   
   wprintf(mLog,"\n Restore files from DVD/BD \n");   

   vGetFiles();                                                            //  get files on DVD/BD
   wprintf(mLog,"   %d files on DVD/BD \n",Vnf);
   if (! Vnf) return 0;

   ++Fdialog;
   
   zd = zdialog_new("copy files from DVD/BD",mWin,"browse","done","cancel",null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"label","labdev","vb1","DVD/BD device");          //   DVD/BD device      [___________][v]
   zdialog_add_widget(zd,"comboE","entdvd","vb2",BJdvd);

   zdialog_add_widget(zd,"label","labfrom","vb1","copy-from DVD/BD");      //  copy-from DVD/BD    [______________]
   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","labfiles","dialog","files to restore");
   zdialog_add_widget(zd,"frame","framefiles","dialog",0,"expand");
   zdialog_add_widget(zd,"scrwin","scrfiles","framefiles");
   zdialog_add_widget(zd,"edit","editfiles","scrfiles");

   for (int ii = 0; ii < ndvds; ii++)                                      //  load curr. data into widgets
      zdialog_cb_app(zd,"entdvd",dvddevdesc[ii]);
                                                                           //  remove get/stuff mount point  v.5.3
   editwidget = zdialog_widget(zd,"editfiles");
   for (int ii = 0; ii < RJnx; ii++)                                       //  get restore include/exclude recs,
      wprintf(editwidget,"%s""\n",RJinex[ii]);                             //   pack into file selection edit box

   zdialog_resize(zd,400,400);
   zdialog_run(zd,RJedit_event);                                           //  run dialog with response function
   return 0;
}


//  edit dialog event function

int  RJedit_event(zdialog *zd, cchar *event)
{
   char           text[40], *pp, fcfrom[maxfcc];
   int            zstat, ftf, cc;

   zstat = zd->zstat;
   if (! zstat) return 0;
   if (zstat != 1 && zstat != 2) goto end_dialog;                          //  cancel or destroy
   
   RJreset();                                                              //  reset restore job data

   zdialog_fetch(zd,"entdvd",text,19);                                     //  get DVD/BD device
   strncpy0(BJdvd,text,19);
   pp = strchr(BJdvd,' ');
   if (pp) *pp = 0;
                                                                           //  remove fetch/save mount point  v.5.3
   zdialog_fetch(zd,"entfrom",RJfrom,maxfcc);                              //  copy-from location /home/xxx/.../
   strTrim(RJfrom);

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

   ftf = 1;
   while (true)                                                            //  include/exclude recs from edit box
   {
      pp = wscanf(editwidget,ftf);
      if (! pp) break;
      cc = strTrim(pp);                                                    //  remove trailing blanks
      if (cc < 3) continue;                                                //  ignore absurdities
      if (cc > maxfcc-100) continue;
      RJinex[RJnx] = strdupz(pp);                                          //  copy new record
      if (++RJnx == maxnx) {
         wprintf(mLog," *** exceed %d include/exclude recs \n",maxnx); 
         break;
      }
   }

   if (zstat == 1) {                                                       //  do file-chooser dialog
      strcpy(fcfrom,dvdmp);                                                //  start at /media/xxxx/home/xxxx/
      strcat(fcfrom,RJfrom);
      fc_dialog(fcfrom);
      zd->zstat = 0;                                                       //  dialog continues
      return 0;
   }

   RJvalidate();                                                           //  validate restore job data
   if (RJval) rGetFiles();                                                 //  get files to restore
   else wprintf(mLog," *** correct errors in restore job \n");

end_dialog:
   zdialog_free(zd);                                                       //  destroy dialog
   --Fdialog;
   return 0;
}


//  List and validate DVD/BD files to be restored

int RJlist(cchar * menu)
{
   int       cc1, cc2;
   char     *file1, file2[maxfcc];
   
   if (! RJval) {
      wprintf(mLog," *** restore job has errors \n");
      goto list_exit;
   }

   wprintf(mLog,"\n copy %d files from DVD/BD: %s \n",Rnf, RJfrom);
   wprintf(mLog,"    to directory: %s \n",RJto);
   wprintf(mLog,"\n resulting files will be the following: \n");
   if (! Rnf) goto list_exit;
   
   cc1 = strlen(RJfrom);                                                   //  from: /home/xxx/.../
   cc2 = strlen(RJto);                                                     //    to: /home/yyy/.../

   for (int ii = 0; ii < Rnf; ii++)
   {
      if (checkKillPause()) goto list_exit;

      file1 = Rrec[ii].file;

      if (! strnEqu(file1,RJfrom,cc1)) {
         wprintf(mLog," *** not within copy-from: %s \n",kleenex(file1));
         RJval = 0;
         continue;
      }
      
      strcpy(file2,RJto);
      strcpy(file2+cc2,file1+cc1);
      wprintf(mLog," %s \n",kleenex(file2));
   }

list_exit:      
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  Restore files based on data from restore dialog

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

   if (! RJval || ! 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,dvdmp,RJfrom,RJto);
   if (! nn) goto restore_exit;

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

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

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

   restore_filepoop();                                                     //  restore owner/permissions

   dFilesReset();                                                          //  reset disk file data

restore_exit:   
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  get available DVD/BD devices and mount points

int getDVDs()
{
   int         ii, dvdrw, contx, err;
   const char  *buff, *pp;
   char        command[20];

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

   while ((buff = command_output(contx,command)))
   {
      if (strnEqu(buff,"P:",2)) {                                          //  start next device
         dvdrw = 0;                                                        //  is DVDRW not known
         if (ndvds == maxdvds) continue;
         pp = strstr(buff,"/block/");
         if (! pp) continue;                                               //  not a disk
         strcpy(dvddevs[ndvds],"/dev/");
         strncat(dvddevs[ndvds],pp+7,14);                                  //  prepare /dev/devID
         strcpy(dvddesc[ndvds],"?");                                       //  description not known
      }
         
      if (strnEqu(buff,"S:",2)) {
         pp = strstr(buff,"dvdrw");
         if (! pp) continue;
         if (ndvds == maxdvds) continue;
         dvdrw = 1;                                                        //  device is a DVDRW
         ndvds++;                                                          //  now we can count it
      }
         
      if (strnEqu(buff,"E:",2)) {
         if (! dvdrw) continue;
         pp = strstr(buff,"ID_MODEL=");
         if (! pp) continue;
         strncpy0(dvddesc[ndvds-1],pp+9,40);                               //  description
      }
   }
   
   for (ii = 0; ii < ndvds; ii++)                                          //  combine devices and descriptions
   {                                                                       //    for use in GUI chooser list
      strcpy(dvddevdesc[ii],dvddevs[ii]);
      strcat(dvddevdesc[ii],"  ");
      strcat(dvddevdesc[ii],dvddesc[ii]);
   }
   
   wprintf(mLog," DVD/BD devices found: %d \n",ndvds);                     //  output list of DVDs    v.5.1
   for (ii = 0; ii < ndvds; ii++)
      wprintf(mLog," %s %s \n",dvddevs[ii],dvddesc[ii]);

   return ndvds;
}


//  set DVD/BD device and mount point

int setDVDdevice(cchar *menu)
{
   cchar          *pp1;
   char           *pp2, text[60];
   int            ii, Nth, zstat;
   zdialog        *zd;

   if (*scriptParam) {                                                     //  script
      Nth = 1;                                                             //  parse: /dev/dvd /media/xxxx
      pp1 = strField(scriptParam,' ',Nth++);
      if (pp1) strncpy0(BJdvd,pp1,19);
      pp1 = strField(scriptParam,' ',Nth++);
      if (pp1) {
         strncpy0(dvdmp,pp1,99);                                           //  increase to 99         v.5.6
         dvdmpcc = strlen(dvdmp);                                          //  bugfix   v.5.5
         if (dvdmp[dvdmpcc-1] == '/')
            dvdmp[dvdmpcc--] = 0;                                          //  remove trailing /
      }
      *scriptParam = 0;
      return 0;
   }
   
   zd = zdialog_new("select DVD/BD drive",mWin,"OK","cancel",null);        //  dialog to select DVD/BD and mount point
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5");
   zdialog_add_widget(zd,"label","labdvd","vb1","DVD/BD device");
   zdialog_add_widget(zd,"label","labmp","vb1","mount point");
   zdialog_add_widget(zd,"comboE","entdvd","vb2",BJdvd);
   zdialog_add_widget(zd,"entry","entmp","vb2",dvdmp);

   for (ii = 0; ii < ndvds; ii++)                                          //  stuff avail. DVDs, mount points
      zdialog_cb_app(zd,"entdvd",dvddevdesc[ii]);
      zdialog_stuff(zd,"entmp",dvdmp);
   
   zdialog_run(zd);
   zstat = zdialog_wait(zd);

   if (zstat != 1) {
      zdialog_free(zd);
      return 0;
   }
   
   zstat = zdialog_fetch(zd,"entdvd",text,60);                             //  get selected DVD/BD
   strncpy0(BJdvd,text,19);
   pp2 = strchr(BJdvd,' ');
   if (pp2) *pp2 = 0;

   zdialog_fetch(zd,"entmp",text,39);                                      //  DVD/BD mount point
   strncpy0(dvdmp,text,99);                                                //  v.5.6
   strTrim(dvdmp);
   dvdmpcc = strlen(dvdmp);
   if (dvdmpcc && (dvdmp[dvdmpcc-1] == '/'))                               //  remove trailing /
      dvdmp[dvdmpcc--] = 0;

   wprintf(mLog," DVD/BD and mount point: %s %s \n",BJdvd,dvdmp);
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   zdialog_free(zd);
   return 0;
}


//  set label for subsequent DVD/BD backup via growisofs

int setDVDlabel(cchar *menu)
{
   cchar     *pp;

   if (*dvdlabel) wprintf(mLog," old DVD/BD label: %s \n",dvdlabel);
   else  strcpy(dvdlabel,"dkopp");
   pp = zdialog_text(mWin,"set new DVD/BD label",dvdlabel);
   if (blank_null(pp)) pp = "dkopp";
   strncpy0(dvdlabel,pp,31);
   wprintf(mLog," new DVD/BD label: %s \n",dvdlabel);
   return 1;
}


//  Mount DVD/BD with message feedback to window.                          //  overhauled v.6.1

int mountDVD(cchar *menu)                                                  //  menu mount function
{
   int            err, reset, contx;
   char           command[100], mbuff[100], *pp;
   const char     *pp1;
   FILE           *fid;
   struct stat    dstat;
   
   if (dvdmtd) {
      err = stat(dvdmp,&dstat);
      if ((! err) && (dvdtime == dstat.st_ctime)) return 0;                //  medium unchanged, do nothing
   }
   
   dvdtime = -1;
   dvdmtd = 0;                                                             //  set DVD/BD not mounted
   strcpy(mediumDT,"unknown");
   *mediumDT = 0;
   err = reset = 0;
   vFilesReset();                                                          //  reset DVD/BD files

   contx = 0;
   while ((pp = command_output(contx,"cat /proc/mounts")))                 //  get mounted disk info
   {
      pp1 = strField(pp,' ',1);                                            //  get /dev/xxx
      if (strNeq(pp1,BJdvd)) {
         zfree(pp);                                                        //  not my DVD/BD
         continue;
      }
      pp1 = strField(pp,' ',2);                                            //  get mount point
      zfree(pp);
      strncpy0(mbuff,pp1,99);                                              //  use existing mount point
      repl_1str(mbuff,dvdmp,"\\040"," ");                                  //  replace \040 with 1 blank    v.6.2
      strTrim(dvdmp);
      dvdmpcc = strlen(dvdmp);
      wprintf(mLog," %d %d already mounted \n",BJdvd,dvdmp);
      dvdmtd = 1;
   }

   if (dvdmtd) goto showpoop;

   err = stat(dvdmp,&dstat);                                               //  check mount point exists
   if (err) {
      sprintf(mbuff,"mkdir -m 0755 %s 2>&1",dvdmp);                        //  no, create it
      err = do_shell("mkdir",mbuff);
      if (err) goto done;
   }

   sprintf(mbuff,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp);               //  mount the DVD/BD
   err = do_shell("mount",mbuff);
   if (! err) {
      dvdmtd = 1;
      goto showpoop;
   }
   
   if (checkKillPause()) goto done;                                        //  killed by user
   if (err != 32) goto done;                                               //  broken pipe
   wprintf(mLog," waiting for mount conflict (?) \n");

   for (int tries = 0; tries < 30; tries++)
   {
      if (dvdmtd) break;
      sleep(1);
      contx = 0;
      while ((pp = command_output(contx,"cat /proc/mounts")))              //  get mounted disk info
      {
         pp1 = strField(pp,' ',1);                                         //  get /dev/xxx
         if (strNeq(pp1,BJdvd)) {
            zfree(pp);                                                     //  not my DVD/BD
            continue;
         }
         pp1 = strField(pp,' ',2);                                         //  get mount point
         zfree(pp);
         strncpy0(mbuff,pp1,99);                                           //  use existing mount point
         repl_1str(mbuff,dvdmp,"\\040"," ");                               //  replace \040 with 1 blank    v.6.2
         strTrim(dvdmp);
         dvdmpcc = strlen(dvdmp);
         wprintf(mLog," %s %s mounted by foreign agent \n",BJdvd,dvdmp);
         dvdmtd = 1;
      }
   }
   
   if (dvdmtd) goto showpoop;

   else {
      wprintx(mLog,0," giving up \n",boldfont);
      goto done;
   }

showpoop:

   dvdtime = dstat.st_ctime;                                               //  set DVD/BD ID = mod time

   snprintf(command,99,"volname %s",BJdvd);                                //  get DVD/BD label
   fid = popen(command,"r");
   if (fid) {
      pp = fgets_trim(mbuff,99,fid,1);
      if (pp) strncpy0(dvdlabel,pp,31);
      pclose(fid);
   }

   strcpy(mbuff,dvdmp);
   strcat(mbuff,V_DATETIME);                                               //  get last usage date/time if poss.
   fid = fopen(mbuff,"r");
   if (fid) {
      pp = fgets_trim(mbuff,99,fid,1);
      if (pp) strncpy0(mediumDT,pp,15);
      fclose(fid);
   }

   wprintf(mLog," DVD/BD label: %s  last dkopp: %s \n",dvdlabel,mediumDT);

done:

   if (dvdmtd) {
      commFail = 0;
      return 0;
   }

   commFail++;
   return err;
}


//  unmount DVD/BD

int unmountDVD(cchar *menu)
{
   char     command[60];
   
   vFilesReset();
   dvdmtd = 0;
   dvdtime = -1;

   sprintf(command,"umount %s 2>&1",dvdmp);                                //  use mount point     v.4.8
   do_shell("umount",command);
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0
   commFail = 0;                                                           //  ignore unmount error
   return 0;
}


//  eject DVD/BD with message feedback to window
//  not all computers support programmatic eject

int ejectDVD(cchar *menu)
{
   char     command[60];
   
   vFilesReset();
   dvdmtd = 0;
   dvdtime = -1;

   sprintf(command,"eject %s 2>&1",BJdvd);
   do_shell("eject",command);
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   commFail = 0;                                                           //  ignore eject error
   return 0;
}


//  wait for DVD/BD and reset hardware (get over lockups after growisofs)

int resetDVD(cchar * menu)
{
   if (*subprocName) {                                                     //  try to kill running job
      signalProc(subprocName,"resume");
      signalProc(subprocName,"kill");
      sleep(1);
   }
   
   ejectDVD(0);                                                            //  the only way I know to reset
   sleep(1);                                                               //    a hung-up DVD/BD drive

   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  Erase DVD/BD medium by filling it with zeros

int eraseDVD(cchar * menu)
{
   char        command[200];
   int         nstat;
   
   nstat = zmessageYN(mWin,"Erase DVD/BD. This will take some time. \n Continue?");
   if (! nstat) goto erase_exit;

   vFilesReset();                                                          //  reset DVD/BD file data

   sprintf(command,"growisofs -Z %s=/dev/zero %s 2>&1",BJdvd,gforce);
   do_shell("growisofs", command);                                         //  do growisofs, echo outputs

erase_exit:
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  Format DVD/BD (2-4 minutes)

int formatDVD(cchar * menu)
{
   char        command[60];
   int         nstat;
   
   nstat = zmessageYN(mWin,"Format DVD/BD. This will take 2-4 minutes. \n Continue?");
   if (! nstat) goto format_exit;

   vFilesReset();                                                          //  reset DVD/BD file data

   sprintf(command,"dvd+rw-format -force %s 2>&1",BJdvd);
   do_shell("dvd+rw-format", command);

format_exit:
   if (Fgui) wprintf(mLog," ready \n");                                    //  v.5.0

   return 0;
}


//  save logging window as text file

int saveScreen(cchar * menu)
{
   if (*scriptParam) {
      wfiledump(mLog, scriptParam);
      *scriptParam = 0;
      return 0;
   }
   
   wfilesave(mLog);
   return 0;
}


//  Display help/about or help/contents

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

   if (strEqu(menu,"contents")) showz_userguide();                         //  help file in new process

   return 0;
}


//  construct file-chooser dialog box 
//  note: Fdialog unnecessary: this dialog called from other dialogs

int fc_dialog(cchar *dirk)
{
   GtkWidget   *vbox;

   fc_dialogbox = gtk_dialog_new_with_buttons("choose files", 
                  GTK_WINDOW(mWin), GTK_DIALOG_MODAL, "hidden",100, 
                  "include",101, "exclude",102, "done",103, null);

   gtk_window_set_default_size(GTK_WINDOW(fc_dialogbox),600,500);
   G_SIGNAL(fc_dialogbox,"response",fc_response,0);

   fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN);
   vbox = gtk_dialog_get_content_area(GTK_DIALOG(fc_dialogbox));
   gtk_box_pack_end(GTK_BOX(vbox),fc_widget,1,1,0);

   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),0);

   gtk_widget_show_all(fc_dialogbox);
   return 0;
}


//  file-chooser dialog handler (file selection, OK, Cancel, Kill)

int fc_response(GtkDialog *dwin, int arg, void *data)
{
   GSList         *flist = 0;
   char           *file1, *file2, *ppf;
   int             ii, err, hide;
   struct stat64   filestat;
   
   if (arg == 103 || arg == -4)                                            //  done, cancel
   {
      gtk_widget_destroy(GTK_WIDGET(dwin));
      return 0;
   }
   
   if (arg == 100)                                                         //  hidden
   {
      hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(fc_widget));
      hide = 1 - hide;
      gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),hide);
   }
   
   if (arg == 101 || arg == 102)                                           //  include, 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 = stat64(file2,&filestat);
         if (err) wprintf(mLog," *** error: %s  file: %s \n",strerror(errno),kleenex(file2));
         
         if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*");                //  if directory, append wildcard
         
         ppf = file2;
         if (strnEqu(ppf,dvdmp,dvdmpcc)) ppf += dvdmpcc;                   //  omit DVD/BD mount point

         if (arg == 101) wprintf(editwidget,"include %s""\n",ppf);
         if (arg == 102) wprintf(editwidget,"exclude %s""\n",ppf);

         zfree(file2);
      }
   }

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


//  backup helper function
//  set nominal backup date/time
//  write date/time and updated medium use count to temp file

int writeDT()
{
   time_t      dt1;
   struct tm   dt2;                                                        //  year/month/day/hour/min/sec
   FILE        *fid;

   dt1 = time(0);
   dt2 = *localtime(&dt1);

   snprintf(backupDT,15,"%4d%02d%02d-%02d%02d",dt2.tm_year+1900,           //  yyyymmdd-hhmm
            dt2.tm_mon+1, dt2.tm_mday, dt2.tm_hour, dt2.tm_min);
   
   strcpy(mediumDT,backupDT);

   fid = fopen(TFdatetime,"w");
   if (! fid) {
      wprintf(mLog," *** cannot open /tmp scratch file \n");
      commFail++;
      return 0;
   }

   fprintf(fid,"%s \n",mediumDT);                                          //  write date/time and medium count
   fclose(fid);
   return 0;
}


//  backup helper function
//  save all file and directory owner and permission data to temp file

int save_filepoop()                                                        //  all files, not just directories
{
   int            ii, cc, err;
   FILE           *fid;
   char           file[maxfcc], dirk[maxfcc], pdirk[maxfcc], *pp;
   struct stat64  dstat;

   fid = fopen(TFfilepoop,"w");
   if (! fid) {
      wprintf(mLog," *** cannot open /tmp scratch file \n");
      commFail++;
      return 0;
   }

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

      while (true)
      {
         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 = stat64(dirk,&dstat);                                        //  get owner and permissions
         if (err) {
            wprintf(mLog," *** error: %s  file: %s \n",strerror(errno),kleenex(dirk));
            break;
         }

         dstat.st_mode = dstat.st_mode & 0777;

         fprintf(fid,"%4d:%4d %3o %s\n",                                   //  output uid:gid permissions directory
              dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk);            //                  (octal)
         
         *pp = '/';                                                        //  restore '/'
      }
      
      strcpy(pdirk,dirk);                                                  //  prior = this directory
      
      strcpy(file,Drec[ii].file);                                          //  disk file, again

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

      dstat.st_mode = dstat.st_mode & 0777;

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

   fclose(fid);
   return 0;
}


//  restore helper function
//  restore original owner and permissions for restored files and directories

int restore_filepoop()
{
   FILE        *fid;
   int         cc1, cc2, ccf, nn, ii, err;
   int         uid, gid, perms;
   char        file1[maxfcc], file2[maxfcc];
   char        poopfile[100];
   
   wprintf(mLog,"\n restore directory owner and permissions \n");
   wprintf(mLog,"  for directories anchored at: %s \n",RJto);
   
   cc1 = strlen(RJfrom);                                                   //  from: /home/xxx/.../
   cc2 = strlen(RJto);                                                     //    to: /home/yyy/.../

   strcpy(poopfile,dvdmp);                                                 //  DVD/BD file with owner & permissions
   strcat(poopfile,V_FILEPOOP);

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

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

      ccf = strlen(file1);                                                 //  match directories too
      if (ccf < cc1) continue;

      while (ii < Rnf)
      {
         nn = strncmp(Rrec[ii].file,file1,ccf);                            //  file in restored file list?
         if (nn >= 0) break;                                               //  (logic depends on sorted lists)
         ii++;
      }

      if (ii == Rnf) break;
      if (nn > 0) continue;                                                //  no

      strcpy(file2,RJto);                                                  //  copy-to location
      strcpy(file2 + cc2, file1 + cc1);                                    //  + org. file, less copy-from part
      wprintf(mLog," owner: %4d:%4d  permissions: %3o  file: %s \n",
                                    uid, gid, perms, kleenex(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);
   return 0;
}


//  create backup history file after successful backup

int createBackupHist()
{
   int         ii, err;
   FILE        *fid;
   char        backupfile[200], buff[230];
   char        disp;

   snprintf(backupfile,199,"%s/dkopp-hist-%s-%s",                          //  create history file name:
                      userdir,backupDT,dvdlabel);                          //    dkopp-hist-yyyymmdd-hhmm-dvdlabel
   
   snprintf(buff,229,"\n""create history file: %s \n",backupfile);
   wprintx(mLog,0,buff,boldfont);

   fid = fopen(backupfile,"w");
   if (! fid) {
      wprintf(mLog," *** cannot open dkopp-hist file \n");
      return 0;
   }
   
   fprintf(fid,"%s (%s backup) \n\n",backupfile,mbmode);

   for (ii = 0; ii < BJnx; ii++)                                           //  output include/exclude recs
      fprintf(fid," %s \n",BJinex[ii]);
   fprintf(fid,"\n");
   
   if (strEqu(mbmode,"full"))
   {
      for (ii = 0; ii < Dnf; ii++)                                         //  output all files for backup
      {
         if (Drec[ii].dvd != dvdnum) continue;                             //  screen for DVD/BD number
         fprintf(fid,"%s\n",Drec[ii].file);                                        
      }
   }
   
   else {
      for (ii = 0; ii < Dnf; ii++) {                                       //  output new and modified files
         disp = Drec[ii].disp;
         if ((disp == 'n') || (disp == 'm')) 
               fprintf(fid,"%s\n",Drec[ii].file);
      }
   }

   err = fclose(fid);
   if (err) wprintf(mLog," *** dkopp-hist file error %s \n",strerror(errno));
   return 0;
}


//  parse an include/exclude filespec statement
//  return: 0=comment  1=OK  2=parse-error  3=fspec-error

int inexParse(char * rec, char *& rtype, char *& fspec)
{
   char    *pp1, *pp2;
   int      ii;

   rtype = fspec = 0;
   
   if (rec[0] == '#') return 0;                                            //  comment recs.
   if (strlen(rec) < 3) return 0;
   strTrim(rec);
   
   ii = 0;
   while ((rec[ii] == ' ') && (ii < 30)) ii++;                             //  find 1st non-blank
   if (rec[ii] == 0) return 0;
   if (ii == 30) return 0;                                                 //  blank record
   
   rtype = rec + ii;                                                       //  include/exclude

   while ((rec[ii] > ' ') && (ii < 30)) ii++;                              //  look for next blank or null
   if (ii == 30) return 2;
   
   if (rec[ii] == ' ') { rec[ii] = 0; ii++; }                              //  end of rtype
   if (strlen(rtype) > 7) return 2;

   while ((rec[ii] == ' ') && (ii < 30)) ii++;                             //  find next non-blank
   if (ii == 30) return 2;

   fspec = rec + ii;                                                       //  filespec (wildcards)
   if (strlen(fspec) < 4) return 3;
   if (strlen(fspec) > maxfcc-100) return 3;

   if (strEqu(rtype,"exclude")) return 1;                                  //  exclude, done
   if (strNeq(rtype,"include")) return 2;                                  //  must be include

   if (fspec[0] != '/') return 3;                                          //  must have at least /topdirk/
   pp1 = strchr(fspec+1,'/');
   if (!pp1) return 3;
   if (pp1-fspec < 2) return 3;
   pp2 = strchr(fspec+1,'*');                                              //  any wildcards must be later
   if (pp2 && (pp2 < pp1)) return 3;
   pp2 = strchr(fspec+1,'%');
   if (pp2 && (pp2 < pp1)) return 3;
   return 1;                                                               //  include + legit. fspec
}


//  list backup job data and validate as much as practical

int BJvalidate(cchar * menu)
{
   int            ii, err, nerr = 0;
   int            year, mon, day;
   struct tm      tm_date, *tm_date2;
   struct stat    dstat;
   
   wprintx(mLog,0,"\n""Validate backup job data \n",boldfont);

   BJval = 0;

   if (! BJnx) {
      wprintf(mLog," *** no job data present \n");
      commFail++;
      return 0;
   }

   wprintf(mLog," DVD/BD device: %s \n",BJdvd);
   wprintf(mLog," capacity GB: %.1f \n",BJcap);

   if (BJspeed == 0) wprintf(mLog," write speed: default \n",BJspeed);     //  v.4.5
   else  wprintf(mLog," write speed: %d (x 1.38 MB/sec) \n",BJspeed);
   
   err = stat(BJdvd,&dstat);
   if (err || ! S_ISBLK(dstat.st_mode)) {
      wprintf(mLog," *** DVD/BD device is apparently invalid \n");
      nerr++;
   }
   
   if (BJcap < dvdcapmin || BJcap > dvdcapmax) {
      wprintf(mLog," *** DVD/BD capacity is apparently invalid \n");
      nerr++;
   }

   wprintf(mLog," backup %s \n",BJbmode);
   if (! strcmpv(BJbmode,"full","incremental","accumulate",null)) {
      wprintf(mLog," *** backup mode not full/incremental/accumulate \n");
      nerr++;
   }
   
   wprintf(mLog," verify %s \n",BJvmode);
   if (! strcmpv(BJvmode,"full","incremental","thorough",null)) {
      wprintf(mLog," *** verify mode not full/incremental/thorough \n");
      nerr++;
   }
   
   wprintf(mLog," file date from: %s \n",BJdatefrom);                      //  file age limit      v.4.8
   err = 0;
   ii = sscanf(BJdatefrom,"%d.%d.%d",&year,&mon,&day);
   if (ii != 3) err = 1;
   tm_date.tm_year = year - 1900;
   tm_date.tm_mon = mon - 1;
   tm_date.tm_mday = day;
   tm_date.tm_hour = tm_date.tm_min = tm_date.tm_sec = 0;
   tm_date.tm_isdst = -1;
   BJtdate = mktime(&tm_date);
   tm_date2 = localtime(&BJtdate);
   if (tm_date2->tm_year - year + 1900 != 0) err = 3;
   if (tm_date2->tm_year + 1900 < 1970) err = 4;                           //  < 1970 disallowed    v.4.8
   if (tm_date2->tm_mon - mon + 1 != 0) err = 5;
   if (tm_date2->tm_mday - day != 0) err = 6;
   if (err) {
      wprintf(mLog," *** date must be > 1970.01.01 \n");
      nerr++;
      BJtdate = 0;
   }
   
   nerr += nxValidate(BJinex,BJnx);                                        //  validate include/exclude recs

   wprintf(mLog," *** %d errors \n",nerr);
   if (nerr) commFail++;
   else BJval = 1;
   return 0;
}


//  validate restore job data

int RJvalidate()
{
   int      cc, nerr = 0;
   char     rdirk[maxfcc];
   DIR      *pdirk;

   if (RJval) return 1;   

   wprintf(mLog,"\n Validate restore job data \n");

   if (! RJnx) {
      wprintf(mLog," *** no job data present \n");
      return 0;
   }

   wprintf(mLog," copy-from: %s \n",RJfrom);
   strcpy(rdirk,dvdmp);                                                    //  validate copy-from location
   strcat(rdirk,RJfrom);                                                   //  /media/dvd/home/...
   pdirk = opendir(rdirk);
   if (! pdirk) {
      wprintf(mLog," *** invalid copy-from location \n");
      nerr++;
   }
   else closedir(pdirk);

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

   wprintf(mLog,"   copy-to: %s \n",RJto);
   pdirk = opendir(RJto);                                                  //  validate copy-to location
   if (! pdirk) {
      wprintf(mLog," *** invalid copy-to location \n");
      nerr++;
   }
   else closedir(pdirk);

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

   nerr += nxValidate(RJinex,RJnx);                                        //  validate include/exclude recs

   wprintf(mLog," %d errors \n",nerr);
   if (! nerr) RJval = 1;
   else RJval = 0;
   return RJval;
}


//  list and validate a set of include/exclude recs

int nxValidate(char **inexrecs, int nrecs)
{
   char    *rtype, *fspec, nxrec[maxfcc];
   int      ii, nstat, errs = 0;

   for (ii = 0; ii < nrecs; ii++)                                          //  process include/exclude recs
   {
      strcpy(nxrec,inexrecs[ii]);
      wprintf(mLog," %s \n",nxrec);                                        //  output
      
      nstat = inexParse(nxrec,rtype,fspec);                                //  parse
      if (nstat == 0) continue;                                            //  comment
      if (nstat == 1) continue;                                            //  OK

      if (nstat == 2) {
         wprintf(mLog," *** cannot parse \n");                             //  cannot parse
         errs++;
         continue;
      }

      if (nstat == 3) {                                                    //  bad filespec
         wprintf(mLog," *** invalid filespec \n");
         errs++;
         continue;
      }
   }
   
   return errs;
}


//  get all files for backup as specified by include/exclude records
//  save in Drec[] array

int dGetFiles()
{
   cchar          *fsp;
   char           *rtype, *fspec, bjrec[maxfcc], *mbytes;
   int            ftf, cc, nstat, wstat, err;
   int            ii, jj, nfiles, ndvd, toobig, nexc;
   double         nbytes, dvdbytes;
   struct stat64  filestat;

   if (! BJval) {                                                          //  validate job data if needed
      dFilesReset(); 
      BJvalidate(0); 
      if (! BJval) return 0;                                               //  job has errors
   }

   if (Dnf > 0) return 0;                                                  //  avoid refresh

   wprintx(mLog,0,"\n""finding all files for backup \n",boldfont);
   
   for (ii = 0; ii < BJnx; ii++)                                           //  process include/exclude recs
   {
      BJfiles[ii] = 0;                                                     //  initz. include/exclude rec stats
      BJbytes[ii] = 0.0;
      BJdvdno[ii] = 0;
      
      strcpy(bjrec,BJinex[ii]);                                            //  next record
      nstat = inexParse(bjrec,rtype,fspec);                                //  parse
      if (nstat == 0) continue;                                            //  comment

      if (strEqu(rtype,"include"))                                         //  include filespec
      {
         ftf = 1;

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

            cc = strlen(fsp);
            if (cc > maxfcc-100) zappcrash("file cc: %d, %99s...",cc,fsp);

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

            err = lstat64(fsp,&filestat);                                  //  check accessibility          v.4.6
            if (err == 0) {
               if (! S_ISREG(filestat.st_mode) &&                          //  include files and symlinks   v.4.6
                   ! S_ISLNK(filestat.st_mode)) continue;                  //  omit pipes, devices ...
            }

            Drec[Dnf].stat = err;                                          //  save file status
            Drec[Dnf].inclx = ii;                                          //  save pointer to include rec
            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;
            Drec[Dnf].disp = Drec[Dnf].ivf = 0;                            //  initialize
            
            BJfiles[ii]++;                                                 //  count included files and bytes
            BJbytes[ii] += Drec[Dnf].size;

            if (++Dnf == maxfs) {
               wprintf(mLog," *** exceeded %d files \n",maxfs);
               goto errret;
            }
         }
      }
         
      if (strEqu(rtype,"exclude"))                                         //  exclude filespec
      {
         for (jj = 0; jj < Dnf; jj++)                                      //  check 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;
            zfree(Drec[jj].file);                                          //  clear file data in array
            Drec[jj].file = 0;
            Drec[jj].stat = 0;                                             //  bugfix
         }
      }
   }                                                                       //  end of include/exclude recs

   for (ii = 0; ii < Dnf; ii++)                                            //  list and remove error files
   {                                                                       //  (after excluded files removed)
      if (! Drec[ii].file) continue;                                       //  bugfix  v.4.6

      if (Drec[ii].stat)
      {
         err = stat64(Drec[ii].file,&filestat);
         wprintf(mLog," *** %s  omit: %s \n",strerror(errno),kleenex(Drec[ii].file));
         jj = Drec[ii].inclx;
         BJfiles[jj]--;                                                    //  un-count file and bytes
         BJbytes[jj] -= Drec[ii].size;
         zfree(Drec[ii].file);
         Drec[ii].file = 0;
      }
   }

   for (ii = 0; ii < Dnf; ii++)                                            //  list and remove too-big files
   {
      if (! Drec[ii].file) continue;                                       //  bugfix  v.4.6

      if (Drec[ii].size > BJcap * giga)
      {
         wprintf(mLog," *** omit file too big: %s \n",kleenex(Drec[ii].file));
         jj = Drec[ii].inclx;
         BJfiles[jj]--;                                                    //  un-count file and bytes
         BJbytes[jj] -= Drec[ii].size;
         zfree(Drec[ii].file);
         Drec[ii].file = 0;
      }
   }
   
   for (nexc = ii = 0; ii < Dnf; ii++)
   {
      if (! Drec[ii].file) continue;

      if (Drec[ii].mtime < BJtdate)                                        //  omit files excluded by date
      {                                                                    //    or older than 1970      v.4.8
         jj = Drec[ii].inclx;
         BJfiles[jj]--;                                                    //  un-count file and bytes
         BJbytes[jj] -= Drec[ii].size;
         zfree(Drec[ii].file);
         Drec[ii].file = 0;
         nexc++;
      }
   }

   if (nexc) wprintf(mLog," %d files excluded by selection date \n",nexc);

   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) zfree(Drec[jj].file);
            Drec[jj] = Drec[ii];
            Drec[ii].file = 0;
         }
         ii++;
         jj++;
      }
   }

   Dnf = jj;                                                               //  final file count
   
   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 < BJnx; ii++)                                           //  compute total files and bytes
   {                                                                       //    from include/exclude recs
      nfiles += BJfiles[ii];
      nbytes += BJbytes[ii];
   }

   mbytes = formatKBMB(nbytes,4);                                          //  v.5.2
   wprintf(mLog," files for backup: %d  %s \n",nfiles,mbytes);
   
   if ((nfiles != Dnf) || (Dbytes != nbytes)) {                            //  must match
      wprintf(mLog," *** bug: nfiles: %d  Dnf: %d \n",nfiles,Dnf);
      wprintf(mLog," *** bug: nbytes: %.0f  Dbytes: %.0f \n",nbytes,Dbytes);
      goto errret;
   }
   
   //  assign DVD/BD sequence number to all files, under constraint that
   //  all files from same include record are on same DVD/BD, if possible
      
   ndvd = 1;                                                               //  1st DVD/BD sequence no.
   dvdbytes = 0.0;                                                         //  DVD/BD bytes so far
   toobig = 0;
   
   for (ii = jj = 0; ii < Dnf; ii++)                                       //  loop all files
   {
      if (Drec[ii].inclx != Drec[jj].inclx) jj = ii;                       //  start of include group

      if (dvdbytes + Drec[ii].size > BJcap * giga) {                       //  exceeded DVD/BD capacity in this group
         if (jj > 0 && Drec[jj].dvd == Drec[jj-1].dvd) ii = jj;            //  if same DVD/BD as prior, restart group
         else toobig++;
         ndvd++;                                                           //  next DVD/BD no.
         dvdbytes = 0.0;                                                   //  reset byte counter
      }

      Drec[ii].dvd = ndvd;                                                 //  set DVD/BD no. for file
      dvdbytes += Drec[ii].size;                                           //  accum. bytes for this DVD/BD no.
      
      if (ii == jj) BJdvdno[Drec[ii].inclx] = ndvd;                        //  set 1st DVD/BD no. for include rec
   }

   BJndvd = ndvd;                                                          //  final DVD/BD media count
      
   if (toobig) wprintf(mLog," *** warning: single include set exceeds DVD/BD capacity \n");

   SortFileList((char *) Drec,sizeof(dfrec),Dnf,'A');                      //  sort Drec[Dnf] by Drec[].file
   
   for (ii = 1; ii < Dnf; ii++)                                            //  look for duplicate files
      if (strEqu(Drec[ii].file,Drec[ii-1].file)) {
         wprintf(mLog," *** duplicate file: %s \n",kleenex(Drec[ii].file));
         BJval = 0;                                                        //  invalidate backup job
      }

   if (! BJval) goto errret;
   return 0;

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


//  get existing files on DVD/BD medium, save in Vrec[] array 
//  (the shell command "find ... -type f" does not find the
//   files "deleted" via copy from /dev/null in growisofs)

int vGetFiles()
{
   int            cc, gcc, err;
   char           command[100], *pp;
   char           fspec1[maxfcc], fspec2[maxfcc];
   FILE           *fid;
   struct stat64  filestat;

   if (Vnf) return 0;                                                      //  avoid refresh

   mountDVD(0);                                                            //  mount with retries
   if (! dvdmtd) return 0;                                                 //  cannot mount

   wprintx(mLog,0,"\n""find all DVD/BD files \n",boldfont);

   sprintf(command,"find \"%s\" -type f -or -type l >%s",                  //  get regular files and symlinks
                                          dvdmp,TFdvdfiles);               //  add quotes in case of blanks    v.6.2
   wprintf(mLog," %s \n",command);
   
   err = system(command);                                                  //  list all DVD/BD files to temp file
   if (err) {
      wprintf(mLog," *** find command failed: %s \n",wstrerror(err));
      commFail++;
      return 0;
   }

   fid = fopen(TFdvdfiles,"r");                                            //  read file list
   if (! fid) {
      wprintf(mLog," *** cannot open /tmp scratch file \n");
      commFail++;
      return 0;
   }

   gcc = strlen(V_DKOPPDIRK);
   
   while (1)
   {
      pp = fgets_trim(fspec1,maxfcc-2,fid);                                //  get next file
      if (! pp) break;                                                     //  eof
      
      cc = strlen(pp);                                                     //  absurdly long file name
      if (cc > maxfcc-100) {
         wprintf(mLog," *** absurd file skipped: %300s (etc.) \n",kleenex(pp));
         continue;
      }

      if (strnEqu(fspec1+dvdmpcc,V_DKOPPDIRK,gcc)) continue;               //  ignore special dkopp files
      repl_1str(fspec1,fspec2,"\\=","=");                                  //  replace "\=" with "=" in file name
      Vrec[Vnf].file = strdupz(fspec2 + dvdmpcc);                          //  save without DVD/BD mount point

      err = lstat64(fspec1,&filestat);                                     //  check accessibility    v.4.6
      Vrec[Vnf].stat = err;                                                //  save file status
      Vrec[Vnf].size = filestat.st_size;                                   //  save file size
      Vrec[Vnf].mtime = filestat.st_mtime                                  //  save last mod time
                      + filestat.st_mtim.tv_nsec * nano;
      if (err) Vrec[Vnf].size = Vrec[Vnf].mtime = 0;
      Vnf++;
      if (Vnf == maxfs) zappcrash("exceed %d files",maxfs);
   }

   fclose (fid);

   SortFileList((char *) Vrec,sizeof(vfrec),Vnf,'A');                      //  sort Vrec[Vnf] by Vrec[].file
   
   wprintf(mLog," DVD/BD files: %d \n",Vnf);
   return 0;
}


//  get all DVD/BD restore files specified by include/exclude records

int rGetFiles()
{
   char       *rtype, *fspec, fspecx[maxfcc], rjrec[maxfcc];
   int         ii, jj, cc, nstat, wstat, ninc, nexc;

   if (! RJval) return 0;

   rFilesReset();                                                          //  clear restore files
   vGetFiles();                                                            //  get DVD/BD files
   if (! Vnf) return 0;

   wprintf(mLog,"\n""find all DVD/BD files to restore \n");
   
   for (ii = 0; ii < RJnx; ii++)                                           //  process include/exclude recs
   {
      strcpy(rjrec,RJinex[ii]);                                            //  next record
      wprintf(mLog," %s \n",rjrec);                                        //  output
      
      nstat = inexParse(rjrec,rtype,fspec);                                //  parse
      if (nstat == 0) continue;                                            //  comment
      
      repl_1str(fspec,fspecx,"\\=","=");                                   //  replace "\=" with "=" in file name

      if (strEqu(rtype,"include"))                                         //  include filespec
      {
         ninc = 0;                                                         //  count of included files

         for (jj = 0; jj < Vnf; jj++)                                      //  screen all DVD/BD files
         {
            wstat = MatchWild(fspecx,Vrec[jj].file);
            if (wstat != 0) continue;
            Rrec[Rnf].file = strdupz(Vrec[jj].file);                       //  add matching files
            Rnf++; ninc++;
            if (Rnf == maxfs) zappcrash("exceed %d files",maxfs);
         }
            
         wprintf(mLog,"  %d files added \n",ninc);
      }

      if (strEqu(rtype,"exclude"))                                         //  exclude filespec
      {
         nexc = 0;

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

            wstat = MatchWild(fspecx,Rrec[jj].file);
            if (wstat != 0) continue;
            zfree(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) zfree(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: /home/.../

   for (ii = 0; ii < Rnf; ii++)                                            //  get selected DVD/BD files to restore
   {
      if (! strnEqu(Rrec[ii].file,RJfrom,cc)) {
         wprintf(mLog," *** not under copy-from; %s \n",Rrec[ii].file);
         RJval = 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 DVD/BD files, set dispositions in Drec[] and Vrec[] arrays
//       n  new         on disk, not on DVD/BD
//       d  deleted     on DVD/BD, not on disk
//       m  modified    on both, but not equal
//       u  unchanged   on both, and equal

int setFileDisps()
{
   int            dii, vii, comp;
   char           disp;
   double         diff;
   
   dii = vii = 0;
   nnew = nmod = nunc = ndel = 0;
   Mbytes = 0.0;                                                           //  total bytes, new and modified files
   
   while ((dii < Dnf) || (vii < Vnf))                                      //  scan disk and DVD/BD files in parallel
   {
      if ((dii < Dnf) && (vii == Vnf)) comp = -1;                          //  disk file after last DVD/BD file
      else if ((dii == Dnf) && (vii < Vnf)) comp = +1;                     //  DVD/BD file after last disk file
      else comp = strcmp(Drec[dii].file,Vrec[vii].file);                   //  compare disk and DVD/BD file names

      if (comp < 0)
      {                                                                    //  unmatched disk file: new on disk
         Drec[dii].disp = 'n';
         Mbytes += Drec[dii].size;                                         //  accumulate Mbytes
         nnew++;                                                           //  count new files
         dii++;
      }

      else if (comp > 0)
      {                                                                    //  unmatched DVD/BD file: deleted on disk
         Vrec[vii].disp = 'd';
         ndel++;                                                           //  count deleted files
         vii++;
      }

      else if (comp == 0)                                                  //  file present on disk and DVD/BD
      {
         disp = 'u';                                                       //  set initially unchanged
         if (Drec[dii].stat != Vrec[vii].stat) disp = 'm';                 //  fstat() statuses are different
         diff = fabs(Drec[dii].size - Vrec[vii].size);
         if (diff > 0) disp = 'm';                                         //  sizes are different
         diff = fabs(Drec[dii].mtime - Vrec[vii].mtime);
         if (diff > modtimetolr) disp = 'm';                               //  mod times are different
         Drec[dii].disp = Vrec[vii].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

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


//  Sort file list in memory (disk files, DVD/BD 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;                                           //  compare filespecs functions
   if (sort == 'A') HeapSort(recs,RL,NR,fcompA);                           //  normal ascii compare
   if (sort == 'D') HeapSort(recs,RL,NR,fcompD);                           //  compare directories first
   return 0;
}

int fcompA(cchar * rec1, cchar * rec2)                                     //  ascii comparison
{
   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 are
   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;
   char        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 < BJnx; ii++) zfree(BJinex[ii]);
   BJnx = 0;
   *BJbmode = *BJvmode = 0;
   BJval = BJmod = 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 < RJnx; ii++) zfree(RJinex[ii]);
   RJnx = 0;
   RJval = 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++) 
   {
      zfree(Drec[ii].file);
      Drec[ii].file = 0;
   }

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

int vFilesReset()
{                                                                          //  DVD/BD files data
   for (int ii = 0; ii < Vnf; ii++) 
   {
      zfree(Vrec[ii].file);
      Vrec[ii].file = 0;
   }

   Vnf = 0;
   Vbytes = Mbytes = 0.0;
   return 0;
}

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

   Rnf = 0;
   return 0;
}


//  helper function to copy a file from DVD/BD to disk

cchar * copyFile(cchar * vfile, char *dfile)
{
   char              vfile1[maxfcc], vfilex[maxfcc];
   int               fid1, fid2, err, rcc;
   char              *pp, buff[vrcc];
   cchar             *errmess;
   struct stat64     fstat;
   struct timeval    ftimes[2];

   strcpy(vfile1,dvdmp);                                                   //  prepend DVD/BD mount point
   strcat(vfile1,vfile);
   repl_1str(vfile1,vfilex,"=","\\=");                                     //  replace "=" with "\=" in DVD/BD file
   
   fid1 = open(vfilex,O_RDONLY+O_NOATIME+O_LARGEFILE);                     //  open input file
   if (fid1 == -1) return strerror(errno);

   fid2 = open(dfile,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700);           //  open output file
   if (fid2 == -1 && errno == ENOENT) {
      pp = dfile;
      while (true) {                                                       //  create one or more directories,
         pp = strchr(pp+1,'/');                                            //    one level at a time
         if (! pp) break;
         *pp = 0;
         err = mkdir(dfile,0700);
         if (! err) chmod(dfile,0700);
         *pp = '/';
         if (err) {
            if (errno == EEXIST) continue;
            errmess = strerror(errno);
            close(fid1);
            return errmess;
         }
      }
      fid2 = open(dfile,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700);        //  open output file again
   }

   if (fid2 == -1) {
      errmess = strerror(errno);
      close(fid1);
      return errmess;
   }
   
   while (true)
   {
      rcc = read(fid1,buff,vrcc);                                          //  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;
      }
   }

   close(fid1);
   close(fid2);

   stat64(vfilex,&fstat);                                                  //  get input file attributes

   ftimes[0].tv_sec = fstat.st_atime;                                      //  conv. access times to microsecs
   ftimes[0].tv_usec = fstat.st_atim.tv_nsec / 1000;
   ftimes[1].tv_sec = fstat.st_mtime;
   ftimes[1].tv_usec = fstat.st_mtim.tv_nsec / 1000;

   chmod(dfile,fstat.st_mode);                                             //  set output file attributes
   err = chown(dfile,fstat.st_uid,fstat.st_gid);                           //  (if supported by file system)
   if (err) printf("error: %s \n",wstrerror(err));   
   utimes(dfile,ftimes);

   return 0;
}


//  Verify helper function
//  Verify that file on BD/DVD medium is readable, return its length.
//  Optionally compare backup file to current file, byte for byte.
//  return:  0: OK  1: open error  2: read error  3: compare fail

cchar * checkFile(char * dfile, int compf, double &tcc)
{
   int            vfid = 0, dfid = 0;
   int            err, vcc, dcc, cmperr = 0;
   int            open_flags = O_RDONLY+O_NOATIME+O_LARGEFILE;             //  O_DIRECT not allowed for DVD/BD
   char           vfile[maxfcc], *vbuff = 0, *dbuff = 0;
   cchar          *errmess = 0;
   double         dtime, vtime;
   struct stat64  filestat;

   tcc = 0.0;

   strcpy(vfile,dvdmp);                                                    //  prepend mount point
   repl_1str(dfile,vfile+dvdmpcc,"=","\\=");                               //  replace "=" with "\=" in DVD/BD file

   err = lstat64(vfile,&filestat);                                         //  check symlinks but do not follow
   if (err) return strerror(errno);                                        //  v.4.6
   if (S_ISLNK(filestat.st_mode)) return 0;
   
   if (compf) goto comparefiles;

   vfid = open(vfile,open_flags);                                          //  open DVD/BD file
   if (vfid == -1) return strerror(errno);

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

   while (1)                                                               //  read DVD/BD file
   {
      vcc = read(vfid,vbuff,vrcc);
      if (vcc == 0) break;
      if (vcc == -1) { errmess = strerror(errno); break; }
      tcc += vcc;                                                          //  accumulate length      
      if (checkKillPause()) break;
      zmainloop(10);                                                       //  keep gtk alive         v.6.3
   }
   goto cleanup;

comparefiles:

   vfid = open(vfile,open_flags);                                          //  open DVD/BD file
   if (vfid == -1) return strerror(errno);

   dfid = open(dfile,open_flags);                                          //  open corresp. disk file
   if (dfid == -1) { errmess = strerror(errno); goto cleanup; }

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

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

      dcc = read(dfid,dbuff,vrcc);
      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;
      zmainloop(5);                                                        //  keep gtk alive         v.6.3
   }

   if (vcc != dcc) cmperr++;

   if (cmperr) {                                                           //  compare error
      stat64(dfile,&filestat);
      dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano;         //  file modified since snapshot?
      stat64(vfile,&filestat);
      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);                                                  //  close files
   if (dfid) close(dfid);
   if (vbuff) free(vbuff);                                                 //  free buffers
   if (dbuff) free(dbuff);
   return errmess;
}


//  track current /directory/.../filename.ext  on logging window 
//  display directory and file names in overlay mode (no scrolling)

int track_filespec(cchar * filespec)
{
   int         cc;
   char        pdirk[300], pfile[300], *pp;
   
   if (! Fgui) {                                                           //  v.5.5
      printf(" %s \n",filespec);
      return 0;
   }
   
   pp = (char *) strrchr(filespec+1,'/');                                  //  parse directory/filename  v.4.7.1 gcc
   if (pp) {
      cc = pp - filespec + 2;
      strncpy0(pdirk,filespec,cc);
      strncpy0(pfile,pp+1,299);
   }
   else {
      strcpy(pdirk," ");
      strncpy0(pfile,filespec,299);
   }

   wprintf(mLog,-3," %s \n",kleenex(pdirk));                               //  output /directory
   wprintf(mLog,-2," %s \n",kleenex(pfile));                               //          filename
  
   return 0;
}


//  log error message and scroll down to prevent it from being overlaid

int track_filespec_err(cchar * filespec, cchar * errmess)
{
   if (Fgui) {
      wprintf(mLog,-3," *** %s  %s \n",errmess,kleenex(filespec));
      wprintf(mLog," \n");
   }
   else printf(" %s  %s \n",errmess,filespec);                             //  v.5.5
   return 0;
}


//  remove special characters in exotic file names causing havoc in output formatting

cchar * kleenex(cchar *name)
{
   static char    name2[1000];

   strncpy0(name2,name,999);

   for (int ii = 0; name2[ii]; ii++)
      if (name2[ii] >= 8 && name2[ii] <= 13)                               //  screen out formatting chars.
         name2[ii] = '?';

   return name2;
}


//  do shell command (subprocess) and echo outputs to log window
//  returns command status: 0 = OK, +N = error
//  compensate for growisofs failure not always indicated as bad status
//  depends on growisofs output being in english

int do_shell(cchar * pname, cchar * command)
{
   int         scroll, pscroll;
   int         err, gerr = 0, contx = 0;
   char        buff[1000];
   const char  *crec, *errmess;

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

   strncpy0(subprocName,pname,20);                                         //  set subprocess name, active
   if (strEqu(pname,"growisofs")) track_growisofs_files(0);                //  initialize progress tracker

   scroll = pscroll = 1;
   
   while ((crec = command_output(contx,command)))
   {
      strncpy0(buff,crec,999);

      pscroll = scroll;
      scroll = 1;
      
      if (strEqu(pname,"growisofs")) {                                     //  growisofs output
         if (track_growisofs_files(buff)) scroll = 0;                      //  conv. % done into file position
         if (strstr(buff,"genisoimage:")) gerr = 999;                      //  trap errors not reported in
         if (strstr(buff,"mkisofs:")) gerr = 998;                          //    flakey growisofs status
         if (strstr(buff,"failed")) gerr = 997;
         if (strstr(buff,"media is not recognized")) gerr = 996;           //  v.5.9.2
      }

      if (strstr(buff,"formatting")) scroll = 0;                           //  dvd+rw-format output
      
      if (scroll) {                                                        //  output to next line
         wprintf(mLog," %s: %s \n",pname,kleenex(buff));
         zsleep(0.1);                                                      //  throttle output a little
      }
      else if (Fgui) {                                                     //  supress output in batch mode    v.5.0
         if (pscroll) wprintf(mLog,"\n");                                  //  transition from scroll to overlay
         wprintf(mLog,-2," %s: %s \n",pname,kleenex(buff));                //  output, overlay prior output
      }

      if (killFlag) {                                                      //  v.6.0
         sprintf(buff,"pkill %s",subprocName);
         wprintf(mLog,"*** %s \n",buff);
         err = system(buff);
      }
      
      while (pauseFlag) {
         zsleep(0.2);                                                      //  v.6.0
         zmainloop();
      }
   }

   errmess = 0;
   err = command_status(contx);
   if (err) errmess = strerror(err);
   if (strEqu(pname,"growisofs")) {                                        //  v.5.9.2
      err = gerr;
      if (err) errmess = "growisofs failure";
   }
   if (err) wprintf(mLog," %s status: %d %s \n", pname, err, errmess);
   else wprintf(mLog," %s status: OK \n",pname);
   
   *subprocName = 0;                                                       //  no longer active
   if (err) commFail++;
   return err;
}


//  Convert "% done" from growisofs into corresponding position in list of files being copied.
//  Incremental backups start with  % done = (initial DVD/BD space used) / (final DVD/BD space used).

int track_growisofs_files(char * buff)
{
   static double     bbytes, gpct0, gpct;
   static int        dii, dii2, err;
   static char       *dfile;
   
   if (! buff) {                                                           //  initialize
      dii = 0;
      dii2 = -1;
      bbytes = 0;
      dfile = (char *) "";
      return 0;
   }

   if (! strstr(buff,"% done")) return 0;                                  //  not a % done record

   err = convSD(buff,gpct,0.0,100.0);                                      //  get % done, 0-100
   if (err > 1) return 0;
   
   if (strEqu(mbmode,"full")) {                                            //  full backup, possibly > 1 DVD/BD
      while (dii < Dnf) {
         if (bbytes/Dbytes2 > gpct/100) break;                             //  exit if enough bytes
         if (Drec[dii].dvd == dvdnum) {
            bbytes += Drec[dii].size;                                      //  sum files matching DVD/BD number
            dii2 = dii;
         }
         dii++;
      }
   }

   else  {                                                                 //  incremental backup
      if (bbytes == 0) gpct0 = gpct;                                       //  establish base % done
      while (dii < Dnf) {
         if (bbytes/Mbytes > (gpct-gpct0)/(100-gpct0)) break;              //  exit if enough bytes
         if (Drec[dii].disp == 'n' || Drec[dii].disp == 'm') {
            bbytes += Drec[dii].size;                                      //  sum new and modified files
            dii2 = dii;
         }
         dii++;
      }
   }

   if (dii2 > -1) dfile = Drec[dii2].file;                                 //  file corresponding to byte count
   snprintf(buff,999,"%6.1f %c  %s",gpct,'%',dfile);                       //  nn.n %  /directory/.../filename
   return 1;
}


//  supply unused zdialog callback function

void KBstate(int key, int state) { return; }



