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

   dkopp   copy files to / restore files from BRD/DVD media

   Copyright 2007-2024 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@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. See https://www.gnu.org/licenses

   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.

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

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

//  parameters and limits

#define release "dkopp-8.2"                                                            //  release 8.2
#define debug 0
#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/BRD files
#define maxhist 200                                                                    //  max no. history files
#define giga  1000000000.0                                                             //  gigabyte (not 1024**3)
#define modtimetolr 1.0                                                                //  tolerance, equal mod times
#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/BRD

#define V_DKOPPDIRK  "/dkopp-data/"                                                    //  dkopp special files on DVD/BRD
#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

namespace zfuncs {                                                                     //  externals from zfuncs module
   extern GdkDisplay        *display;                                                  //  X11 workstation (KB, mouse, screen)
   extern GdkDeviceManager  *manager;                                                  //  knows screen / mouse associations
   extern GdkScreen         *screen;                                                   //  monitor (screen)
   extern GdkDevice         *mouse;                                                    //  pointer device
   extern GtkSettings       *settings;                                                 //  screen settings
   extern GtkWidget         *mainwin;                                                  //  main window for zfuncs parent      7.5
}

#define MWIN GTK_WINDOW(mWin)

//  file scope variables

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
ch       subprocName[20];                                                              //  name of created subprocess
ch       scriptParam[200];                                                             //  parameter from script file
ch       mbmode[20], mvmode[20];                                                       //  actual backup, verify modes
double   pctdone;                                                                      //  % done from growisofs
ch       scrFile[XFCC];                                                                //  command line script file
ch       backupDT[16];                                                                 //  nominal backup date: yyyymmdd-hhmm

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

//  available DVD/BRD devices

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

//  backup job data

ch       BJfile[XFCC];                                                                 //  backup job file
ch       BJdvd[20];                                                                    //  DVD/BRD device: /dev/hdb
ch       BJbmode[20];                                                                  //  backup: full/incremental/accumulate
ch       BJvmode[20];                                                                  //  verify: full/incremental/thorough
ch       BJdatefrom[12];                                                               //  mod date selection, yyyy.mm.dd
time_t   BJtdate;                                                                      //  binary mod date selection
int      BJval;                                                                        //  backup job data validated
int      BJmod;                                                                        //  backup job data modified
ch      *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/BRD seqnc. no. (1,2...)
int      BJnx;                                                                         //  actual record count < maxnx

//  DVD/BRD medium data

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

//  current files for backup

struct dfrec {                                                                         //  disk file record
   ch       *file;                                                                     //    directory/filename
   double   size;                                                                      //    byte count
   double   mtime;                                                                     //    mod time
   int      stat;                                                                      //    fstat() status
   int      inclx;                                                                     //    include rec for this file
   ch       disp;                                                                      //    status: new modified unchanged
   ch       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/BRD medium

//  DVD/BRD file data

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

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

//  disk:DVD/BRD 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

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

struct   rfrec {                                                                       //  restore file record
   ch       *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, ch *menu);                                            //  process toolbar button event
void menufunc(GtkWidget *item, ch *menu);                                              //  process menu select event
void script_func(void *);                                                              //  execute script file

int quit_dkopp(ch *);                                                                  //  exit application
int clearScreen(ch *);                                                                 //  clear logging window
int signalFunc(ch *);                                                                  //  kill/pause/resume curr. function
int checkKillPause();                                                                  //  test flags: killFlag and pauseFlag

int fileOpen(ch *);                                                                    //  file open dialog
int fileSave(ch *);                                                                    //  file save dialog
int BJload(ch *fspec);                                                                 //  backup job data <<< file
int BJstore(ch *fspec);                                                                //  backup job data >>> file
int BJvload(ch *);                                                                     //  load job file from DVD/BRD

int BJedit(ch *);                                                                      //  backup job edit dialog
int BJedit_event(zdialog *zd, ch *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(ch *);                                                                      //  backup menu function
int FullBackup(ch *vmode);                                                             //  full backup + verify
int IncrBackup(ch *bmode, ch *vmode);                                                  //  incremental / accumulate + verify
int Verify(ch *);                                                                      //  verify functions

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

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

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

int helpFunc(ch *);                                                                    //  help function

int fc_dialog(ch *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(ch   *rec, ch   *&rtype, ch   *&fspec);                                  //  parse include/exclude record
int BJvalidate(ch *);                                                                  //  validate backup job data
int RJvalidate();                                                                      //  validate restore job data
int nxValidate(ch   **recs, int nr);                                                   //  validate include/exclude recs

int dGetFiles();                                                                       //  generate file list from job
int vGetFiles();                                                                       //  find all DVD/BRD files
int rGetFiles();                                                                       //  generate restore job file list
int setFileDisps();                                                                    //  set file disps: new del mod unch
int SortFileList(ch   *recs, int RL, int NR, ch   sort);                               //  sort file list in memory
int filecomp(ch *file1, ch *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/BRD file data, free memory
int rFilesReset();                                                                     //  reset restore file data, free memory

ch *checkFile(ch *dfile, int compf, double &tcc);                                      //  validate file on BRD/DVD medium
ch *copyfile(ch *vfile, ch *dfile);                                                    //  copy file from DVD/BRD to disk

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

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


//  dkopp menu table

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

#define nmenu  43
struct menuent menus[nmenu] = {
//  top-menu    sub-menu                lock    menu-function
{  "button",   "edit job",                1,    BJedit            },
{  "button",   "clear",                   0,    clearScreen       },
{  "button",   "run job",                 1,    Backup            },
{  "button",   "run DVD/BRD",             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/BRD",            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/BRD",             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/BRD files",      1,    Report            },
{  "Report",   "find files",              1,    Report            },
{  "Report",   "view backup hist",        1,    Report            },
{  "Restore",  "setup DVD/BRD restore",   1,    RJedit            },
{  "Restore",  "list restore files",      1,    RJlist            },
{  "Restore",  "restore files",           1,    Restore           },
{  "DVD/BRD",   "set DVD/BRD device",     1,    setDVDdevice      },
{  "DVD/BRD",   "set DVD/BRD label",      1,    setDVDlabel       },
{  "DVD/BRD",   "erase DVD/BRD",          1,    eraseDVD          },
{  "DVD/BRD",   "format DVD/BRD",         1,    formatDVD         },
{  "DVD/BRD",   "mount DVD/BRD",          1,    mountDVD          },
{  "DVD/BRD",   "unmount DVD/BRD",        1,    unmountDVD        },
{  "DVD/BRD",   "eject DVD/BRD",          1,    ejectDVD          },
{  "DVD/BRD",   "reset DVD/BRD",          0,    resetDVD          },
{  "Help",     "about",                   0,    helpFunc          },
{  "Help",     "user guide",              0,    helpFunc          }  };


//  dkopp main program

int main(int argc, ch   *argv[])
{
   GtkWidget   *mbar;                                                                  //  menubar
   GtkWidget   *mFile, *mBackup, *mVerify, *mReport, *mRestore;
   GtkWidget   *mDVD, *mHelp;
   int         ii;
   
   zinitapp(release,argc,argv);                                                        //  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

   for (ii = 1; ii < argc; ii++)                                                       //  get command line options
   {
      if (strmatch(argv[ii],"-nogui")) Fgui = 0;                                       //  command line operation
      else if (strmatch(argv[ii],"-job") && argc > ii+1)                               //  -job jobfile  (load job)
            strcpy(BJfile,argv[++ii]);
      else if (strmatch(argv[ii],"-run") && argc > ii+1)                               //  -run jobfile  (load and run job)
          { strcpy(BJfile,argv[++ii]); clrun++; }
      else if (strmatch(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
   {
      mLog = mWin = 0;                                                                 //  output goes to STDOUT
      initfunc(0);                                                                     //  start job or script
      unmountDVD(0);                                                                   //  unmount DVD/BRD
      ejectDVD(0);                                                                     //  eject DVD/BRD (may not work)
      return 0;                                                                        //  exit
   }

   mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL);                                         //  create main window
   zfuncs::mainwin = mWin;

   gtk_window_set_title(GTK_WINDOW(mWin),release);
   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_text_view_set_left_margin(GTK_TEXT_VIEW(mLog),2);
   gtk_container_add(GTK_CONTAINER(mScroll),mLog);                                     //  add to scrolled window

   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/BRD",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/BRD",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/BRD 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/BRD restore",menufunc);
         add_submenu_item(mRestore,"list restore files",menufunc);
         add_submenu_item(mRestore,"restore files",menufunc);
      mDVD = add_menubar_item(mbar,"DVD/BRD",menufunc);
         add_submenu_item(mDVD,"set DVD/BRD device",menufunc);
         add_submenu_item(mDVD,"set DVD/BRD label",menufunc);
         add_submenu_item(mDVD,"mount DVD/BRD",menufunc);
         add_submenu_item(mDVD,"unmount DVD/BRD",menufunc);
         add_submenu_item(mDVD,"eject DVD/BRD",menufunc);
         add_submenu_item(mDVD,"reset DVD/BRD",menufunc);
         add_submenu_item(mDVD,"erase DVD/BRD",menufunc);
         add_submenu_item(mDVD,"format DVD/BRD",menufunc);
      mHelp = add_menubar_item(mbar,"Help",menufunc);
         add_submenu_item(mHelp,"about",menufunc);
         add_submenu_item(mHelp,"user guide",menufunc);

/***  buttons removed   8.2

   GtkWidget *tbar = create_toolbar(mVbox,32);                                         //  create toolbar and buttons
   add_toolbar_button(tbar,"edit job","edit backup job","editjob.png",buttonfunc);
   add_toolbar_button(tbar,"run job","run backup job","run.png",buttonfunc);
   add_toolbar_button(tbar,"run DVD/BRD","run job on DVD/BRD","run.png",buttonfunc);
   add_toolbar_button(tbar,"pause","pause running job","media-pause.png",buttonfunc);
   add_toolbar_button(tbar,"resume","resume running job","media-play.png",buttonfunc);
   add_toolbar_button(tbar,"kill job","kill running job","kill.png",buttonfunc);
   add_toolbar_button(tbar,"clear","clear screen","clear.png",buttonfunc);
   add_toolbar_button(tbar,"quit","quit dkopp","quit.png",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;
   ch       *home;
   time_t   datetime;

   strcpy(homedir,get_zhomedir());                                                     //  get temp file names
   snprintf(TFdiskfiles,200,"%s/diskfiles",homedir);
   snprintf(TFdvdfiles,200,"%s/dvdfiles",homedir);
   snprintf(TFfilepoop,200,"%s/filepoop",homedir);
   snprintf(TFjobfile,200,"%s/jobfile",homedir);
   snprintf(TFdatetime,200,"%s/datetime",homedir);
   snprintf(TFrestorefiles,200,"%s/restorefiles.sh",homedir);
   snprintf(TFrestoredirks,200,"%s/restoredirks.sh",homedir);

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

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

   strcpy(BJdvd,"/dev/sr0");                                                           //  default DVD/BRD device
   strcpy(dvdmp,"/media/dkopp");                                                       //  default mount point
   dvdmpcc = strlen(dvdmp);                                                            //  mount point cc
   strcpy(dvdlabel,"dkopp");                                                           //  default DVD/BRD label
   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
   BJtdate = 0;

   BJnx = 4;                                                                           //  backup job include/exclude recs
   for (ii = 0; ii < BJnx; ii++)
      BJinex[ii] = (ch   *) zmalloc(50,"BJinex");

   home = getenv("HOME");                                                              //  get "/home/username"
   if (! home) home = (ch   *) "/home/xxx";
   strcpy(BJinex[0],"# dkopp default backup job");                                     //  initz. default backup specs           8.1
   sprintf(BJinex[1],"include %s/*",home);                                             //  include /home/username/*
   sprintf(BJinex[2],"exclude *thumbnails*");                                          //  exclude thumbnails
   sprintf(BJinex[2],"exclude *.cache*");                                              //  exclude cache

   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/BRD medium last backup date-time
   dvdtime = -1;                                                                       //  DVD/BRD device mod time
   dvdmtd = 0;                                                                         //  DVD/BRD 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

   txwidget_append2(mLog,0,"\n Searching for DVD/BRD devices ... \n");
   g_timeout_add(1000,getDVDs,0);                                                      //  blocks GTK until done

   return 0;
}


//  process toolbar button events (simulate menu selection)

void buttonfunc(GtkWidget *item, ch *button)
{
   ch   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 *, ch *menu)
{
   static int     ii;
   static ch      menu1[20] = "", menu2[40] = "";
   int            kk;

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

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

   else {                                                                              //  no match to menus
      txwidget_append2(mLog,0," *** bad command: %s \n",menu);
      commFail++;
      return;
   }

   if (menuLock && menus[ii].lock) {                                                   //  no lock funcs can run parallel
      if (Fgui)
         zmessageACK(mWin,"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,
      txwidget_append2(mLog,1,"\n""command: %s > %s \n",menu1,menu2);                  //    echo command to window

   kk = ii;                                                                            //  move to non-static memory

   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;
   ch       buff[200], menu1[20], menu2[40];
   ch       *pp;
   ch       *bb;

   fid = fopen(scrFile,"r");                                                           //  open file
   if (! fid) {
      txwidget_append2(mLog,0," *** 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

      txwidget_append2(mLog,0,"\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 = substring(buff,'>',Nth++);
      if (pp) strncpy0(menu1,pp,20);
      pp = substring(buff,'>',Nth++);
      if (pp) strncpy0(menu2,pp,40);
      pp = substring(buff,'>',Nth++);
      if (pp) strncpy0(scriptParam,pp,200);

      strTrim(menu1);                                                                  //  get rid of trailing blanks
      strTrim(menu2);

      if (strmatch(menu1,"exit")) break;

      menufunc(null,menu1);                                                            //  simulate menu entries
      menufunc(null,menu2);

      while (Fdialog) sleep(1);                                                        //  if dialog, wait for compl.
   }

   txwidget_append2(mLog,0,"script exiting \n");
   fclose(fid);
   *scrFile = 0;
   return;
}


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

int quit_dkopp(ch *menu)
{
   int      yn;

   if (! Fgui) return 0;

   signalFunc("kill job");

   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/BRD
      ejectDVD(0);                                                                     //  eject DVD/BRD (may not work)
   }

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


//  clear logging window

int clearScreen(ch *menu)
{
   txwidget_clear(mLog);
   return 0;
}


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

int signalFunc(ch *menu)
{
   if (strmatch(menu,"kill job"))
   {
      if (! menuLock) {
         txwidget_append2(mLog,0,"\n""ready \n");                                      //  already dead
         return 0;
      }

      if (killFlag) {                                                                  //  redundant kill
         if (*subprocName) {
            txwidget_append2(mLog,0," *** kill again: %s \n",subprocName);
            signalProc(subprocName,"kill");                                            //  kill subprocess
         }
         else txwidget_append2(mLog,0," *** waiting for function exit \n");            //  or wait for function to die
         return 0;
      }

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

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

      return 0;
   }

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

   if (strmatch(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(ch *menu)
{
   ch       *file;
   int      err = 0;

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

   ++Fdialog;

   file = zgetfile("open backup job",MWIN,"file",homedir,1);                           //  get file from user
   if (file) {
      if (strlen(file) > XFCC-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(ch *menu)
{
   ch       *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 = zgetfile("save backup job",MWIN,"save",BJfile,1);
   if (file) {
      if (strlen(file) > XFCC-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(ch *fspec)
{
   FILE     *fid;
   ch       buff[1000];
   ch       *fgs, *rtype, *rdata;
   ch       rtype2[20];
   int      cc, Nth, nerrs;

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

   txwidget_append2(mLog,1,"\n""loading job file: %s \n",fspec);

   fid = fopen(fspec,"r");                                                             //  open file
   if (! fid) {
      txwidget_append2(mLog,0," *** 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) {
         txwidget_append2(mLog,0," *** input record too big \n");
         nerrs++;
         continue;
      }

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

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

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

      if (strmatch(rtype2,"verify")) {
         rdata = substring(buff,' ',Nth++);                                            //  verify mode
         if (rdata) {
            strncpy0(BJvmode,rdata,19);
            strToLower(BJvmode);
         }
         continue;
      }

      if (strmatch(rtype2,"datefrom")) {
         rdata = substring(buff,' ',Nth++);                                            //  file mod date selection
         if (rdata) strncpy0(BJdatefrom,rdata,11);
         continue;
      }

      if (strmatchV(rtype2,"include","exclude","#",null)) {
         BJinex[BJnx] = zstrdup(buff,"BJinex");                                        //  include/exclude or comment rec.
         if (++BJnx >= maxnx) {
            txwidget_append2(mLog,0," *** exceed %d include/exclude recs \n",maxnx);
            nerrs++;
            break;
         }
         continue;
      }

      txwidget_append2(mLog,0," *** 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(ch *fspec)
{
   FILE     *fid;
   int      ii;

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

   fprintf(fid,"device %s \n",BJdvd);                                                  //  device /dev/dvd
   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

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

   fclose(fid);
   return 0;
}


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

int BJvload(ch *menu)
{
   ch       vjfile[100];

   BJreset();                                                                          //  reset job data

   mountDVD(0);                                                                        //  (re) mount DVD/BRD
   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(ch *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=5");
   zdialog_add_widget(zd,"label","labdev","hb1","DVD/BRD device","space=3");           //  DVD/BRD device  [______________][v]
   zdialog_add_widget(zd,"combo","entdvd","hb1",BJdvd);

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=8");
   zdialog_add_widget(zd,"button","bopen","hb2","open job file");                      //  [open job] [DVD/BRD job] [save as]
   zdialog_add_widget(zd,"button","bdvd","hb2","open DVD/BRD job");
   zdialog_add_widget(zd,"button","bsave","hb2"," save as ");

   zdialog_add_widget(zd,"hbox","hb3","dialog");
   zdialog_add_widget(zd,"vbox","vb3","hb3",0,"homog");
   zdialog_add_widget(zd,"label","space","hb3",0,"space=20"); 
   zdialog_add_widget(zd,"vbox","vb4","hb3",0,"homog");

   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,"zentry","entdate","vb4","yyyy.mm.dd","size=10");

   zdialog_add_widget(zd,"hsep","sep2","dialog",0,"space=8");                          //  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,"zedit","edinex","scrwinex");

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

   zdialog_resize(zd,0,600);
   zdialog_run(zd,BJedit_event,"parent");                                              //  run dialog
   return 0;
}


//  edit dialog event function

int BJedit_event(zdialog *zd, ch *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) {
         txwidget_clear(editwidget);                                                   //  clear include/exclude recs
         return 0;
      }

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

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

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

   if (strmatch(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++)                                                      //  DVD/BRD drives available
      zdialog_stuff(zd,"entdvd",dvddevdesc[ii]);
                                                                                       //  remove mount point get/stuff
   if (strmatch(BJbmode,"full")) zdialog_stuff(zd,"bmrb1",1);
   if (strmatch(BJbmode,"incremental")) zdialog_stuff(zd,"bmrb2",1);
   if (strmatch(BJbmode,"accumulate")) zdialog_stuff(zd,"bmrb3",1);
   if (strmatch(BJvmode,"full")) zdialog_stuff(zd,"vmrb1",1);
   if (strmatch(BJvmode,"incremental")) zdialog_stuff(zd,"vmrb2",1);
   if (strmatch(BJvmode,"thorough")) zdialog_stuff(zd,"vmrb3",1);
   zdialog_stuff(zd,"entdate",BJdatefrom);                                             //  file mod date selection

   editwidget = zdialog_gtkwidget(zd,"edinex");
   txwidget_clear(editwidget);
   for (int ii = 0; ii < BJnx; ii++)
      txwidget_append2(editwidget,0,"%s""\n",BJinex[ii]);

   return 0;
}


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

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

   BJreset();                                                                          //  reset job data

   zdialog_fetch(zd,"entdvd",text,19);                                                 //  get DVD/BRD device
   strncpy0(BJdvd,text,19);
   pp = strchr(BJdvd,' ');
   if (pp) *pp = 0;
                                                                                       //  remove mount point fetch/save
   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

   for (line = 0; ; line++)
   {
      pp = txwidget_line(editwidget,line,1);                                           //  include/exclude recs.
      if (! pp || ! *pp) break;
      strTrim(pp);                                                                     //  remove trailing blanks
      BJinex[BJnx] = zstrdup(pp,"BJinex");                                             //  copy new record
      if (++BJnx >= maxnx) {
         txwidget_append2(mLog,0," *** exceed %d include/exclude recs \n",maxnx);
         break;
      }
   }

   BJmod++;                                                                            //  job modified
   BJvalidate(0);                                                                      //  check for errors, set BJval
   return 0;
}


//  perform DVD/BRD backup using growisofs utility

int Backup(ch *menu)
{
   strcpy(mbmode,"");
   strcpy(mvmode,"");

   if (strmatchV(menu,"full","incremental","accumulate",null))                         //  backup only
      strcpy(mbmode,menu);

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

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

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

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

backup_done:
   if (Fgui) txwidget_append2(mLog,0,"ready \n");
   return 0;
}


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

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

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

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

   txwidget_append2(mLog,1,"\n""begin full backup \n");
   txwidget_append2(mLog,0," files: %d  bytes: %.0f \n",Dnf,                           //  files and bytes to copy
                                             formatKBMB(Dbytes,3));

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

   BJstore(TFjobfile);                                                                 //  copy job file (DVD/BRD) 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) {
      txwidget_append2(mLog,0," *** 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
   {
      dfile = Drec[ii].file;                                                           //  add to growisofs path-list
      repl_1str(dfile,vfile,XFCC,"=","\\\\=");                                         //  replace "=" with "\\=" in file name
      fprintf(fid,"%s=%s\n",vfile+1,dfile);                                            //  directories/file=/directories/file
      Dbytes2 += Drec[ii].size;
   }

   fclose(fid);

   txwidget_append2(mLog,0," writing DVD/BRD, %s \n",formatKBMB(Dbytes,3));
   start_timer(time0);                                                                 //  start timer for growisofs

   snprintf(command,200,                                                               //  build growisofs command line
      "growisofs -Z %s %s -r -graft-points "
      "-iso-level 4 -gui -V \"%s\" %s -path-list %s 2>&1",                             //  label in quotes
       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(mWin,"parent","growisofs error",                          //  manual compensation for growisofs
                        "abort","retry","ignore (continue)",null);                     //    and/or gnome bugs
      if (zstat == 1) goto backup_fail;
      if (zstat == 2) goto backup_retry;
   }

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

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

verify_retry:

   if (*BJvmode)                                                                       //  do verify if requested
   {
      mountDVD(0);                                                                     //  test if DVD hung
      if (! dvdmtd) {
         zstat = zdialog_choose(mWin,"parent","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(mWin,"parent","verify error",
                        "abort","retry","ignore (continue)",null);
         if (zstat == 1) goto backup_fail;
         if (zstat == 2) goto verify_retry;

         txwidget_append2(mLog,0," backup is being repeated \n");
         commFail = 0;
      }
   }

   createBackupHist();                                                                 //  create backup history file

   txwidget_append2(mLog,0," backup job complete \n");
   ejectDVD(0);
   return 0;

backup_fail:

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


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

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

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

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

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

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

   txwidget_append2(mLog,1,"\n""begin %s backup \n",BJbmode);
   txwidget_append2(mLog,0," files: %d  bytes: %s \n",Mfiles,                          //  files and bytes to copy
                                             formatKBMB(Mbytes,3));

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

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

   fid = fopen(TFdiskfiles,"w");                                                       //  temp file for growisofs path-list
   if (! fid) {
      txwidget_append2(mLog,0," *** 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,XFCC,"=","\\\\=");                                      //  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 (strmatch(BJbmode,"incremental")) {                                              //  incremental backup (not accumulate)
      for (ii = 0; ii < Vnf; ii++) {                                                   //  process deleted files still on DVD/BRD
         if (Vrec[ii].disp == 'd') {
            dfile = Vrec[ii].file;                                                     //  add to growisofs path-list
            repl_1str(dfile,vfile,XFCC,"=","\\\\=");                                   //  replace "=" with "\\=" in file name
            fprintf(fid,"%s=%s\n",vfile+1,"/dev/null");                                //  directories/file=/dev/null
         }
      }
   }

   fclose(fid);

   start_timer(time0);                                                                 //  start timer for growisofs

   snprintf(command,200,"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(mWin,"parent","growisofs error",                          //  manual compensation for growisofs
                        "abort","retry","ignore (continue)",null);                     //    and/or gnome bugs
      if (zstat == 1) goto backup_fail;
      if (zstat == 2) goto backup_retry;
   }

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

   vFilesReset();                                                                      //  reset DVD/BRD files

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

verify_retry:

   if (*BJvmode)                                                                       //  do verify if requested
   {
      mountDVD(0);                                                                     //  test if DVD/BRD hung
      if (! dvdmtd) {
         zstat = zdialog_choose(mWin,"parent","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/BRD
      if (commFail) {
         zstat = zdialog_choose(mWin,"parent","verify error",
                        "abort","retry","ignore (continue)",null);
         if (zstat == 1) goto backup_fail;
         if (zstat == 2) goto verify_retry;
      }
   }

   createBackupHist();                                                                 //  create backup history file

   ejectDVD(0);
   return 0;

backup_fail:
   commFail++;
   txwidget_append2(mLog,1," *** BACKUP FAILED \n");
   vFilesReset();
   ejectDVD(0);
   return 0;
}


//  verify DVD/BRD disc data integrity

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

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

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

   start_timer(time0);

   if (strmatch(menu,"full"))                                                          //  verify all files are readable
   {
      txwidget_append2(mLog,1,"\n""verify ALL files on DVD/BRD \n");
      if (Fgui) txwidget_append2(mLog,0,"\n\n");

      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) {
            txwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n");
            goto verify_exit;
         }
      }
   }

   if (strmatch(menu,"incremental"))                                                   //  verify files in prior incr. backup
   {
      txwidget_append2(mLog,1,"\n""verify files in prior incremental backup \n");

      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;
         txwidget_append2(mLog,0,"  %s \n",kleenex(filespec));                         //  output filespec
         errmess = checkFile(filespec,0,dcc1);                                         //  check file on DVD/BRD, get length
         if (errmess) txwidget_append2(mLog,0,"  *** %s \n",errmess);
         if (errmess) verrs++;
         vfiles++;
         vbytes += dcc1;

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

   if (strmatch(menu,"thorough"))                                                      //  compare DVD/BRD to disk files
   {
      txwidget_append2(mLog,1,"\n Read and verify ALL files on DVD/BRD. \n");
      txwidget_append2(mLog,0," Compare to disk files with matching names and mod times.\n");
      if (Fgui) txwidget_append2(mLog,0,"\n\n");

      for (ii = 0; ii < Vnf; ii++)                                                     //  process DVD/BRD 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 (stat(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/BRD 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/BRD file, 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) {
            txwidget_append2(mLog,1," *** OVER 100 ERRORS, GIVING UP *** \n");
            goto verify_exit;
         }
      }
   }

   txwidget_append2(mLog,0," DVD/BRD files: %d  bytes: %s \n",vfiles,formatKBMB(vbytes,3));
   txwidget_append2(mLog,0," DVD/BRD read errors: %d \n",verrs);

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

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

   if (verrs + cerrs) txwidget_append2(mLog,1," *** THERE WERE ERRORS *** \n");
   else txwidget_append2(mLog,0," NO ERRORS \n");

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


//  Reports menu function

int Report(ch *menu)
{
   if (strmatch(menu, "get files for backup")) get_current_files(0);
   if (strmatch(menu, "diffs summary")) report_summary_diffs(0);
   if (strmatch(menu, "diffs by directory")) report_directory_diffs(0);
   if (strmatch(menu, "diffs by file")) report_file_diffs(0);
   if (strmatch(menu, "list files for backup")) list_current_files(0);
   if (strmatch(menu, "list DVD/BRD files")) list_DVD_files(0);
   if (strmatch(menu, "find files")) find_files(0);
   if (strmatch(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(ch *menu)
{
   ch       *bytes;
   int      ii;

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

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

   txwidget_append2(mLog,1,"\n  files    bytes  include/exclude filespec \n");

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

   bytes = formatKBMB(Dbytes,3);
   txwidget_append2(mLog,0," %6d %9s   TOTAL \n", Dnf, bytes);

report_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


//  report disk:DVD/BRD differences summary

int report_summary_diffs(ch *menu)
{
   ch       *bytes;

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

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

   txwidget_append2(mLog,0,"\n disk files: %d  DVD/BRD files: %d \n",Dnf,Vnf);
   txwidget_append2(mLog,0,"\n Differences between DVD/BRD and files on disk: \n");
   txwidget_append2(mLog,0," %7d  disk files not on DVD/BRD - new \n",nnew);
   txwidget_append2(mLog,0," %7d  files on disk and DVD/BRD - unchanged \n",nunc);
   txwidget_append2(mLog,0," %7d  files on disk and DVD/BRD - modified \n",nmod);
   txwidget_append2(mLog,0," %7d  DVD/BRD files not on disk - deleted \n",ndel);

   bytes = formatKBMB(Mbytes,3);
   txwidget_append2(mLog,0," Total differences: %d files  %s \n",nnew+ndel+nmod,bytes);

report_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

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

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

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

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

   txwidget_append2(mLog,0,"\n Disk:DVD/BRD differences by directory \n");
   txwidget_append2(mLog,0,"   new   mod   del   bytes   directory \n");

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

   while ((dii < Dnf) || (vii < Vnf))                                                  //  scan disk and DVD/BRD files 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/BRD or disk
      else pdirk = Drec[dii].file;

      pp = (ch   *) strrchr(pdirk,'/');                                                //  isolate directory
      if (pp) *pp = 0;
      if (! strmatch(pdirk,ppdirk)) {                                                  //  if directory changed, output
         bytes = formatKBMB(nbytes,3);                                                 //    totals from prior directory
         if (kfiles > 0) txwidget_append2(mLog,0," %5d %5d %5d %8s  %s \n",
                                 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/BRD file: deleted
         kdel++;                                                                       //  count deleted file
         kfiles++;
         vii++;
      }

      else if (comp == 0) {                                                            //  file present on disk and DVD/BRD
         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
      txwidget_append2(mLog,0," %5d %5d %5d %8s  %s \n",knew,kmod,kdel,
                                                            bytes,ppdirk);
   }

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

report_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

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

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

   report_summary_diffs(0);                                                            //  report summary first

   txwidget_append2(mLog,0,"\n Detailed list of disk:DVD/BRD differences: \n");
   txwidget_append2(mLog,0,"\n %d new files (on disk, not on DVD/BRD) \n",nnew);

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

   txwidget_append2(mLog,0,"\n %d modified files (disk and DVD/BRD files are different) \n",nmod);

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

   txwidget_append2(mLog,0,"\n %d deleted files (on DVD/BRD, not on disk) \n",ndel);

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

report_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


//  list all files for backup

int list_current_files(ch *menu)
{
   int      dii;

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

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

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

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

report_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


//  list all files on mounted DVD/BRD

int list_DVD_files(ch *menu)
{
   int      vii;

   txwidget_append2(mLog,0,"\n List all files on DVD/BRD: \n");

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

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

   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

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

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

   txwidget_append2(mLog,0,"\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);
   txwidget_append2(mLog,0," search pattern: %s \n",fspec2);

   txwidget_append2(mLog,1,"\n matching files on disk: \n");

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

   txwidget_append2(mLog,1,"\n matching files on DVD/BRD: \n");

   for (vii = 0; vii < Vnf; vii++)                                                     //  search DVD/BRD files
   {
      if (checkKillPause()) goto report_exit;
      if (MatchWild(fspec2,Vrec[vii].file) == 0)
            txwidget_append2(mLog,0,"  %s \n",kleenex(Vrec[vii].file));
   }

   txwidget_append2(mLog,1,"\n matching files in backup history: \n");

   zlist = zlist_new(maxhist);
   snprintf(hfile,199,"%s/dkopp-hist-*",homedir);                                      //  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;
      zlist_append(zlist,hfile1,0);                                                    //  add to list
      nn++;
   }

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

   zlist_purge(zlist);                                                                 //  purge null entries                 7.4
   zlist_sort(zlist);                                                                  //  sort list ascending

   for (hii = 0; hii < nn; hii++)                                                      //  loop all history files
   {
      hfile1 = zlist_get(zlist,hii);
      txwidget_append2(mLog,0,"  %s \n",hfile1);

      fid = fopen(hfile1,"r");                                                         //  next history file
      if (! fid) {
         txwidget_append2(mLog,0,"   *** 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)
            txwidget_append2(mLog,0,"    %s \n",buff);
      }

      fclose(fid);
   }

report_exit:
   if (zlist) zlist_free(zlist);
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

int view_backup_hist(ch *menu)
{
   ch       *fspec1;
   ch       fspec2[200], histfile[200];
   ch       *pp;
   int      ii, jj, nn;
   int      zstat, ftf;
   zdialog  *zd;
   zlist_t  *zlist = 0;

   txwidget_append2(mLog,0," available history files in %s \n",homedir);

   snprintf(fspec2,199,"%s/dkopp-hist-*",homedir);
   zlist = zlist_new(maxhist);
   ftf = 1;
   nn = 0;

   while (true)
   {
      fspec1 = SearchWild(fspec2,ftf);                                                 //  file: dkopp-hist-yyyymmdd-hhmm-label
      if (! fspec1) break;
      pp = (ch   *) strrchr(fspec1,'/') + 12;                                          //  get yyyymmdd-hhmm-label
      if (nn == maxhist) break;
      zlist_append(zlist,pp,0);                                                        //  add to list
      nn++;
   }

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

   zlist_purge(zlist);                                                                 //  purge null entries                 7.4
   zlist_sort(zlist);                                                                  //  sort list ascending

   for (ii = 0; ii < nn; ii++)                                                         //  report sorted list
      txwidget_append2(mLog,0," dkopp-hist-%s \n",zlist_get(zlist,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,"combo","hfile","dialog");

   jj = nn - 20;
   if (jj < 0) jj = 0;
   for (ii = jj; ii < nn; ii++)                                                        //  stuff combo box list with
      zdialog_stuff(zd,"hfile",zlist_get(zlist,ii));                                   //    20 newest hist file IDs
   zdialog_stuff(zd,"hfile",zlist_get(zlist,nn-1));                                    //  default entry is newest file

   zdialog_run(zd,0,"parent");                                                         //  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

   zshell("log ack","xdg-open %s/%s-%s",homedir,"dkopp-hist",histfile);                //  view the file

report_exit:
   if (zlist) zlist_free(zlist);
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

int RJedit(ch *menu)
{
   zdialog        *zd;

   txwidget_append2(mLog,0,"\n Restore files from DVD/BRD \n");

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

   ++Fdialog;

   zd = zdialog_new("copy files from DVD/BRD",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/BRD device");                     //  DVD/BRD device       [___________][v]
   zdialog_add_widget(zd,"combo","entdvd","vb2",BJdvd);

   zdialog_add_widget(zd,"label","labfrom","vb1","copy-from DVD/BRD");                 //  copy-from DVD/BRD   [______________]
   zdialog_add_widget(zd,"label","labto","vb1","copy-to disk");                        //  copy-to disk        [______________]
   zdialog_add_widget(zd,"zentry","entfrom","vb2",RJfrom);
   zdialog_add_widget(zd,"zentry","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,"zedit","editfiles","scrfiles");

   for (int ii = 0; ii < ndvds; ii++)                                                  //  load curr. data into widgets
      zdialog_stuff(zd,"entdvd",dvddevdesc[ii]);
                                                                                       //  remove get/stuff mount point
   editwidget = zdialog_gtkwidget(zd,"editfiles");

   for (int ii = 0; ii < RJnx; ii++)                                                   //  get restore include/exclude recs,
      txwidget_append2(editwidget,0,"%s""\n",RJinex[ii]);                              //   pack into file selection edit box

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


//  edit dialog event function

int  RJedit_event(zdialog *zd, ch *event)
{
   ch       text[40], *pp, fcfrom[XFCC];
   int      zstat, line, 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/BRD device
   strncpy0(BJdvd,text,19);
   pp = strchr(BJdvd,' ');
   if (pp) *pp = 0;
                                                                                       //  remove fetch/save mount point
   zdialog_fetch(zd,"entfrom",RJfrom,XFCC);                                            //  copy-from location /home/xxx/.../
   strTrim(RJfrom);

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

   for (line = 0; ; line++)
   {
      pp = txwidget_line(editwidget,line,1);
      if (! pp || ! *pp) break;
      cc = strTrim(pp);                                                                //  remove trailing blanks
      if (cc < 3) continue;                                                            //  ignore absurdities
      if (cc > XFCC-100) continue;
      RJinex[RJnx] = zstrdup(pp,"RJinex");                                             //  copy new record
      if (++RJnx == maxnx) {
         txwidget_append2(mLog,0," *** 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 txwidget_append2(mLog,0," *** correct errors in restore job \n");

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


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

int RJlist(ch *menu)
{
   int       cc1, cc2;
   ch       *file1, file2[XFCC];

   if (! RJval) {
      txwidget_append2(mLog,0," *** restore job has errors \n");
      goto list_exit;
   }

   txwidget_append2(mLog,0,"\n copy %d files from DVD/BRD: %s \n",Rnf, RJfrom);
   txwidget_append2(mLog,0,"    to directory: %s \n",RJto);
   txwidget_append2(mLog,0,"\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 (! strmatchN(file1,RJfrom,cc1)) {
         txwidget_append2(mLog,0," *** not within copy-from: %s \n",kleenex(file1));
         RJval = 0;
         continue;
      }

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

list_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


//  Restore files based on data from restore dialog

int Restore(ch *menu)
{
   int      ii, nn, ccf;
   ch       dfile[XFCC];
   ch       *errmess;

   if (! RJval || ! Rnf) {
      txwidget_append2(mLog,0," *** 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;

   txwidget_append2(mLog,1,"\n""begin restore of %d files to: %s \n",Rnf,RJto);

   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);
      txwidget_append2(mLog,0," %s \n",kleenex(dfile));
      errmess = copyfile(Rrec[ii].file,dfile);
      if (errmess) txwidget_append2(mLog,0," *** %s \n",errmess);
   }

   restore_filepoop();                                                                 //  restore owner/permissions

   dFilesReset();                                                                      //  reset disk file data

restore_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


//  get available DVD/BRD devices
//  the lshw command blocks everything for several seconds

int getDVDs(void *)                                                                    //  overhauled
{
   int      ii, dvdrw;
   ch       buff[200], *pp;
   ch       command[50] = "lshw -class disk 2>/dev/null";                              //  better than udevadm
   FILE     *fid;

   dvdrw = ndvds = 0;

   fid = popen(command,"r");
   if (! fid) {
      txwidget_append2(mLog,0," No DVD/BRD devices found");
      return 0;
   }
   
   while (true)
   {
      pp = fgets_trim(buff,200,fid);                                                   //  read lshw command output
      if (! pp) break;

      if (strstr(buff,"*-")) {                                                         //  start some device
         if (strstr(buff,"*-cdrom")) dvdrw = 1;                                        //  start DVD/BRD device
         else dvdrw = 0;
         continue;
      }

      if (! dvdrw) continue;                                                           //  ignore recs for other devices

      if (strstr(buff,"description:")) {
         pp = strstr(buff,"description:");                                             //  save DVD/BRD description
         pp += 12;
         if (*pp == ' ') pp++;                                                         //  (assume description comes first)
         strncpy0(dvddesc[ndvds],pp,40);
         continue;
      }

      if (strstr(buff,"/dev/")) {
         pp = strstr(buff,"/dev/");                                                    //  have /dev/sr0 or similar format
         if (pp[7] < '0' || pp[7] > '9') continue;
         pp[8] = 0;
         strcpy(dvddevs[ndvds],pp);                                                    //  save DVD/BRD device
         ndvds++;
         continue;
      }
   }
   
   pclose(fid);

   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]);
   }

   txwidget_append2(mLog,0," DVD/BRD devices found: %d \n",ndvds);                     //  output list of DVDs
   for (ii = 0; ii < ndvds; ii++)
      txwidget_append2(mLog,0," %s %s \n",dvddevs[ii],dvddesc[ii]);

   return 0;
}


//  set DVD/BRD device and mount point

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

   if (*scriptParam) {                                                                 //  script
      Nth = 1;                                                                         //  parse: /dev/dvd /media/xxxx
      pp1 = substring(scriptParam,' ',Nth++);
      if (pp1) strncpy0(BJdvd,pp1,19);
      pp1 = substring(scriptParam,' ',Nth++);
      if (pp1) {
         strncpy0(dvdmp,pp1,99);
         dvdmpcc = strlen(dvdmp);
         if (dvdmp[dvdmpcc-1] == '/')
            dvdmp[dvdmpcc--] = 0;                                                      //  remove trailing /
      }
      *scriptParam = 0;
      return 0;
   }

   zd = zdialog_new("select DVD/BRD drive",mWin,"OK","cancel",null);                   //  dialog to select DVD/BRD & 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/BRD device");
   zdialog_add_widget(zd,"label","labmp","vb1","mount point");
   zdialog_add_widget(zd,"combo","entdvd","vb2",BJdvd);
   zdialog_add_widget(zd,"zentry","entmp","vb2",dvdmp);

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

   zdialog_run(zd,0,"parent");
   zstat = zdialog_wait(zd);

   if (zstat != 1) {
      zdialog_free(zd);
      return 0;
   }

   zstat = zdialog_fetch(zd,"entdvd",text,60);                                         //  get selected DVD/BRD
   strncpy0(BJdvd,text,19);
   pp2 = strchr(BJdvd,' ');
   if (pp2) *pp2 = 0;

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

   txwidget_append2(mLog,0," DVD/BRD and mount point: %s %s \n",BJdvd,dvdmp);
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   zdialog_free(zd);
   return 0;
}


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

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

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


//  Mount DVD/BRD with message feedback to window.

int mountDVD(ch *menu)                                                                 //  menu mount function
{
   int      err, reset;
   ch       buff[400], mbuff[200];
   ch       *pp, *pp1;
   FILE     *fid;
   STATB    dstat;

   if (dvdmtd) {
      err = stat(dvdmp,&dstat);
      if ((! err) && (dvdtime == dstat.st_ctime)) return 0;                            //  medium unchanged, do nothing
   }

   dvdmtd = 0;                                                                         //  set DVD/BRD not mounted
   dvdtime = -1;
   strcpy(mediumDT,"unknown");
   *mediumDT = 0;
   err = reset = 0;
   vFilesReset();                                                                      //  reset DVD/BRD files

   fid = popen("cat /etc/mtab","r");                                                   //  get mounted devices
   if (! fid) goto trymount;

   while (true)
   {
      pp = fgets_trim(buff,400,fid);
      if (! pp) break;

      pp1 = substring(pp,' ',1);                                                       //  get /dev/xxx
      if (! pp1 || ! strmatch(pp1,BJdvd)) continue;                                    //  not my DVD/BRD

      pp1 = substring(pp,' ',2);                                                       //  get mount point
      if (! pp1) continue;

      repl_1str(pp1,dvdmp,100,"\\040"," ");                                            //  replace "\040" with " "
      dvdmpcc = strlen(dvdmp);
      txwidget_append2(mLog,0," already mounted: %s %s \n",BJdvd,dvdmp);
      dvdmtd = 1;
   }
   
   pclose(fid);

   if (dvdmtd) goto showpoop;

trymount:

   mkdir(dvdmp,0755);                                                                  //  create default mount point

   snprintf(mbuff,200,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp);                      //  mount the DVD/BRD
   err = do_shell("mount",mbuff);
   if (! err) {
      dvdmtd = 1;
      goto showpoop;
   }

   zmessageACK(mWin,"mount DVD/BRD and wait for completion");
   
   while (true)
   {
      fid = popen("cat /etc/mtab","r");                                                //  get mounted disk info
      if (! fid) goto mount;
      
      while (true)
      {
         pp = fgets_trim(buff,400,fid);
         if (! pp) break;

         pp1 = substring(pp,' ',1);                                                    //  get /dev/xxx
         if (! pp1 || ! strmatch(pp1,BJdvd)) continue;                                 //  not my DVD/BRD

         pp1 = substring(pp,' ',2);                                                    //  get mount point
         if (! pp1) continue;

         repl_1str(pp1,dvdmp,100,"\\040"," ");                                         //  replace "\040" with " "
         dvdmpcc = strlen(dvdmp);
         txwidget_append2(mLog,0," %d %d mounted \n",BJdvd,dvdmp);
         dvdmtd = 1;
      }
      
      pclose(fid);

      if (dvdmtd) goto showpoop;                                                       //  mounted OK

mount:

      snprintf(mbuff,200,"mount -t iso9660 %s %s 2>&1",BJdvd,dvdmp);                   //  mount the DVD/BRD
      err = do_shell("mount",mbuff);
      if (! err) {
         dvdmtd = 1;
         goto showpoop;
      }

      txwidget_append2(mLog,0," waiting for mount ... \n");

      for (int ii = 0; ii < 5; ii++)                                                   //  5 secs between "wait" messages
      {
         if (checkKillPause()) {                                                       //  killed by user
            commFail++;
            return 1;
         }
         zsleep(1);
      }
   }

showpoop:

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

   snprintf(buff,99,"volname %s",BJdvd);                                               //  get DVD/BRD label
   fid = popen(buff,"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);
   }

   txwidget_append2(mLog,0," DVD/BRD label: %s  last dkopp: %s \n",dvdlabel,mediumDT);
   commFail = 0;
   return 0;
}


//  unmount DVD/BRD

int unmountDVD(ch *menu)
{
   ch       command[100];

   vFilesReset();
   dvdmtd = 0;
   dvdtime = -1;

   snprintf(command,100,"umount %s 2>&1",dvdmp);                                       //  use mount point
   do_shell("umount",command);
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   commFail = 0;                                                                       //  ignore unmount error
   return 0;
}


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

int ejectDVD(ch *menu)
{
   ch       command[60];

   vFilesReset();
   dvdmtd = 0;
   dvdtime = -1;

   sprintf(command,"eject %s 2>&1",BJdvd);
   do_shell("eject",command);
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   commFail = 0;                                                                       //  ignore eject error
   return 0;
}


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

int resetDVD(ch *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/BRD drive
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

int eraseDVD(ch *menu)
{
   ch       command[200];
   int      nstat;

   nstat = zmessageYN(mWin,"Erase DVD/BRD. This will take some time. \n Continue?");
   if (! nstat) goto erase_exit;

   vFilesReset();                                                                      //  reset DVD/BRD 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) txwidget_append2(mLog,0," ready \n");
   return 0;
}


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

int formatDVD(ch *menu)
{
   ch       command[60];
   int      nstat;

   nstat = zmessageYN(mWin,"Format DVD/BRD. This will take 2-4 minutes. \n Continue?");
   if (! nstat) goto format_exit;

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

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

format_exit:
   if (Fgui) txwidget_append2(mLog,0," ready \n");
   return 0;
}


//  Display help/about or help/user guide

int helpFunc(ch *menu)
{
   if (strmatch(menu,"about")) zabout(mWin);                                           //  7.7
   if (strmatch(menu,"user guide")) showz_docfile(mWin,"userguide",0);
   return 0;
}


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

int fc_dialog(ch *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;
   ch       *file1, *file2, *ppf;
   int      ii, err, hide;
   STATB    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 = (ch   *) g_slist_nth_data(flist,ii);
         if (! file1) break;

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

         err = stat(file2,&filestat);
         if (err) txwidget_append2(mLog,0," *** error: %s  file: %s \n",
                                             strerror(errno),kleenex(file2));

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

         ppf = file2;
         if (strmatchN(ppf,dvdmp,dvdmpcc)) ppf += dvdmpcc;                             //  omit DVD/BRD mount point

         if (arg == 101) txwidget_append2(editwidget,0,"include %s""\n",ppf);
         if (arg == 102) txwidget_append2(editwidget,0,"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) {
      txwidget_append2(mLog,0," *** 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;
   ch       file[XFCC], dirk[XFCC], pdirk[XFCC], *pp;
   STATB    dstat;

   fid = fopen(TFfilepoop,"w");
   if (! fid) {
      txwidget_append2(mLog,0," *** 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 = stat(dirk,&dstat);                                                      //  get owner and permissions
         if (err) {
            txwidget_append2(mLog,0," *** 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 = stat(file,&dstat);                                                         //  get owner and permissions
      if (err) {
         txwidget_append2(mLog,0," *** 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;
   ch       file1[XFCC], file2[XFCC];
   ch       poopfile[100];

   txwidget_append2(mLog,0,"\n restore directory owner and permissions \n");
   txwidget_append2(mLog,0,"  for directories anchored at: %s \n",RJto);

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

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

   fid = fopen(poopfile,"r");
   if (! fid) {
      txwidget_append2(mLog,0," *** cannot open DVD/BRD 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
      txwidget_append2(mLog,0," owner: %4d:%4d  permissions: %3o  file: %s \n",
                                    uid, gid, perms, kleenex(file2));
      err = chown(file2,uid,gid);
      if (err) txwidget_append2(mLog,0," *** error: %s \n",strerror(errno));
      err = chmod(file2,perms);
      if (err) txwidget_append2(mLog,0," *** error: %s \n",strerror(errno));

   }

   fclose(fid);
   return 0;
}


//  create backup history file after successful backup

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

   snprintf(backupfile,199,"%s/dkopp-hist-%s-%s",                                      //  create history file name:
                      homedir,backupDT,dvdlabel);                                      //    dkopp-hist-yyyymmdd-hhmm-dvdlabel

   txwidget_append2(mLog,1,"\n""create history file: %s \n",backupfile);

   fid = fopen(backupfile,"w");
   if (! fid) {
      txwidget_append2(mLog,0," *** 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 (strmatch(mbmode,"full"))
      for (ii = 0; ii < Dnf; ii++)                                                     //  output all files for backup
         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) txwidget_append2(mLog,0," *** 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(ch   * rec, ch   *& rtype, ch   *& fspec)
{
   ch      *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) > XFCC-100) return 3;

   if (strmatch(rtype,"exclude")) return 1;                                            //  exclude, done
   if (! strmatch(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(ch *menu)
{
   int         ii, err, nerr = 0;
   int         year, mon, day;
   struct tm   tm_date, *tm_date2;
   STATB       dstat;

   txwidget_append2(mLog,1,"\n""Validate backup job data \n");

   BJval = 0;

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

   txwidget_append2(mLog,0," DVD/BRD device: %s \n",BJdvd);

   err = stat(BJdvd,&dstat);
   if (err || ! S_ISBLK(dstat.st_mode)) {
      txwidget_append2(mLog,0," *** DVD/BRD device is apparently invalid \n");
      nerr++;
   }

   txwidget_append2(mLog,0," backup %s \n",BJbmode);
   if (! strmatchV(BJbmode,"full","incremental","accumulate",null)) {
      txwidget_append2(mLog,0," *** backup mode not full/incremental/accumulate \n");
      nerr++;
   }

   txwidget_append2(mLog,0," verify %s \n",BJvmode);
   if (! strmatchV(BJvmode,"full","incremental","thorough",null)) {
      txwidget_append2(mLog,0," *** verify mode not full/incremental/thorough \n");
      nerr++;
   }

   txwidget_append2(mLog,0," file date from: %s \n",BJdatefrom);                       //  file age limit
   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
   if (tm_date2->tm_mon - mon + 1 != 0) err = 5;
   if (tm_date2->tm_mday - day != 0) err = 6;
   if (err) {
      txwidget_append2(mLog,0," *** date must be > 1970.01.01 \n");
      nerr++;
      BJtdate = 0;
   }

   nerr += nxValidate(BJinex,BJnx);                                                    //  validate include/exclude recs

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


//  validate restore job data

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

   if (RJval) return 1;

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

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

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

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

   txwidget_append2(mLog,0,"   copy-to: %s \n",RJto);
   pdirk = opendir(RJto);                                                              //  validate copy-to location
   if (! pdirk) {
      txwidget_append2(mLog,0," *** 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

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


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

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

   for (ii = 0; ii < nrecs; ii++)                                                      //  process include/exclude recs
   {
      strcpy(nxrec,inexrecs[ii]);
      txwidget_append2(mLog,0," %s \n",nxrec);                                         //  output

      nstat = inexParse(nxrec,rtype,fspec);                                            //  parse
      if (nstat == 0) continue;                                                        //  comment
      if (nstat == 1) continue;                                                        //  OK

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

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

   return errs;
}


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

int dGetFiles()
{
   ch       *fsp;
   ch       *rtype, *fspec, bjrec[XFCC], *mbytes;
   int      ftf, cc, nstat, wstat, err;
   int      ii, jj, nfiles, nexc;
   double   nbytes;
   STATB    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

   txwidget_append2(mLog,1,"\n""finding all files for backup \n");

   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 (strmatch(rtype,"include"))                                                   //  include filespec
      {
         ftf = 1;

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

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

            Drec[Dnf].file = zstrdup(fsp,"Drec");

            err = lstat(fsp,&filestat);                                                //  check accessibility
            if (err == 0) {
               if (! S_ISREG(filestat.st_mode) &&                                      //  include files and symlinks
                   ! 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) {
               txwidget_append2(mLog,0," *** exceeded %d files \n",maxfs);
               goto errret;
            }
         }
      }

      if (strmatch(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;

      if (Drec[ii].stat)
      {
         err = stat(Drec[ii].file,&filestat);
         txwidget_append2(mLog,0," *** %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 (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
         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) txwidget_append2(mLog,0," %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,3);
   txwidget_append2(mLog,0," files for backup: %d  %s \n",nfiles,mbytes);

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

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

   for (ii = 1; ii < Dnf; ii++)                                                        //  look for duplicate files
      if (strmatch(Drec[ii].file,Drec[ii-1].file)) {
         txwidget_append2(mLog,0," *** 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/BRD 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;
   ch       command[200], *pp;
   ch       fspec1[XFCC], fspec2[XFCC];
   FILE     *fid;
   STATB    filestat;

   if (Vnf) return 0;                                                                  //  avoid refresh

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

   txwidget_append2(mLog,1,"\n""find all DVD/BRD files \n");

   snprintf(command,200,"find \"%s\" -type f -or -type l >%s",                         //  get regular files and symlinks
                                          dvdmp,TFdvdfiles);                           //  add quotes in case of blanks
   txwidget_append2(mLog,0," %s \n",command);

   err = zshell(0,command);                                                            //  list all DVD/BRD files to temp file
   if (err) {
      txwidget_append2(mLog,0," *** find command failed: %s \n",strerror(err));
      commFail++;
      return 0;
   }

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

   gcc = strlen(V_DKOPPDIRK);

   while (1)
   {
      pp = fgets_trim(fspec1,XFCC-2,fid);                                              //  get next file
      if (! pp) break;                                                                 //  eof

      cc = strlen(pp);                                                                 //  absurdly long file name
      if (cc > XFCC-100) {
         txwidget_append2(mLog,0," *** absurd file skipped: %200s (etc.) \n",kleenex(pp));
         continue;
      }

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

      err = lstat(fspec1,&filestat);                                                   //  check accessibility
      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((ch   *) Vrec,sizeof(vfrec),Vnf,'A');                                  //  sort Vrec[Vnf] by Vrec[].file

   txwidget_append2(mLog,0," DVD/BRD files: %d \n",Vnf);
   return 0;
}


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

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

   if (! RJval) return 0;

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

   txwidget_append2(mLog,0,"\n""find all DVD/BRD files to restore \n");

   for (ii = 0; ii < RJnx; ii++)                                                       //  process include/exclude recs
   {
      strcpy(rjrec,RJinex[ii]);                                                        //  next record
      txwidget_append2(mLog,0," %s \n",rjrec);                                         //  output

      nstat = inexParse(rjrec,rtype,fspec);                                            //  parse
      if (nstat == 0) continue;                                                        //  comment

      repl_1str(fspec,fspecx,XFCC,"\\=","=");                                          //  replace "\=" with "=" in file name

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

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

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

      if (strmatch(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++;
         }

         txwidget_append2(mLog,0,"  %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;
   txwidget_append2(mLog,0," total file count: %d \n",Rnf);

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

   for (ii = 0; ii < Rnf; ii++)                                                        //  get selected DVD/BRD files to restore
   {
      if (! strmatchN(Rrec[ii].file,RJfrom,cc)) {
         txwidget_append2(mLog,0," *** not under copy-from; %s \n",Rrec[ii].file);
         RJval = 0;                                                                    //  mark restore job invalid
         continue;
      }
   }

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


//  helper function for backups and reports
//
//  compare disk and DVD/BRD files, set dispositions in Drec[] and Vrec[] arrays
//       n  new         on disk, not on DVD/BRD
//       d  deleted     on DVD/BRD, not on disk
//       m  modified    on both, but not equal
//       u  unchanged   on both, and equal

int setFileDisps()
{
   int      dii, vii, comp;
   ch       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/BRD files parallel
   {
      if ((dii < Dnf) && (vii == Vnf)) comp = -1;                                      //  disk file after last DVD/BRD file
      else if ((dii == Dnf) && (vii < Vnf)) comp = +1;                                 //  DVD/BRD file after last disk file
      else comp = strcmp(Drec[dii].file,Vrec[vii].file);                               //  compare disk and DVD/BRD 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/BRD file: not on disk
         Vrec[vii].disp = 'd';
         ndel++;                                                                       //  count deleted files
         vii++;
      }

      else if (comp == 0)                                                              //  file present on disk and DVD/BRD
      {
         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/BRD files, restore files).
//  Sort ascii sequence, or sort subdirectories in a directory before files.

int SortFileList(ch   * recs, int RL, int NR, ch   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(ch *rec1, ch *rec2)                                                         //  ascii comparison
{
   dfrec  *r1 = (dfrec *) rec1;
   dfrec  *r2 = (dfrec *) rec2;
   return strcmp(r1->file,r2->file);
}

int fcompD(ch *rec1, ch *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(ch *file1, ch *file2)                                                     //  special compare filenames
{                                                                                      //  subdirectories compare before files
   ch       *pp1, *pp10, *pp2, *pp20;
   ch       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/BRD 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/BRD 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/BRD to disk

ch *copyfile(ch *vfile, ch *dfile)
{
   ch       vfile1[XFCC], vfilex[XFCC];
   int      fid1, fid2, err, rcc;
   ch       *pp, buff[vrcc];
   ch       *errmess;
   STATB    fstat;
   struct timeval ftimes[2];

   strcpy(vfile1,dvdmp);                                                               //  prepend DVD/BRD mount point
   strcat(vfile1,vfile);
   repl_1str(vfile1,vfilex,XFCC,"=","\\=");                                            //  replace "=" with "\=" in DVD/BRD 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);

   stat(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",strerror(err));
   utimes(dfile,ftimes);

   return 0;
}


//  Verify helper function
//  Verify that file on BRD/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

ch *checkFile(ch *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/BRD
   ch       vfile[XFCC], *vbuff = 0, *dbuff = 0;
   ch       *errmess = 0;
   double   dtime, vtime;
   STATB    filestat;

   tcc = 0.0;

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

   err = lstat(vfile,&filestat);                                                       //  check symlinks but do not follow
   if (err) return strerror(errno);
   if (S_ISLNK(filestat.st_mode)) return 0;

   if (compf) goto comparefiles;

   vfid = open(vfile,open_flags);                                                      //  open DVD/BRD 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/BRD 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
   }
   goto cleanup;

comparefiles:

   vfid = open(vfile,open_flags);                                                      //  open DVD/BRD 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
   }

   if (vcc != dcc) cmperr++;

   if (cmperr) {                                                                       //  compare error
      stat(dfile,&filestat);
      dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano;                     //  file modified since snapshot?
      stat(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(ch *filespec)
{
   int      cc;
   ch       pdirk[300], pfile[300], *pp;

   if (! Fgui) {
      printf(" %s \n",filespec);
      return 0;
   }

   pp = (ch   *) strrchr(filespec+1,'/');                                              //  parse directory/filename
   if (pp) {
      cc = pp - filespec + 2;
      strncpy0(pdirk,filespec,cc);
      strncpy0(pfile,pp+1,299);
   }
   else {
      strcpy(pdirk," ");
      strncpy0(pfile,filespec,299);
   }

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


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

int track_filespec_err(ch *filespec, ch *errmess)
{
   if (Fgui) {
      txwidget_replace(mLog,0,-2," *** %s  %s \n",errmess,kleenex(filespec));
      txwidget_append2(mLog,0," \n");
   }
   else printf(" %s  %s \n",errmess,filespec);
   return 0;
}


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

ch *kleenex(ch *name)
{
   static ch   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(ch *pname, ch *command)
{
   int      scroll, pscroll;
   int      err = 0, gerr = 0;
   ch       *pp, buff[1000];
   ch       *errmess;
   FILE     *fid;
   
   static pollfd *pfd = 0;                                                             //  8.1
   
   if (! pfd)
      pfd = (pollfd *) zmalloc(sizeof(pollfd),"pollfd"); 

   txwidget_append2(mLog,1,"\n""shell: %s \n",command);

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

   scroll = pscroll = 1;
   txwidget_scroll(mLog,-1);                                                           //  scroll to last line

   fid = popen(command,"r");
   if (! fid) {
      err = errno;
      goto checkstat;
   }
   
   while (true)
   {
      pfd->fd = fid->_fileno;
      pfd->events = POLLIN + POLLHUP;
      pfd->revents = 0;

      while (! poll(pfd,1,10000)) {                                                    //  wait for data or 10 seconds           8.1
         zmainloop();
         txwidget_append2(mLog,0," waiting ...\n");
      }
  
      pp = fgets_trim(buff,1000,fid);
      if (! pp) break;

      pscroll = scroll;
      scroll = 1;

      if (strmatch(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;
      }

      if (strstr(buff,"formatting")) scroll = 0;                                       //  dvd+rw-format output

      if (scroll) {                                                                    //  output to next line
         txwidget_append2(mLog,0," %s: %s \n",pname,kleenex(buff));
         zsleep(0.002);                                                                //  throttle output a little
      }
      else if (Fgui) {                                                                 //  supress output in batch mode
         if (pscroll) txwidget_append2(mLog,0,"\n");                                   //  transition from scroll to overlay
         txwidget_replace(mLog,0,-1," %s: %s \n",pname,kleenex(buff));                 //  output, overlay prior output
      } 

      if (killFlag) {
         txwidget_append2(mLog,0,"*** %s \n","pkill %s",subprocName);
         err = zshell(0,buff);
      }
      
      zmainloop();

      while (pauseFlag) zmainsleep(0.2);
   }
   
   pclose(fid);

checkstat:

   errmess = 0;
   if (err) errmess = strerror(err);

   if (strmatch(pname,"growisofs")) {
      err = gerr;
      if (err == 997) err = 0;                                                         //  growisofs likes 997
      if (err) errmess = "growisofs failure";
   }
   if (err) txwidget_append2(mLog,0," %s status: %d %s \n", pname, err, errmess);
   else txwidget_append2(mLog,0," %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/BRD space used) / (final DVD/BRD space used).

int track_growisofs_files(ch   * buff)
{
   static double     bbytes, gpct0, gpct;
   static int        dii, dii2, err;
   static ch         *dfile;

   if (! buff) {                                                                       //  initialize
      dii = 0;
      dii2 = -1;
      bbytes = 0;
      dfile = (ch   *) "";
      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 (strmatch(mbmode,"full")) {                                                      //  full backup, possibly > 1 DVD/BRD
      while (dii < Dnf) {
         if (bbytes/Dbytes2 > gpct/100) break;                                         //  exit if enough bytes
         bbytes += Drec[dii].size;                                                     //  sum files
         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 KBevent(GdkEventKey *event)
{ return; }



