diff -Nru ukopp-4.4/Makefile ukopp-4.7/Makefile --- ukopp-4.4/Makefile 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/Makefile 2013-02-27 22:11:30.000000000 +0000 @@ -1,11 +1,11 @@ # ukopp makefile PROGRAM = ukopp -VERSION = 4.4 +VERSION = 4.7 SOURCE = $(PROGRAM)-$(VERSION).cc # defaults for parameters that may be pre-defined -CXXFLAGS ?= -O3 -g -Wall -Wno-deprecated-declarations +CXXFLAGS ?= -O3 -g -Wall LDFLAGS ?= -O3 -g -Wall -rdynamic PREFIX ?= /usr @@ -26,8 +26,10 @@ $(PROGRAM).o: $(SOURCE) $(CXX) $(CFLAGS) -o $(PROGRAM).o $(SOURCE) -zfuncs.o: zfuncs.cc - $(CXX) $(CFLAGS) zfuncs.cc -D PREFIX=\"$(PREFIX)\" +zfuncs.o: zfuncs.cc zfuncs.h + $(CXX) $(CFLAGS) zfuncs.cc \ + -D PREFIX=\"$(PREFIX)\" -D DOCDIR=\"$(DOCDIR)\" \ + -Wno-deprecated-declarations install: $(PROGRAM) mkdir -p $(DESTDIR)$(BINDIR) @@ -36,7 +38,7 @@ mkdir -p $(DESTDIR)$(MANDIR) mkdir -p $(DESTDIR)$(PREFIX)/share/applications cp -f $(PROGRAM) $(DESTDIR)$(BINDIR) - cp -f icons/* $(DESTDIR)$(ICONDIR) + cp -f -R icons/* $(DESTDIR)$(ICONDIR) cp -f -R doc/* $(DESTDIR)$(DOCDIR) # man page cp -f doc/$(PROGRAM).man $(PROGRAM).1 @@ -49,8 +51,8 @@ uninstall: rm -f $(DESTDIR)$(BINDIR)/$(PROGRAM) - rm -R -f $(DESTDIR)$(SHAREDIR) - rm -R -f $(DESTDIR)$(DOCDIR) + rm -f -R $(DESTDIR)$(SHAREDIR) + rm -f -R $(DESTDIR)$(DOCDIR) rm -f $(DESTDIR)$(MANDIR)/$(PROGRAM).1.gz xdg-desktop-menu uninstall $(DESTDIR)$(MENUFILE) rm -f $(DESTDIR)$(MENUFILE) Binary files /tmp/rcpYkS0uOT/ukopp-4.4/data/images/jobedit.jpeg and /tmp/1DX0xDIX7w/ukopp-4.7/data/images/jobedit.jpeg differ diff -Nru ukopp-4.4/data/userguide-en.html ukopp-4.7/data/userguide-en.html --- ukopp-4.4/data/userguide-en.html 1970-01-01 00:00:00.000000000 +0000 +++ ukopp-4.7/data/userguide-en.html 2013-02-27 22:11:30.000000000 +0000 @@ -0,0 +1,1100 @@ + + + + + + + + + + +ukopp user guide   v.4.7
+
+concepts
+
+first tryout  (1-page primer for +those with RTFM problems)
+
+toolbar buttons
+
+file menu +
+
+backup menu
+verify menu
+
+report menu
+
+
restore menu +
+format menu
+
editing +backup jobs +
+technical notes
+
+
+
License and +Warranty +
+
Ukopp is a free program licensed under the GNU General Public +License V3 (Free Software Foundation). Ukopp is not warranted for any +purpose, but if you find a bug, I will try to fix it.
+
+
+Origin and Contact
+
+
Ukopp originates from the author's web site at: http://kornelix.com/ukopp
+Other web sites may offer it for download. Modifications could have +been made.
+
+If you have questions, suggestions or a bug to report:
kornelix@yahoo.de

+
+
+Introduction

+
+Ukopp
+is a Linux +program for copying or backing-up disk files to a separate storage +device, e.g. a USB drive or SD memory card. Any disk directory may also +be +used as a backup location. You +can select files +to be copied +using a GUI. +You +can navigate through the file system and select files or +directories to include or exclude at any level in the directory +hierarchy. These choices can be saved in a job +file +to automate recurring backups. +If +new files appear in an included or excluded directory, they are +automatically taken into account. You need to revise the job file only +if you change the directories or make new exceptions within those +directories. +
+
+Ukopp
copies only +new and modified files: +files that have not changed since the last backup are bypassed in +microseconds. A typical daily backup of personal files can be done in a +few seconds. Ukopp can optionally retain +previous +versions +of backup files instead of overwriting them with newer versions. You +can optionally specify the retention time and / or the number of +versions to +retain for each group of included files. You can see these versions in +the +backup directories and recover them if needed.
+
+Ukopp has a +
synchronize +function, which is a simple method to keep files in two computers +synchronized using a USB stick or other portable memory. Ukopp copies +the newest version of a file from one device to the other. +
+
+Backups +can be verified three ways: full, incremental, and compare. A
full +verify +reads all the backup files and reports any files having read errors. +An incremental +verify +reads only those files that have been newly written by a preceding +backup job. This is very fast and provides a high level of security. +A compare +verify +reads all backup files and compares them with their corresponding +disk files. This is normally not necessary, but provides an effective +check that all hardware and software is working correctly. +
+
+You +can report all files in a backup job, or all files in a backup +directory. You can search for file names using wildcards. You +can report the differences between backup files and their +corresponding disk files: files that have been created, deleted, or +modified since the backup was made. These reports are available in +three levels of detail: a list of all changed files, total file and +byte counts per directory, and overall totals.
+
+
+For +disaster recovery or file transfer, ukopp has a
file +restore +capability. You can select and restore backup files to their original +directories or anywhere else. File owner, permissions, and modification +time are are also +restored, even if the backup device uses a Microsoft FAT file system +(normally the initial state for USB drives).
+
+
+
+
Concepts +
+
+The files in a backup job are specified with include and exclude +records. These have filespecs with optional wildcards placed almost +anywhere.
+
+Examples:
+
+
    +include /home/*         +        # add all user files
+ +    +include /root/*         +        # add all root files
+ +    +include /shared/*/documents/*   # add shared document files
+ +    +exclude */mp3/*         +        # remove files in mp3 directories +
+ +    +exclude */.Trash/*         +     # remove trash files +
+
+
+The +first include adds all files in all users' home directories +and sub-directories. The second include adds all files under the /root +directory. +The third include adds all files under the /shared top directory that +also have an intermediate directory named /documents. The two exclude +records remove all files within all /.Trash and /mp3 directories. +
+
+
The backup target is the +device or directory where the source files are copied-to.
+This is the directory of a mounted device or some other specified +directory.
+If the target is /media/sdb2 +(where the device sdb2 is mounted)
+and the source files are:  /home/<user>/documents/*, +
+the backup files will be:  /media/sdb2/home/<user>/documents/*. +
+
+GUI interface:
The above records are normally generated using a +file selection dialog.
+This is documented in a following section: editing backup jobs.
+
+
+File Selection Logic

+
    +loop: +
+        get next include/exclude record

+        if EOF, done

+        if include: add all matching +files to backup file set

+
    +    if exclude: remove all matching files from backup +file set
+    loop-end
+
+
+
Note that +excludes are effective only against prior includes. They have no +effect on following includes, which are processed afterwards. See the +section on editing backup jobs. +
+
+Restriction: include records must include at least the first directory +name (top-level) without wildcards (the GUI file-chooser does this +automatically).

+

+Retaining +multiple file versions: +if this option is elected, existing backup files that need updating are +renamed with a version number instead of being overwritten. If the +backup file "foo.bar" is updated, it is renamed to "foo.bar (1)", and +"foo.bar" becomes the newest backup. If it is updated again, "foo.bar" +is renamed to "foo.bar (2)", and so forth. Newer versions have higher +numbers, and the unversioned file is always the current or latest +version. The section on editing job files explains how to specify old +version retention policies. +
+
+Ukopp limitations
+
+    +max. 500,000 files in a backup job (compile time constant)
+    +max. file retention is 9999 days and 9999 versions
+    must run as +root user to mount backup device with [mount] button
+ +    +must run as root user or use sudo to copy non-owned files
+
    +not useful for disk imaging (operating system backup)
+
+
+

+Ukopp +first tryout

+
+After +installing ukopp, please perform the following short exercise. This may +be +all you need at first. You can enhance your file security and +ultimately save time if you read this whole document.
The +exercise +will check that ukopp functions correctly +on your system and help you become familiar with ukopp usage. +
+
    +
  1. Choose a backup device or directory. If using a +pluggable device (e.g. USB drive), plug it in.
  2. +
  3. Start ukopp: click the desktop launcher or input a +terminal command: $ ukopp.
  4. +
  5. Select button [ target ]. The drop-down list shows +disk devices and their mount points if mounted. You can choose one of +these, or input your chosen backup directory.
  6. +
  7. Select +button [ mount ] if you need to mount the +backup device. Check that the selected +target device/directory mounts OK. Mount may not work unless you are +root. Use the [root] button to become the root user. This works like 'sudo' - enter your own password. If this does not work on your system, restart ukopp with 'sudo' or 'su'.
    +
  8. +
  9. Select button [ edit job ].
  10. +
  11. Erase the default backup job shown (select and +delete, or use the [ clear ] button).
  12. +
  13. Select the button [ browse ] at the bottom.
  14. +
  15. Navigate through the directories and select the +directories and files to be copied.
    + +
  16. +
  17. Select the [ done ] button when finished selecting files.
  18. +
  19. Inspect the generated include and exclude records
    + +
  20. +
  21. Set a verify method: choose one of none, +incremental, full, or compare. Use compare until you are confident that +everything is working, then speed things up later by changing to +incremental.
  22. +
  23. Select button [ done ] when finished editing the +job.
  24. +
  25. If there are errors shown, select [ edit job ] and +fix them (remember that exclude records must follow relevant include +records - excludes are exceptions to prior includes).
  26. +
  27. Select menu: Report > get disk files. Inspect +the counts. Be sure the total byte count is within capacity. Look for +zero counts, indicating possible errors. Re-edit if needed.
  28. +
  29. Select button: [ run job ]. Backup and verify +should run automatically. Check that the error count is zero.
  30. +
  31. Save the job file if desired: menu: File > save +job.
  32. +
  33. Select button: [ quit ].
  34. +
  35. Next steps: play with the report and restore +functions.
  36. +
+
+Detailed Usage Instructions
+
+

+Toolbar +buttons
+
+
+root
+
This button restarts ukopp with root privileges if the password +(sudo) is OK. This is necessary to mount a backup device or copy files +that you do not own. If this does not work on you system, restart ukopp using 'sudo' or 'su'.
+
+target
+
The drop-down list displays all drives that are +visible to ukopp, with their mount points (if mounted) and +descriptions. Choose one +of these to set the target device and directory for a subsequent backup. +You may also type-in a directory directly. This must be a valid +directory for which you have write permission, and of course there +should be enough space for the backup files. If an unmounted device is +chosen, it will be mounted at a new directory under /media/sdxx and this +will be the target backup directory unless you change it. If the job +file has a target device and directory specified, ukopp will attempt to +mount this device and directory when the [mount] button is pressed.
+

+
+edit job
+
Shortcut to the backup job editor (same as menu File +> edit job)
.
+
+
run job
+
The current job is executed.
+
+
+pause / resume
+
The currently running job or menu function may be +paused and resumed. Use this to inspect output on the fly.
+
+
+
kill job
+The currently running function is killed.
+
+
+clear
+
The main window, where messages and reports are +written, is cleared.
+
+
+quit
+
Exit ukopp.
+
+
+
+

+File Menu
+
+
+open job

+Open a previously saved backup job file for re-use (edit, run). Default +location is the hidden directory /home/user/.ukopp (or /root/.ukopp).
+
+
+edit job
+
Opens an edit dialog for the current backup job (the last job file +opened). If no file has been opened, internal +default data will be used as a starting point. Editing jobs is +explained in a later section.

+
+list job

+List the current backup job data and diagnose any errors.
+
+
+save job
+
Save current backup job (with changes) to the same job file that +was last opened.
+
+save job as

+Save current backup job (with changes) to a selected job file. Default +is the same file that +was last opened, but you may select any file.

+
+run job

+The current backup job is executed. Backup and verify modes are taken +from the job.
+
+
+
+

+Backup +menu
+
+
backup
+The backup job is run without verify. You can then run whatever verify +you want.
+
+
+synchronize

+This is a bi-directional copy. Files present on one side only (disk or +backup location) are copied to the other side. Files that are present +on both sides will get the newest version copied to the other side. +"Newest" is based on the time of the last file update.
+
+
+Assume you normally use computer A, but you need to use B while +traveling. You can use a portable memory device (SD card, USB stick) to +keep the computer files synchronized.
+
    +
  1. A and B must have identical backup job files, +naming the same set of backup files.
  2. +
  3. Initial synchronization: backup A, move the memory +device to B, restore to B.
  4. +
  5. Work with B: create and modify some files.
  6. +
  7. Run synchronize on B, move the memory device to A, +run synchronize on A.
  8. +
  9. The modifications done on B are now carried over +to A.
  10. +
  11. You can update files on both A and B in parallel, +as long as you work on different files between synchronizations. +Synchronize A, then B, then A. Now both will have the same set of +files, and these will be the newest ones present on either A or B.
    +
  12. +
+
+
+Verify +menu
+
+
+full

+All backup files are read and checked for errors.
+
+
+incremental
+
New backup files are read and checked for errors. +"New" means any files written by an immediately preceding backup. Files +not +modified are not checked. The backup files are compared with the disk +files as in the "compare" verify described below. Non-matching files +are listed.

+
+compare

+All backup files having the same modification time and size as their +corresponding files on disk are read and compared with the disk. There +should be no differences. This verifies that ukopp is working +correctly. Other files are read and checked, but not compared to disk.
+
+
+
+

+Report +menu
+
+
+get disk files

+The backup job include and exclude records are listed, along with the +file and byte counts that are added or removed. Look for zero counts, +indicating a possible error. The disk directories are read at the time +this command is executed, and the list of files included in the backup +job is retained in memory. This data is used to determine which backup +files are now out of date and must be copied again from disk. The file +list is static and is not updated by disk activity. The list of "new" +files for a subsequent incremental verify is also reset.
+Note that this is a report to help verify that a backup job is complete +and correct. It is not necessary to run this report before running a +backup job.
+
+
diffs summary +
+Report the total number of files in each category:
 
+
+ + + + + + + + + + + + + + + + + + + +
   new files disk +files with no corresponding backup file
   modified +files 
+
disk and backup files exist +but are not identical +
   deleted files backup files with no +corresponding disk file
   unchanged disk and backup files exist +and are identical +(same modification time)
+ +Differences between the disk and the backup files may be caused by disk +updates (file additions, deletions, updates, or moves), or by changes +to the backup job file itself. +
+
+diffs by directory
+
The above counts are reported for each directory +having any differences between the disk and backup files.
+
+
+diffs by file

+List all different files, +grouped in the first three categories above. +If a file is present on both the disk and the backup location, and the +backup file is newer than the disk file, then the file is flagged in a +way that is easy to see. This can be normal if the backup media has +files from another computer that are used to refresh the (older) files +on this computer (see the synchronize function in the backup menu).

+
+version summary

+List backup files having old versions retained, with the range of +versions and file ages (days) available. File age is days since the +file was modified.
+
+
+expired versions

+List backup file versions that are expired and will be purged from the +backup medium or location with the next backup run.
+
+
+
list disk +files
+
All files in the backup file set are listed in +alphabetic sequence. Use this to check that the correct files are being +backed-up.
+
+
+
list backup +files
+All backup files are listed in alphabetic sequence. A summary of the +space used for prior file versions is also provided.
+
+
+
find files
+Enter a search pattern with optional wildcards (e.g. +
/home/dir*name/file*name).
+All matching disk files and backup files are listed.
+
+
+save screen

+The main window, where messages and reports are written, is saved as an +ordinary text file.
+
 
+
+
+Restore +menu
+
+
+setup restore job
+
+Specify the copy-from location (backup device or location), the copy-to +location, and the files to be restored.
+
+
The copy-from location is the +topmost directory of a tree of files to +be restored. +
+
    +example: +/home/joeblow/documents +  +# backup device mount point is omitted +
+The copy-to location is an existing disk directory where the tree of +files will be copied-to.
+
+
    +example 1: +/home/joeblow/documents +
+ +    example 2: /home/joeblow/documents/restored +
+ +In example 1, the restored files will go back to the same place they +were when backed-up.
+In example 2, they will go to a +new place.

+

+
Files to be +restored are specified the same way as in a backup job (see the section +below on editing backup jobs). +
+
+
If +you need to restore multiple trees of files, you can do this in +multiple runs, or you can simply begin the tree at a higher level and +use the file selection dialog to specify multiple sub-trees, with +included and excluded branches. +
+
+Note that files being restored from a backup media will replace the +corresponding files on disk, but other files, not being restored, are +not affected. Thus  include +/some/directory/*  in a restore job will replace all +files in 
/directory/  +that match the files being restored, but other files will be left in +place. If you do not want this, you must delete the files in  /directory/  before doing the restore.
+
+
list restore +files +
+After performing the file restore setup above, use this function to +list all matching files that will be restored, at the locations where +they will be restored. You should check this list carefully to be sure +you are restoring the correct files to the intended locations.
+
+
+
restore files +
+
When +you are satisfied with the restore job specification, use this menu to +start the restore. You will see a running log of the activity. The file +owners, permissions and modification times are also restored, even if +the backup +files are on a Microsoft FAT file system. +
+
+
+
+Format menu
+
+
+format device
+
+This is a convenient way to initialize a portable memory device such as +a USB stick or SD card for use with ukopp. You may select the vfat +(Microsoft) or ext2 (Linux) file system. You may choose from +all known devices, mounted or unmounted. You may +also choose a device label which will show under the device desktop +icon if automatic mounting is enabled. Before format begins, you are +shown which device will be formatted and given an opportunity to stop. Be sure you format the correct device, since all +data on this device will be lost!
+
+
+
Microsoft +vfat works somewhat faster than ext2 for USB devices, for reasons not +clear to me. The disadvantage is that some of the strange file names +typically found in Linux hidden directories are not vfat compatible and +will not copy (error messages are produced and the backup job +continues). Use ext2 if you must copy these files. Use vfat if you must +exchange files with a Windows computer. +
+
+
+

+Editing +backup jobs
+
+
+
+The [edit job] button starts the job edit dialog. See the screenshot +below.

+
+include and exclude records

+
You may edit the backup job +(the include and exclude records) directly +in the text window. You may also use the browse button to start a file +selection dialog. The dialog has the buttons [include] and [exclude]. +The "show hidden" checkbox turns the display of hidden +files on or off. Select one or more directories or files, using +left-mouse or +Ctrl+left-mouse, then press the [include] or [exclude] button. The +selected directories or files will be written into the text window as +include or exclude records. If you select a directory, the entry is +modified to add a wildcard at the next level, e.g. selecting  /aaa/bbb/ccc  and +then +pressing [include] generates  /aaa/bbb/ccc/*.
+

+The include and exclude records allow precise control of the backup +file set, allowing you to quickly converge on the desired results:

+
    +include +/aaa/bbb/*         +    # include file tree under /aaa/bbb/
+ +    exclude /aaa/bbb/ccc/*     +    # exception: exclude the /ccc/ subtree
+ +    include /aaa/bbb/ccc/xxx.yyy   # exception: +include file /ccc/xxx.yyy +
+
+
+Because of wildcards, newly added files within the scope of existing +include or exclude records are automatically comprehended. In the above +example, if a new file is added in /aaa/bbb/* then it will be +automatically included in the next backup job.
+
+
+
Here is and example of how to include all files in a direcory but +exclude all subdirectories:
+    include +/some/directory/*
+    exclude +/some/directory/*/*
+
+
old file retention policy
+
You +may optionally +enter a retention policy for old backup files. If +there is no retention, a modified disk file +replaces the corresponding backup file, and a deleted disk file causes +the corresponding backup file to be deleted. If you wish to retain +previous file versions, you must specify a retention time in days, and +a retention version count. The values in the GUI dialog (days and +versions) apply to each file that is selected when the [include] button +is pressed. Old +file versions +are deleted when they are older than BOTH retain rules: if retention is +D days and V versions, old file versions will be deleted only when +older +than D days, and not within the latest V versions. The latest version +is never deleted. You can +disable either of the limits by using zero (retain zero versions or +zero days). +
+
+Here are some examples that will hopefully make this clear:

+
    retain +10 days and 3 versions: delete +versions older +than 10 days unless one of the 3 newest versions
+ +    +retain 10 days and 0 versions: delete all versions older than 10 days
+ +    +retain 0 days and 8 versions: retain the 8 +newest versions regardless of age
+
+If a retention policy is given, the include record in the text box has +"(ddd,vvv)" appended to it, where ddd is the retention days and vvv +the version count.
+

+You +may alternate between editing the text window and using the +file-chooser dialog. When you are done, press [done] to accept. The +include / exclude records will be validated to the extent possible. Re-edit +to fix any problems. To change the sequence, cut and paste in the text +window. When you are done, use the report functions "get disk files" +and "list disk files" to verify that you have the correct +files!
+
+

[choose target]
+button
+This works like the [target] button on the toolbar, described above.
+

+ +verify method
+Choose one of the radio buttons to determine how ukopp verifies that +the backup copies are free of errors.

+
+ + + + + + + + + + + + + + + + + + + +
   none no automatic verify after +backup (use the verify menu +instead)
   +incremental  
+
verify and compare all +files copied by the backup job (new +and modified files)
   full read all backup files to +check data integrity
   compare full + compare all backup +files to corresponding disk +files (if present)
+
+
ukopp job +edit dialog
+
+
+Summary: +The [ edit job ] toolbar button pops up the middle window. This can be +edited directly: click anywhere in the text area and start writing. The +right window is the choose files dialog, which is started with the +[ browse ] +button in the middle window. Choose files using the right window, and +the middle window records your choices. You can navigate around the +directory +hierarchy and select any number of files or directories. The hidden +button toggles the display of hidden files. Click one of the include or +exclude buttons to get the selected files added to or removed from the +backup list. Selecting a directory is an implied selection of all its +contained files, thus the selection appears as /directory/* +in the list of selected files. To make an exception, go down one level, +select some files, and select the opposite include or exclude button. +You can refine the file selections manually if desired. It is sometimes +handy to use wildcards in the directories to make more general and +compact selection criteria.
+     example: 
exclude +/home/*thunderbird*/Trash
+This would exclude trashed e-mail even if the +overlying directories change (they do) and even for multiple users.
+
+
+You can add comments (or disable a record) by putting # in column 1.
+
+
+Annotated example of a backup job file
+
+This is an example of what one might do to backup all personal files. +In this example, we avoid backing up stuff that is not important +(browser cache) or stuff that can be automatically regenerated (gnome +thumbnails). Two old file versions should always be retained, and all +revisions up to 10 days. +All files copied during this run should be +read and verified. Files not copied (because they have not changed +since the last backup) are not verified. The backup target or location +is a USB disk that, when plugged-in, mounts at
/media/disk +(which can be +changed at run time if desired). +
+
    +include +/home/rosi/* (10,2)    +(include Rosi's personal files)
+ +    exclude */.thumbnails/*   +     +(omit gnome +thumbnail files)
+ +    exclude */firefox/*Cache/*     (omit +the +browser +cache files)
+ +    verify incremental          +   (verify files copied by each run)
+ +    target /dev/sdf1           +    (use removable USB disk sdf1
)
+
+The above backup job can be created using the following steps:
+ +    +("xxxxx" means +the random directory name that firefox generates for a user)
+
+
+

+Technical +Notes
+
+
+Symlink files:
These are treated like regular files. They are +copied if +included in the backup job. The target file of an included symlink is +NOT automatically included. A target file is included only if it's own +file name is included in the backup job. Symlinks are verified by +checking they are readable using function readlink(). If the target file system is vfat, symlinks will +not copy and will be reported as errors.
+
+Running ukopp as root: +Normally, ukopp will only copy files owned by the user. If files +belonging to root or other users are to +be copied, you must run ukopp as root. Use the [root] button to become +the root user (password required). See the note below about making a +launcher to start ukopp as root user.

+
+Command line arguments:
+
+
    +$ ukopp -job jobfile           # +load job file
+ +    $ ukopp jobfile         +       # load job file
+ +    $ ukopp -run jobfile   +        # +load job file and run it
+ +    $ ukopp -nogui -run jobfile    +# run as batch job without window
+
+
+If the jobfile name contains blanks, +quotes are required, e.g. 
$ ukopp -job "my ukopp job"
+
+The  -nogui  option can be used for a pure command line job +that has no window and will not ask for any user inputs. This can be +used for deferred execution (cron job). The backup location must be +available at the time the job runs.
+
+File type association:
I suggest using the +extension +.ukopp for job files and specifying ukopp as the "start with" program. +Then you can click on a job file and launch ukopp.
+
+
+Desktop launcher:
a desktop launcher may contain a command +like this:
+
+
     +gksu /usr/bin/ukopp -job myjob.ukopp
+"gksu" will ask for the sudo password and open the job +file as root.
+
+
+All backups are incremental:
A backup file is considered identical +to its +corresponding source file if their modification times are the +same. Such files are not backed up. If the modification times +differ by less than 2 seconds they are considered equal. 2 seconds is +the +time resolution for a Microsoft VFAT file system, usually +present by default on USB drives.
+For Linux usage it is best to reformat the USB drive to ext4. It +is theoretical possible that a backup file could have the same +modification time as the corresponding source file but still have +different content. This could happen if the disk file was updated, +copied by ukopp, and updated again within 2 seconds. For use on a +personal computer this is practically impossible. Another possiblility +is that the source or backup file was copied from another computer or +the modification time was manipulated.
+
+Restoring file owner and permissions: A detachable drive may +not support Linux file owner and permissions (e.g. Microsoft +FAT). The ukopp backup function copies a special file to the backup +location, with the data needed to restore file owner and permissions. +The ukopp restore and synchronize functions use this file.
+
+
+Special ukopp files:
A directory named ukopp-data is written to +the +backup location.
+It contains the following three files:

+
  +datetime            backup date-time
+  poopfile     +      + owner and permissions data for all files
+  jobfile     +        a +copy of the backup job file used
+
+These are ordinary text files +which you can view with an editor. +
+
+Special file types:
pipes, devices, and +sockets are not copied.
+
+
+Duplicate files:
If job file "include" records +overlap, resulting in duplicate files in the backup set, this is +reported and the backup does not proceed.
+
+
+
Finding disk drives: the Linux utility udevinfo is used to find block devices +with the characteristics "disk". The file /etc/mtab is used to find mount points for mounted devices.
+
+Removing detachable drives:
To remove a detachable +drive, right click on its desktop or file manager icon, select +"unmount" or "eject" or "safely remove", and wait for the "OK to +remove" message, or the LED on the drive to stop blinking. Pulling the +drive out without doing this can result in data corruption or total +loss.
+
+
File system +cache: The flush() function +is called after each file is copied to insure the file is completely +present on the backup medium and not waiting in the file system cache. +This is a little slower, but insures file system integrity if the job +fails to complete. The verify function uses direct I/O to read files +directly from the medium instead of using the memory cache. This is not +slower for large block I/O, and provides additional insurance that the +data on the medium is valid.
+

+NTFS:
+this Windows file system can be used as a backup source, but not as a +backup target. This is because the Linux driver for NTFS fails for a +file open() function call with the attribute O_DIRECT, meaning direct +I/O that bypasses memory caching.
+
+
Linux +error codes: Linux error codes can be +misleading. If an attempt is made to open a file that is already open +and is therefore locked, the error text is "no such file or directory". +I have noticed several such screwups in Linux. This will hopefully +improve +over time.
+
+
Funny +file names: Disk +drives formatted with the vfat file system (Microsoft FAT) will not +accept some Linux file names. Notably, files names containing " : " or +" ? " or ending with a blank will fail to copy, and this will be +reported in the backup job. Unless you need Microsoft compatibility, +format the drive with ext2, or avoid copying the weird file names you +can find among the hidden files in your home +directory. +
+
+Retention and version limits:
The retention upper limits are 9999 +days +and 9999 versions. Retained versions for a file could reach 9999 before +ukopp +stopped working. These limits are easy to increase, but performance +would start to deteriorate long before this. If you reach 1000 retained +versions it is time to start over (erase the backup medium).
+
+
+ + \ No newline at end of file diff -Nru ukopp-4.4/debian/changelog ukopp-4.7/debian/changelog --- ukopp-4.4/debian/changelog 2013-04-14 03:00:37.000000000 +0000 +++ ukopp-4.7/debian/changelog 2013-10-08 11:42:57.000000000 +0000 @@ -1,8 +1,22 @@ -ukopp (4.4-1ubuntu1) raring; urgency=low +ukopp (4.7-1) unstable; urgency=low - * Added missing pthread lib to link, fixing FTBFS. + * New upstream release. + * Update watch file + * Fix VCS urls + * debian/control: + + fix project homepage url + + bump standards-version + * Use debhelper 9 for hardering + * Refreshed old patch + * Added keywords to .desktop file + * new 03-fix-ftbfs-underlink.patch: Added missing pthread + lib to link, fixing FTBFS on armhf. + Thanks to Daniel T Chen (closes: #713696). + * new 04-fix-install-userguide.patch: fix missing data dir + on install. + * debian/dkopp.doc-base: fix paths. - -- Daniel T Chen Sat, 13 Apr 2013 23:00:21 -0400 + -- Leo Iannacone Wed, 10 Jul 2013 14:29:35 +0200 ukopp (4.4-1) unstable; urgency=low diff -Nru ukopp-4.4/debian/compat ukopp-4.7/debian/compat --- ukopp-4.4/debian/compat 2012-01-08 10:18:52.000000000 +0000 +++ ukopp-4.7/debian/compat 2013-10-08 11:42:57.000000000 +0000 @@ -1 +1 @@ -7 +9 diff -Nru ukopp-4.4/debian/control ukopp-4.7/debian/control --- ukopp-4.4/debian/control 2012-05-29 08:25:19.000000000 +0000 +++ ukopp-4.7/debian/control 2013-10-08 11:42:57.000000000 +0000 @@ -2,14 +2,14 @@ Section: admin Priority: optional Maintainer: Leo Iannacone -Build-Depends: debhelper (>= 7.0.50~), +Build-Depends: debhelper (>= 9), pkg-config, libgtk-3-dev, libglib2.0-dev -Standards-Version: 3.9.3 -Homepage: http://kornelix.squarespace.com/ukopp/ -Vcs-Git: git://git.debian.org/collab-maint/ukopp.git -Vcs-Browser: http://git.debian.org/?p=collab-maint/ukopp.git +Standards-Version: 3.9.4 +Homepage: http://www.kornelix.com/ukopp.html +Vcs-Git: git://anonscm.debian.org/collab-maint/ukopp.git +Vcs-Browser: http://anonscm.debian.org/gitweb/?p=collab-maint/ukopp.git Package: ukopp Architecture: any diff -Nru ukopp-4.4/debian/patches/01-desktop_file.patch ukopp-4.7/debian/patches/01-desktop_file.patch --- ukopp-4.4/debian/patches/01-desktop_file.patch 2012-01-08 10:18:52.000000000 +0000 +++ ukopp-4.7/debian/patches/01-desktop_file.patch 2013-10-08 11:42:57.000000000 +0000 @@ -2,12 +2,12 @@ Author: Alessio Treglia Forwarded: not-needed --- - desktop | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) + desktop | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) ---- ukopp.orig/desktop -+++ ukopp/desktop -@@ -2,7 +2,7 @@ +--- a/desktop ++++ b/desktop +@@ -2,8 +2,9 @@ Name=ukopp GenericName=Copy Files to Removable Media Comment=Full and incremental backup with media verification @@ -16,3 +16,5 @@ Type=Application Terminal=false Exec=/usr/bin/ukopp + Icon=/usr/share/ukopp/icons/ukopp.png ++Keywords=backup diff -Nru ukopp-4.4/debian/patches/02-makefile.patch ukopp-4.7/debian/patches/02-makefile.patch --- ukopp-4.4/debian/patches/02-makefile.patch 2013-04-14 02:58:45.000000000 +0000 +++ ukopp-4.7/debian/patches/02-makefile.patch 2013-10-08 11:42:57.000000000 +0000 @@ -7,11 +7,9 @@ Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) -Index: ukopp-4.4/Makefile -=================================================================== ---- ukopp-4.4.orig/Makefile 2012-04-23 03:29:52.000000000 -0400 -+++ ukopp-4.4/Makefile 2013-04-13 22:58:42.801856340 -0400 -@@ -15,10 +15,10 @@ +--- a/Makefile ++++ b/Makefile +@@ -15,7 +15,7 @@ ICONDIR = $(SHAREDIR)/icons DOCDIR = $(PREFIX)/share/doc/$(PROGRAM) MANDIR = $(PREFIX)/share/man/man1 @@ -19,12 +17,8 @@ +MENUFILE = $(PREFIX)/share/applications/$(PROGRAM).desktop CFLAGS = $(CXXFLAGS) -c `pkg-config --cflags gtk+-3.0` --LIBS = `pkg-config --libs gtk+-3.0` -+LIBS = `pkg-config --libs gtk+-3.0` -lpthread - - $(PROGRAM): $(PROGRAM).o zfuncs.o - $(CXX) $(LDFLAGS) $(PROGRAM).o zfuncs.o $(LIBS) -o $(PROGRAM) -@@ -45,7 +45,6 @@ + LIBS = `pkg-config --libs gtk+-3.0` +@@ -47,7 +47,6 @@ rm -f $(PROGRAM).1.gz # menu (desktop) file cp -f desktop $(DESTDIR)$(MENUFILE) diff -Nru ukopp-4.4/debian/patches/03-fix-ftbfs-underlink.patch ukopp-4.7/debian/patches/03-fix-ftbfs-underlink.patch --- ukopp-4.4/debian/patches/03-fix-ftbfs-underlink.patch 1970-01-01 00:00:00.000000000 +0000 +++ ukopp-4.7/debian/patches/03-fix-ftbfs-underlink.patch 2013-10-08 11:42:57.000000000 +0000 @@ -0,0 +1,21 @@ +Description: Added missing pthread lib to link, fixing FTBFS. +Patch cherry-picked from Ubuntu. +Author: Daniel T Chen +Bug-Debian: http://bugs.debian.org/713696 +Forwarded: yes + +--- + Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/Makefile ++++ b/Makefile +@@ -18,7 +18,7 @@ + MENUFILE = $(PREFIX)/share/applications/$(PROGRAM).desktop + + CFLAGS = $(CXXFLAGS) -c `pkg-config --cflags gtk+-3.0` +-LIBS = `pkg-config --libs gtk+-3.0` ++LIBS = `pkg-config --libs gtk+-3.0` -lpthread + + $(PROGRAM): $(PROGRAM).o zfuncs.o + $(CXX) $(LDFLAGS) $(PROGRAM).o zfuncs.o $(LIBS) -o $(PROGRAM) diff -Nru ukopp-4.4/debian/patches/04-fix-install-userguide.patch ukopp-4.7/debian/patches/04-fix-install-userguide.patch --- ukopp-4.4/debian/patches/04-fix-install-userguide.patch 1970-01-01 00:00:00.000000000 +0000 +++ ukopp-4.7/debian/patches/04-fix-install-userguide.patch 2013-10-08 11:42:57.000000000 +0000 @@ -0,0 +1,32 @@ +Description: Fix missing installation of DATADIR. +Author: Leo Iannacone +Forwarded: yes + +--- + Makefile | 3 +++ + 1 file changed, 3 insertions(+) + +--- a/Makefile ++++ b/Makefile +@@ -13,6 +13,7 @@ + BINDIR = $(PREFIX)/bin + SHAREDIR = $(PREFIX)/share/$(PROGRAM) + ICONDIR = $(SHAREDIR)/icons ++DATADIR = $(SHAREDIR)/data + DOCDIR = $(PREFIX)/share/doc/$(PROGRAM) + MANDIR = $(PREFIX)/share/man/man1 + MENUFILE = $(PREFIX)/share/applications/$(PROGRAM).desktop +@@ -33,11 +34,13 @@ + + install: $(PROGRAM) + mkdir -p $(DESTDIR)$(BINDIR) ++ mkdir -p $(DESTDIR)$(DATADIR) + mkdir -p $(DESTDIR)$(ICONDIR) + mkdir -p $(DESTDIR)$(DOCDIR) + mkdir -p $(DESTDIR)$(MANDIR) + mkdir -p $(DESTDIR)$(PREFIX)/share/applications + cp -f $(PROGRAM) $(DESTDIR)$(BINDIR) ++ cp -f -R data/* $(DESTDIR)$(DATADIR) + cp -f -R icons/* $(DESTDIR)$(ICONDIR) + cp -f -R doc/* $(DESTDIR)$(DOCDIR) + # man page diff -Nru ukopp-4.4/debian/patches/series ukopp-4.7/debian/patches/series --- ukopp-4.4/debian/patches/series 2012-05-29 08:25:19.000000000 +0000 +++ ukopp-4.7/debian/patches/series 2013-10-08 11:42:57.000000000 +0000 @@ -1,2 +1,4 @@ 01-desktop_file.patch 02-makefile.patch +03-fix-ftbfs-underlink.patch +04-fix-install-userguide.patch diff -Nru ukopp-4.4/debian/ukopp.doc-base ukopp-4.7/debian/ukopp.doc-base --- ukopp-4.4/debian/ukopp.doc-base 2012-01-08 10:18:52.000000000 +0000 +++ ukopp-4.7/debian/ukopp.doc-base 2013-10-08 11:42:57.000000000 +0000 @@ -4,5 +4,5 @@ Section: File Management Format: HTML -Index: /usr/share/doc/ukopp/userguide-en.html -Files: /usr/share/doc/ukopp/* +Index: /usr/share/ukopp/data/userguide-en.html +Files: /usr/share/ukopp/data/* diff -Nru ukopp-4.4/debian/watch ukopp-4.7/debian/watch --- ukopp-4.4/debian/watch 2012-01-08 10:18:52.000000000 +0000 +++ ukopp-4.7/debian/watch 2013-10-08 11:42:57.000000000 +0000 @@ -1,3 +1,4 @@ version=3 -http://kornelix.squarespace.com/downloads/ \ - /storage/downloads/ukopp-(.*)\.tar\.gz +http://www.kornelix.com/tarballs.html \ + /uploads/.*/ukopp-(.*)\.tar\.gz + diff -Nru ukopp-4.4/debian-control ukopp-4.7/debian-control --- ukopp-4.4/debian-control 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/debian-control 2013-02-27 22:11:30.000000000 +0000 @@ -1,16 +1,16 @@ Package: ukopp -Version: 4.4 +Version: 4.7 Architecture: amd64 -Section: utility -Installed-Size: 1112 +Section: utils +Installed-Size: 1180 Maintainer: Mike Cornelison -Description: Copy files to (removable) disk backup media. - Incremental operation: copies only files that are newer than existing - copies and is therefore quite fast. Specify directories or files to - include or exclude at any level, using a GUI. Save specifications in a - job file for repeated use. Report differences between source and backup - files at summary, directory, or file level. Optionally retain old file - versions in the backup for a specified time or version count. Optionally - verify backup files by comparing with source files. - +Priority: optional +Homepage: http://kornelix.com/ +Depends: libc6 +Description: Copy files to backup media. + Copies only new and modified files and is therefore quite fast. + Specify directories or files to include or exclude at any level. + Report disk/backup differences at summary, directory or file level. + Optionally retain prior file versions for a specified time or + version count. Optionally verify backup files. diff -Nru ukopp-4.4/doc/changelog ukopp-4.7/doc/changelog --- ukopp-4.4/doc/changelog 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/doc/changelog 2013-02-27 22:11:30.000000000 +0000 @@ -1,6 +1,16 @@ ukopp change log ================ +2013.03.01 v.4.7 ++ Clarifications in GUI and user guide. + +2012.11.01 v.4.6 ++ Replaced deprecated GTK functions with new versions. + +2012.07.20 v.4.5 ++ Toolbar text was added back after Gnome decided to remove it. ++ Minor improvement in error reporting. + 2012.04.23 v.4.4 + Instead of doing a system-wide sync command after all files are copied, fsync() is called at the end of each file. This is a little slower but diff -Nru ukopp-4.4/doc/copyright ukopp-4.7/doc/copyright --- ukopp-4.4/doc/copyright 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/doc/copyright 2013-02-27 22:11:30.000000000 +0000 @@ -2,10 +2,10 @@ Michael Cornelison Copyright: - Copyright 2007, 2008, 2009, 2010, 2011, 2012 Michael Cornelison + Copyright 2007, 2008, 2009, 2010, 2011, 2012, 2013 Michael Cornelison The source program code can be found here: - http://kornelix.squarespace.com/downloads + http://kornelix.com/tarballs License: You are free to distribute this software under the terms @@ -14,4 +14,7 @@ The complete text of the license can be found here: http://www.gnu.org/licenses/gpl-3.0.html +See "/usr/share/common-licenses/GPL-3". + + Binary files /tmp/rcpYkS0uOT/ukopp-4.4/doc/images/jobedit.jpeg and /tmp/1DX0xDIX7w/ukopp-4.7/doc/images/jobedit.jpeg differ diff -Nru ukopp-4.4/doc/userguide-en.html ukopp-4.7/doc/userguide-en.html --- ukopp-4.4/doc/userguide-en.html 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/doc/userguide-en.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,1414 +0,0 @@ - - - - - - - - - - -ukopp user guide   v.4.4
-
-concepts
-
-first tryout  (1-page primer for -those with RTFM problems)
-
-toolbar buttons
-
-file menu -
-
-backup menu
-verify menu
-
-report menu
-
-
restore menu -
-format menu
-
editing -backup jobs -
-technical notes
-
-
-
License and -Warranty -
-
Ukopp is a free program licensed under the GNU General Public -License V3 (Free Software Foundation). Ukopp is not warranted for any -purpose, but if you find a bug, I will try to fix it.
-
-
-Origin and Contact
-
-
Ukopp originates from the author's web site at: http://kornelix.squarespace.com/ukopp
-Other web sites may offer it for download. Modifications could have -been made.
-
-If you have questions, suggestions or a bug to report:
kornelix@yahoo.de

-
-
-Introduction

-
-Ukopp
-is a Linux -program for copying or backing-up disk files to a separate storage -device, e.g. a USB drive or SD memory card. Any disk directory may also -be -used as a backup location. You -can select files -to be copied -using a GUI. -You -can navigate through the file system and select files or -directories to include or exclude at any level in the directory -hierarchy. These choices can be saved in a job -file -to automate recurring backups. -If -new files appear in an included or excluded directory, they are -automatically taken into account. You need to revise the job file only -if you change the directories or make new exceptions within those -directories. -
-
-Ukopp
copies only -new and modified files: -files that have not changed since the last backup are bypassed in -microseconds. A typical daily backup of personal files can be done in a -few seconds. Ukopp can optionally retain -previous -versions -of backup files instead of overwriting them with newer versions. You -can optionally specify the retention time and / or the number of -versions to -retain for each group of included files. You can see these versions in -the -backup directories and recover them if needed.
-
-Ukopp has a -
synchronize -function, which is a simple method to keep files in two computers -synchronized using a USB stick or other portable memory. Ukopp copies -the newest version of a file from one device to the other. -
-
-Backups -can be verified three ways: full, incremental, and compare. A
full -verify -reads all the backup files and reports any files having read errors. -An incremental -verify -reads only those files that have been newly written by a preceding -backup job. This is very fast and provides a high level of security. -A compare -verify -reads all backup files and compares them with their corresponding -disk files. This is normally not necessary, but provides an effective -check that all hardware and software is working correctly. -
-
-You -can report all files in a backup job, or all files in a backup -directory. You can search for file names using wildcards. You -can report the differences between backup files and their -corresponding disk files: files that have been created, deleted, or -modified since the backup was made. These reports are available in -three levels of detail: a list of all changed files, total file and -byte counts per directory, and overall totals.
-
-
-For -disaster recovery or file transfer, ukopp has a
file -restore -capability. You can select and restore backup files to their original -directories or anywhere else. File owner, permissions, and modification -time are are also -restored, even if the backup device uses a Microsoft FAT file system -(normally the initial state for USB drives).
-
-
-
-
Concepts -
-
-The files in a backup job are specified with include and exclude -records. These have filespecs with optional wildcards placed almost -anywhere.
-
-Examples:
-
-
    -include /home/*         -        # add all user files
- -    -include /root/*         -        # add all root files
- -    -include /shared/*/documents/*   # add shared document files
- -    -exclude */mp3/*         -        # remove files in mp3 directories -
- -    -exclude */.Trash/*         -     # remove trash files -
-
-
-The -first include adds all files in all users' home directories -and sub-directories. The second include adds all files under the /root -directory. -The third include adds all files under the /shared top directory that -also have an intermediate directory named /documents. The two exclude -records remove all files within all /.Trash and /mp3 directories. -
-
-
The backup target is the -device or directory where the source files are copied-to.
-This is the directory of a mounted device or some other specified -directory.
-If the target is /media/sdb2 -(where the device sdb2 is mounted)
-and the source files are:  /home/<user>/documents/*, -
-the backup files will be:  /media/sdb2/home/<user>/documents/*. -
-
-GUI interface:
The above records are normally generated using a -file selection dialog.
-This is documented in a following section: editing backup jobs.
-
-
-File Selection Logic

-
    -loop: -
-        get next include/exclude record

-        if EOF, done

-        if include: add all matching -files to backup file set

-
    -    if exclude: remove all matching files from backup -file set
-    loop-end
-
-
-
Note that -excludes are effective only against prior includes. They have no -effect on following includes, which are processed afterwards. See the -section on editing backup jobs. -
-
-Restriction: include records must include at least the first directory -name (top-level) without wildcards (the GUI file-chooser does this -automatically).

-

-Retaining -multiple file versions: -if this option is elected, existing backup files that need updating are -renamed with a version number instead of being overwritten. If the -backup file "foo.bar" is updated, it is renamed to "foo.bar (1)", and -"foo.bar" becomes the newest backup. If it is updated again, "foo.bar" -is renamed to "foo.bar (2)", and so forth. Newer versions have higher -numbers, and the unversioned file is always the current or latest -version. The section on editing job files explains how to specify old -version retention policies. -
-
-Ukopp limitations
-
-    -max. 500,000 files in a backup job (compile time constant)
-    -max. file retention is 9999 days and 9999 versions
-    -must run as root user or use sudo to copy non-owned files
-    must run as -root user to mount backup device with [mount] button

-    -not useful for disk imaging (operating system backup)
-
-
-

-Ukopp -first tryout

-
-After -installing ukopp, please perform the following short exercise. This may -be -all you need at first. You can enhance your file security and -ultimately save time if you read this whole document.
The -exercise -will check that ukopp functions correctly -on your system and help you become familiar with ukopp usage. -
-
    -
  1. Choose a backup device or directory. If using a -pluggable device (e.g. USB drive), plug it in.
  2. -
  3. Start ukopp: click the desktop launcher or input a -terminal command: $ ukopp.
  4. -
  5. Select button [ target ]. The drop-down list shows -disk devices and their mount points if mounted. You can choose one of -these, or input your chosen backup directory.
  6. -
  7. Select -button [ mount ] if you need to mount the -backup device. Check that the selected -target device/directory mounts OK. Mount may not work unless you are -root. Use the [root] button to become the root user (password required).
    -
  8. -
  9. Select button [ edit job ].
  10. -
  11. Erase the default backup job shown (select and -delete, or use the [ clear ] button).
  12. -
  13. Select the button [ browse ] at the bottom.
  14. -
  15. Navigate through the directories and select the -directories and files to be copied.
    -
      -
    • double-click a directory to open it and enable -selection within that directory
    • -
    • select one or more directories / files, using -left-mouse or Ctrl+left-mouse
    • -
    • click [ include ] to include the selected -items in the backup job
    • -
    • click [ exclude ] to exclude the selected -items (previously included from a higher-level directory, and you now -wish to make an exception)
    • -
    • use the buttons at the top to go back up the -directory hierarchy
    • -
    • click [ hidden ] to enable / disable the display of -hidden files
    • -
    • for now, you can ignore the inputs for "Retain old -files" (see editing backup jobs, later)
      -
    • -
    -
  16. -
  17. Select the [ done ] button when finished selecting files.
  18. -
  19. Inspect the generated include and exclude records
    -
      -
    • these may be edited directly if desired (e.g. -erase mistakes or redundancies, change the -order, or even make additions or revisions using the text editor)
    • -
    • re-enter the file chooser dialog if wanted - -new choices will be appended
    • -
    • cycle between the editor and file chooser as much as -needed
    • -
    -
  20. -
  21. Set a verify method: choose one of none, -incremental, full, or compare. Use compare until you are confident that -everything is working, then speed things up later by changing to -incremental.
  22. -
  23. Select button [ done ] when finished editing the -job.
  24. -
  25. If there are errors shown, select [ edit job ] and -fix them (remember that exclude records must follow relevant include -records - excludes are exceptions to prior includes).
  26. -
  27. Select menu: Report > get disk files. Inspect -the counts. Be sure the total byte count is within capacity. Look for -zero counts, indicating possible errors. Re-edit if needed.
  28. -
  29. Select button: [ run job ]. Backup and verify -should run automatically. Check that the error count is zero.
  30. -
  31. Save the job file if desired: menu: File > save -job.
  32. -
  33. Select button: [ quit ].
  34. -
  35. Next steps: play with the report and restore -functions.
  36. -
-
-Detailed Usage Instructions
-
-

-Toolbar -buttons
-
-
-root
-
This button restarts ukopp with root privileges if the password -(sudo) is OK. This is necessary to mount a backup device or copy files -that you do not own.
-
-target
-
The drop-down list displays all drives that are -visible to ukopp, with their mount points (if mounted) and -descriptions. Choose one -of these to set the target device and location for a subsequent backup. -You may also type-in a directory directly. This must be a valid -directory for which you have write permission, and of course there -should be enough space for the backup files. If an unmounted device is -chosen, it will be mounted at a new directory under /media and this -will be the target backup directory unless you change it. If the job -file has a target device and directory specified, ukopp will attempt to -mount this device and directory when the [mount] button is pressed.
-

-
-edit job
-
Shortcut to the backup job editor (same as menu File -> edit job)
.
-
-
run job
-
The current job is executed.
-
-
-pause / resume
-
The currently running job or menu function may be -paused and resumed. Use this to inspect output on the fly.
-
-
-
kill job
-The currently running function is killed.
-
-
-clear
-
The main window, where messages and reports are -written, is cleared.
-
-
-quit
-
Exit ukopp.
-
-
-
-

-File Menu
-
-
-open job

-Open a previously saved backup job file for re-use (edit, run). Default -location is the hidden directory /home/user/.ukopp (or /root/.ukopp).
-
-
-edit job
-
Opens an edit dialog for the current backup job (the last job file -opened). If no file has been opened, internal -default data will be used as a starting point. Editing jobs is -explained in a later section.

-
-list job

-List the current backup job data and diagnose any errors.
-
-
-save job
-
Save current backup job (with changes) to the same job file that -was last opened.
-
-save job as

-Save current backup job (with changes) to a selected job file. Default -is the same file that -was last opened, but you may select any file.

-
-run job

-The current backup job is executed. Backup and verify modes are taken -from the job.
-
-
-
-

-Backup -menu
-
-
backup
-The backup job is run without verify. You can then run whatever verify -you want.
-
-
-synchronize

-This is a bi-directional copy. Files present on one side only (disk or -backup location) are copied to the other side. Files that are present -on both sides will get the newest version copied to the other side. -"Newest" is based on the time of the last file update.
-
-
-Assume you normally use computer A, but you need to use B while -traveling. You can use a portable memory device (SD card, USB stick) to -keep the computer files synchronized.
-
    -
  1. A and B must have identical backup job files, -naming the same set of backup files.
  2. -
  3. Initial synchronization: backup A, move the memory -device to B, restore to B.
  4. -
  5. Work with B: create and modify some files.
  6. -
  7. Run synchronize on B, move the memory device to A, -run synchronize on A.
  8. -
  9. The modifications done on B are now carried over -to A.
  10. -
  11. You can update files on both A and B in parallel, -as long as you work on different files between synchronizations. -Synchronize A, then B, then A. Now both will have the same set of -files, and these will be the newest ones present on either A or B.
    -
  12. -
-
-
-Verify -menu
-
-
-full

-All backup files are read and checked for errors.
-
-
-incremental
-
New backup files are read and checked for errors. -"New" means any files written by an immediately preceding backup. Files -not -modified are not checked. The backup files are compared with the disk -files as in the "compare" verify described below. Non-matching files -are listed.

-
-compare

-All backup files having the same modification time and size as their -corresponding files on disk are read and compared with the disk. There -should be no differences. This verifies that ukopp is working -correctly. Other files are read and checked, but not compared to disk.
-
-
-
-

-Report -menu
-
-
-get disk files

-The backup job include and exclude records are listed, along with the -file and byte counts that are added or removed. Look for zero counts, -indicating a possible error. The disk directories are read at the time -this command is executed, and the list of files included in the backup -job is retained in memory. This data is used to determine which backup -files are now out of date and must be copied again from disk. The file -list is static and is not updated by disk activity. The list of "new" -files for a subsequent incremental verify is also reset.
-Note that this is a report to help verify that a backup job is complete -and correct. It is not necessary to run this report before running a -backup job.
-
-
diffs summary -
-Report the total number of files in each category:
 
-
- - - - - - - - - - - - - - - - - - - -
   new files disk -files with no corresponding backup file
   modified -files 
-
disk and backup files exist -but are not identical -
   deleted files backup files with no -corresponding disk file
   unchanged disk and backup files exist -and are identical -(same modification time)
- -Differences between the disk and the backup files may be caused by disk -updates (file additions, deletions, updates, or moves), or by changes -to the backup job file itself. -
-
-diffs by directory
-
The above counts are reported for each directory -having any differences between the disk and backup files.
-
-
-diffs by file

-List all different files, -grouped in the first three categories above. -If a file is present on both the disk and the backup location, and the -backup file is newer than the disk file, then the file is flagged in a -way that is easy to see. This can be normal if the backup media has -files from another computer that are used to refresh the (older) files -on this computer (see the synchronize function in the backup menu).

-
-version summary

-List backup files having old versions retained, with the range of -versions and file ages (days) available. File age is days since the -file was modified.
-
-
-expired versions

-List backup file versions that are expired and will be purged from the -backup medium or location with the next backup run.
-
-
-
list disk -files
-
All files in the backup file set are listed in -alphabetic sequence. Use this to check that the correct files are being -backed-up.
-
-
-
list backup -files
-All backup files are listed in alphabetic sequence. A summary of the -space used for prior file versions is also provided.
-
-
-
find files
-Enter a search pattern with optional wildcards (e.g. -
/home/dir*name/file*name).
-All matching disk files and backup files are listed.
-
-
-save screen

-The main window, where messages and reports are written, is saved as an -ordinary text file.
-
 
-
-
-Restore -menu
-
-
-setup restore job
-
-Specify the copy-from location (in the backup files), the copy-to -location (disk), and the files to be restored.
-
-
The copy-from location is the -topmost directory of a tree of files to -be restored. -
-
    -example: -/home/joeblow/documents -  -# backup device mount point is omitted -
-The copy-to location is an existing disk directory where the tree of -files will be copied-to.
-
-
    -example 1: -/home/joeblow/documents -
- -    example 2: /home/joeblow/documents/restored -
- -In example 1, the restored files will go back to the same place they -were when backed-up.
-In example 2, they will go to a -new place.

-

-
Files to be -restored are specified the same way as in a backup job (see the section -below on editing backup jobs). -
-
-
If -you need to restore multiple trees of files, you can do this in -multiple runs, or you can simply begin the tree at a higher level and -use the file selection dialog to specify multiple sub-trees, with -included and excluded branches. -
-
-Note that files being restored from a backup media will replace the -corresponding files on disk, but other files, not being restored, are -not affected. Thus  include -/some/directory/*  in a restore job will replace all -files in 
/directory/  -that match the files being restored, but other files will be left in -place. If you do not want this, you must delete the files in  /directory/  before doing the restore.
-
-
list restore -files -
-After performing the file restore setup above, use this function to -list all matching files that will be restored, at the locations where -they will be restored. You should check this list carefully to be sure -you are restoring the correct files to the intended locations.
-
-
-
restore files -
-
When -you are satisfied with the restore job specification, use this menu to -start the restore. You will see a running log of the activity. The file -owners, permissions and modification times are also restored, even if -the backup -files are on a Microsoft FAT file system. -
-
-
-
-Format menu
-
-
-format device
-
-This is a convenient way to initialize a portable memory device such as -a USB stick or SD card for use with ukopp. You may select the vfat -(Microsoft) or ext2 (Linux) file system. You may choose from -all known devices, mounted or unmounted. You may -also choose a device label which will show under the device desktop -icon if automatic mounting is enabled. Before format begins, you are -shown which device will be formatted and given an opportunity to stop. Be sure you format the correct device, since all -data on this device will be lost!
-
-
-
Microsoft -vfat works somewhat faster than ext2 for USB devices, for reasons not -clear to me. The disadvantage is that some of the strange file names -typically found in Linux hidden directories are not vfat compatible and -will not copy (error messages are produced and the backup job -continues). Use ext2 if you must copy these files. Use vfat if you must -exchange files with a Windows computer. -
-
-
-

-Editing -backup jobs
-
-
-
-The [edit job] button starts the job edit dialog. See the screenshot -below.

-
-include and exclude records

-
You may edit the backup job -(the include and exclude records) directly -in the text window. You may also use the browse button to start a file -selection dialog. The dialog has the buttons [include] and [exclude]. -The "show hidden" checkbox turns the display of hidden -files on or off. Select one or more directories or files, using -left-mouse or -Ctrl+left-mouse, then press the [include] or [exclude] button. The -selected directories or files will be written into the text window as -include or exclude records. If you select a directory, the entry is -modified to add a wildcard at the next level, e.g. selecting  /aaa/bbb/ccc  and -then -pressing [include] generates  /aaa/bbb/ccc/*.
-

-The include and exclude records allow precise control of the backup -file set, allowing you to quickly converge on the desired results:

-
    -include -/aaa/bbb/*         -    # include file tree under /aaa/bbb/
- -    exclude /aaa/bbb/ccc/*     -    # exception: exclude the /ccc/ subtree
- -    include /aaa/bbb/ccc/xxx.yyy   # exception: -include file /ccc/xxx.yyy -
-
-
-Because of wildcards, newly added files within the scope of existing -include or exclude records are automatically comprehended. In the above -example, if a new file is added in /aaa/bbb/* then it will be -automatically included in the next backup job.
-
-
-
Here is and example of how to include all files in a direcory but -exclude all subdirectories:
-    include -/some/directory/*
-    exclude -/some/directory/*/*
-
-
old file retention policy
-
You -may optionally -enter a retention policy for old backup files. If -there is no retention, a modified disk file -replaces the corresponding backup file, and a deleted disk file causes -the corresponding backup file to be deleted. If you wish to retain -previous file versions, you must specify a retention time in days, and -a retention version count. The values in the GUI dialog (days and -versions) apply to each file that is selected when the [include] button -is pressed. Old -file versions -are deleted when they are older than BOTH retain rules: if retention is -D days and V versions, old file versions will be deleted only when -older -than D days, and not within the latest V versions. The latest version -is never deleted. You can -disable either of the limits by using zero (retain zero versions or -zero days). -
-
-Here are some examples that will hopefully make this clear:

-
    retain -10 days and 3 versions: delete -versions older -than 10 days but not the 3 -newest
- -    -retain 10 days and 0 versions: delete all versions older than 10 days
- -    -retain 0 days and 8 versions: delete all versions older than the 8 -newest versions
-
-If a retention policy is given, the include record in the text box has -"(ddd,vvv)" appended to it, where ddd is the retention days and vvv -the version count.
-

-You -may alternate between editing the text window and using the -file-chooser dialog. When you are done, press [done] to accept. The -include / exclude records will be validated to the extent possible. Re-edit -to fix any problems. To change the sequence, cut and paste in the text -window. When you are done, use the report functions "get disk files" -and "list disk files" to verify that you have the correct -files!
-
-

-choose target
-button
-This works like the [target] button on the toolbar, described above.
-

- -verify method
-Choose one of the radio buttons to determine how ukopp verifies that -the backup copies are free of errors.

-
- - - - - - - - - - - - - - - - - - - -
   none no automatic verify after -backup (use the verify menu -instead)
   -incremental  
-
verify and compare all -files copied by the backup job (new -and modified files)
   full read all backup files to -check data integrity
   compare full + compare all backup -files to corresponding disk -files (if present)
-
-
ukopp job -edit dialog
-
-
-Summary: -The [ edit job ] toolbar button pops up the middle window. This can be -edited directly: click anywhere in the text area and start writing. The -right window is the choose files dialog, which is started with the -[ browse ] -button in the middle window. Choose files using the right window, and -the middle window records your choices. You can navigate around the -directory -hierarchy and select any number of files or directories. The hidden -button toggles the display of hidden files. Click one of the include or -exclude buttons to get the selected files added to or removed from the -backup list. Selecting a directory is an implied selection of all its -contained files, thus the selection appears as /directory/* -in the list of selected files. To make an exception, go down one level, -select some files, and select the opposite include or exclude button. -You can refine the file selections manually if desired. It is sometimes -handy to use wildcards in the directories to make more general and -compact selection criteria.
-     example: 
exclude -/home/*thunderbird*/Trash
-This would exclude trashed e-mail even if the -overlying directories change (they do) and even for multiple users.
-
-
-You can add comments (or disable a record) by putting # in column 1.
-
-
-Annotated example of a backup job file
-
-This is an example of what one might do to backup all personal files. -In this example, we avoid backing up stuff that is not important -(browser cache) or stuff that can be automatically regenerated (gnome -thumbnails). Two old file versions should always be retained, and all -revisions up to 10 days. -All files copied during this run should be -read and verified. Files not copied (because they have not changed -since the last backup) are not verified. The backup target or location -is a USB disk that, when plugged-in, mounts at
/media/disk -(which can be -changed at run time if desired). -
-
    -include -/home/rosi/* (10,2)    -(include Rosi's personal files)
- -    exclude */.thumbnails/*   -     -(omit gnome -thumbnail files)
- -    exclude */firefox/*Cache/*     (omit -the -browser -cache files)
- -    verify incremental          -   (verify files copied by each run)
- -    target /dev/sdf1           -    (use removable USB disk sdf1
)
-
-The above backup job can be created using the following steps:
-
    -
  • click on button [ edit job ] to start the job edit -dialog
  • -
  • if there is a default job there, clear it out with -the [ clear ] button
  • -
  • use the [ browse ] button to start the "choose -files" dialog
  • -
  • navigate to /home, select rosi, enter the old -version retention policy
    -(10 days, 2 versions), and click the [ -include ] button
  • -
  • click the [ hidden ] -button to expose hidden files
  • -
  • now choose .thumbnails and click the [ exclude ] -button
  • -
  • navigate to .mozilla, then to firefox, then to the -directory xxxxx.default
  • -
  • select the folder Cache and click on the [ exclude -] button
  • -
  • click on [ done ] for the choose files dialog
  • -
  • the -following -text records are now in the edit dialog window:
    -
     include -/home/rosi/* -
    -  exclude -/home/rosi/.thumbnails/*
    -  exclude -/home/rosi/.mozilla/firefox/xxxxx.default/Cache/*
  • -
  • You can edit the records directly to shorten and -add wildcards, if you want.
    -(e.g. replace xxxxx with * to make the job ignore changes to this name )
  • -
  • Use the menu: file > save job as   to save the job -file for daily re-use
    -
  • -
-    -("xxxxx" means -the random directory name that firefox generates for a user)
-
-
-

-Technical -Notes
-
-
-Symlink files:
These are treated like regular files. They are -copied if -included in the backup job. The target file of an included symlink is -NOT automatically included. A target file is included only if it's own -file name is included in the backup job. Symlinks are verified by -checking they are readable using function readlink(). If the target file system is vfat, symlinks will -not copy and will be reported as errors.
-
-Running ukopp as root: -Normally, ukopp will only copy files owned by the user. If files -belonging to root or other users are to -be copied, you must run ukopp as root. Use the [root] button to become -the root user (password required). See the note below about making a -launcher to start ukopp as root user.

-
-Command line arguments:
-
-
    -$ ukopp -job jobfile           # -load job file
- -    $ ukopp jobfile         -       # load job file
- -    $ ukopp -run jobfile   -        # -load job file and run it
- -    $ ukopp -nogui -run jobfile    -# run as batch job without window
-
-
-If the jobfile name contains blanks, -quotes are required, e.g. 
$ ukopp -job "my ukopp job"
-
-The  -nogui  option can be used for a pure command line job -that has no window and will not ask for any user inputs. This can be -used for deferred execution (cron job). The backup location must be -available at the time the job runs.
-
-File type association:
I suggest using the -extension -.ukopp for job files and specifying ukopp as the "start with" program. -Then you can click on a job file and launch ukopp.
-
-
-Desktop launcher:
a desktop launcher may contain a command -like this:
-
-
     -gksu /usr/bin/ukopp -job myjob.ukopp
-"gksu" will ask for the sudo password and open the job -file as root.
-
-
-All backups are incremental:
A backup file is considered identical -to its -corresponding source file if their modification times are the -same. Such files are not backed up. If the modification times -differ by less than 2 seconds they are considered equal. 2 seconds is -the -time resolution for a Microsoft VFAT file system, usually -present by default on USB drives.
-For Linux usage it is best to reformat the USB drive to ext4. It -is theoretical possible that a backup file could have the same -modification time as the corresponding source file but still have -different content. This could happen if the disk file was updated, -copied by ukopp, and updated again within 2 seconds. For use on a -personal computer this is practically impossible. Another possiblility -is that the source or backup file was copied from another computer or -the modification time was manipulated.
-
-Restoring file owner and permissions: A detachable drive may -not support Linux file owner and permissions (e.g. Microsoft -FAT). The ukopp backup function copies a special file to the backup -location, with the data needed to restore file owner and permissions. -The ukopp restore and synchronize functions use this file.
-
-
-Special ukopp files:
A directory named ukopp-data is written to -the -backup location.
-It contains the following three files:

-
  -datetime            backup date-time
-  poopfile     -      - owner and permissions data for all files
-  jobfile     -        a -copy of the backup job file used
-
-These are ordinary text files -which you can view with an editor. -
-
-Special file types:
pipes, devices, and -sockets are not copied.
-
-
-Duplicate files:
If job file "include" records -overlap, resulting in duplicate files in the backup set, this is -reported and the backup does not proceed.
-
-
-
Finding disk drives: the Linux utility udevinfo is used to find block devices -with the characteristics "disk". The file /etc/mtab is used to find mount points for mounted devices.
-
-Removing detachable drives:
To remove a detachable -drive, right click on its desktop or file manager icon, select -"unmount" or "eject" or "safely remove", and wait for the "OK to -remove" message, or the LED on the drive to stop blinking. Pulling the -drive out without doing this can result in data corruption or total -loss.
-
-
File system -cache: The flush() function -is called after each file is copied to insure the file is completely -present on the backup medium and not waiting in the file system cache. -This is a little slower, but insures file system integrity if the job -fails to complete. The verify function uses direct I/O to read files -directly from the medium instead of using the memory cache. This is not -slower for large block I/O, and provides additional insurance that the -data on the medium is valid.
-

-NTFS:
-this Windows file system can be used as a backup source, but not as a -backup target. This is because the Linux driver for NTFS fails for a -file open() function call with the attribute O_DIRECT, meaning direct -I/O that bypasses memory caching.
-
-
Linux -error codes: Linux error codes can be -misleading. If an attempt is made to open a file that is already open -and is therefore locked, the error text is "no such file or directory". -I have noticed several such screwups in Linux. This will hopefully -improve -over time.
-
-
Funny -file names: Disk -drives formatted with the vfat file system (Microsoft FAT) will not -accept some Linux file names. Notably, files names containing " : " or -" ? " or ending with a blank will fail to copy, and this will be -reported in the backup job. Unless you need Microsoft compatibility, -format the drive with ext2, or avoid copying the weird file names you -can find among the hidden files in your home -directory. -
-
-Retention and version limits:
The retention upper limits are 9999 -days -and 9999 versions. Retained versions for a file could reach 9999 before -ukopp -stopped working. These limits are easy to increase, but performance -would start to deteriorate long before this. If you reach 1000 retained -versions it is time to start over (erase the backup medium).
-
-
- - diff -Nru ukopp-4.4/ukopp-4.4.cc ukopp-4.7/ukopp-4.4.cc --- ukopp-4.4/ukopp-4.4.cc 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/ukopp-4.4.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,4275 +0,0 @@ -/************************************************************************** - ukopp - disk to disk backup and restore program - - Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison - source URL: kornelix.squarespace.com - contact: kornelix2@gmail.com - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***************************************************************************/ - -#include -#include -#include "zfuncs.h" - -#define ukopp_title "ukopp v.4.4" // version -#define ukopp_license "GNU General Public License v.3" - -// parameters and limits - -#define normfont "monospace 8" -#define boldfont "monospace bold 8" -#define BIOCC 512*1024 // read and write I/O buffer size -#define maxnx 200 // max include/exclude in job file -#define maxfs 500000 // max disk files, 500K v.4.4 -#define MODTIMETOLR 2.0 // tolerance for "equal" mod times v.4.1 -#define nano 0.000000001 // nanosecond -#define mega (1024*1024) // computer million -#define VSEP1 " (" // file version appendage format: -#define VSEP2 ")" // /xxxx.../filename (nnn) -#define RSEP1 " (" // file retention appendage format: -#define RSEP2 ")" // /xxxx.../filename (nn,nn) - -// special control files in backup directory - -#define BD_UKOPPDIRK "/ukopp-data" // directory for special files -#define BD_POOPFILE "/ukopp-data/poopfile" // file owner & permissions file -#define BD_JOBFILE "/ukopp-data/jobfile" // backup job file -#define BD_DATETIME "/ukopp-data/datetime" // backup date-time file - -// GTK GUI widgets - -GtkWidget *mWin, *mVbox, *mScroll, *mLog; // main window -GtkWidget *fc_widget; // file-chooser dialog widget -GtkWidget *editwidget; // edit box in file selection dialogs -GdkCursor *watchcursor; -GdkWindow *mLogwin; -PangoFontDescription *monofont; // fixed-width font - -// file scope variables - -int main_argc; // command line args -char **main_argv; - -int killFlag; // tell function to quit -int pauseFlag; // tell function to pause/resume -int menuLock; // menu lock flag -int Fgui; // flag, GUI mode or not -int clrun; // flag, command line 'run' command - -char TFbakfiles[100]; // /home/user/.ukopp/xxx temp. files -char TFjobfile[100], TFpoopfile[100]; -char TFdatetime[100], TFformatscript[100]; - -// disk devices and mount points - -char diskdev[100][40]; // /dev/xxx -char diskdesc[100][60]; // device description -char diskmp[100][60]; // mount point, /media/xxxx -int Ndisk, maxdisk = 99; // max. disks / partitions - -int devMounted = 0; // backup device mounted status -int ukoppMounted = 0; // device was mounted by me -int ukoppMpoint = 0; // mount point was made by me -char mountdev[40]; // current mount data -char mountdirk[200]; - -// backup job data - -char BJfilespec[maxfcc]; // backup job file -int BJnnx; // filespec count, 0...maxnx -int BJrtype[maxnx]; // 1/2/3 = comment/include/exclude -char *BJfspec[maxnx]; // filespec (wild) -int BJretND[maxnx]; // retention days -int BJretNV[maxnx]; // retention versions -int BJfiles[maxnx]; // count of matching disk files -double BJbytes[maxnx]; // matching files byte count -int BJvmode; // 0/1/2/3 = none/incr/full/comp -char BJdev[40] = ""; // backup device (maybe) -char BJdirk[200] = ""; // backup target directory -int BJdcc; // target directory cc -int BJvalid = 0; // backup job valid flag -int BJedited = 0; // job edited and not saved - -const char *vertype[4] = { "none","incremental","full","compare" }; // verify types - -// disk files specified in backup job - -struct dfrec { // disk file record - char *file; // file: /directory.../filename - double size; // byte count - double mtime; // mod time - int err; // fstat() status - int jindx; // index to job data BJfspec[] etc. - int bindx; // index to backup files Brec[] - int finc; // included in curr. backup - char disp; // status: new mod unch -}; - -int Dnf; // actual file count < maxfs -double Dbytes; // disk files, total bytes -dfrec Drec[maxfs]; // disk file data records - -// backup files (copies at backup location) - -struct bfrec { // backup file record - char *file; // file: /directory.../filename - double size; // byte count - double mtime; // mod time - int err; // file fstat() status - int retND; // retention days - int retNV; // retention versions - int lover, hiver; // range of previous versions - int nexpv; // no. expired versions - int finc; // included in curr. backup - char disp; // file status: del mod unch -}; - -int Bnf; // actual file count < maxfs -double Bbytes; // backup files, total bytes -bfrec Brec[maxfs]; // backup file data records - // backup file statistics: -int Cfiles; // curr. version file count -double Cbytes; // and total bytes -int Vfiles; // prior version file count -double Vbytes; // and total bytes -int Pfiles; // expired prior versions -double Pbytes; // and total bytes -int fverrs, fcerrs; // file verify and compare errors - -// disk::backup comparison data - -int nnew, ndel, nmod, nunc; // new, del, mod, unch file counts -int Mfiles; // new + mod + del file count -double Mbytes; // new + mod files, total bytes - -// restore job data - -char RJfrom[300]; // restore copy-from: /directory/.../ -char RJto[300]; // restore copy-to: /directory/.../ -int RJnnx; // filespec count, 0...maxnx -int RJrtype[maxnx]; // record type: include/exclude -char *RJfspec[maxnx]; // filespec of include/exclude -int RJvalid; // restore job valid flag - -// restore file data - -struct rfrec { // restore file record - char *file; // restore filespec: /directory.../file - int finc; // flag, file restore was done -}; - -rfrec Rrec[maxfs]; // restore file data records -int Rnf; // actual file count < maxfs - -// ukopp functions - -int initfunc(void *data); // GTK init function -void buttonfunc(GtkWidget *, cchar *menu); // process toolbar button event -void menufunc(GtkWidget *, cchar *menu); // process menu select event - -int getroot(cchar *); // get root privileges -int quit_ukopp(cchar *); // exit application -int clearScreen(cchar *); // clear logging window -int signalFunc(cchar *); // kill/pause/resume curr. function -int checkKillPause(); // test flags: killFlag and pauseFlag - -int BDpoop(); // get all devices and mount points -int chooseTarget(cchar *); // choose device and mount point - -int BJfileOpen(cchar *); // job file open dialog -int BJfileSave(cchar *); // job file save dialog -int BJload(cchar *fspec); // backup job data <<< file -int BJstore(cchar *fspec); // backup job data >>> file -int BJlist(cchar *); // backup job >>> log window -int BJedit(cchar *); // backup job edit dialog - -cchar * parseNXrec(cchar *, int &, char *&, int &, int &); // parse include/exclude record -cchar * parseTarget(cchar *); // parse target record -cchar * parseVerify(cchar *); // parse verify record - -int Backup(cchar *); // backup function -int Synch(cchar *); // synchronize function -int Verify(cchar *); // verify functions -int Report(cchar *); // report functions - -int RJedit(cchar *); // restore job edit dialog -int RJlist(cchar *); // list backup files to be restored -int Restore(cchar *); // file restore function - -int Format(cchar *); // format disk function -int helpFunc(cchar *); // help function - -int mount(cchar *); // mount target device -int unmount(cchar *); // unmount target device -int saveScreen(cchar *); // save logging window to file -int writeDT(); // write date-time to temp file -int synch_poop(const char *mode); // synch owner and permissions data - -int dGetFiles(); // generate backup files from job data -int bGetFiles(); // get backup file list -int rGetFiles(); // generate file list from restore job -int setFileDisps(); // set file disps: new del mod unch -int SortFileList(char *recs, int RL, int NR, char sort); // sort file list in memory -int filecomp(cchar *file1, cchar *file2); // compare files, directories first - -int BJreset(); // reset backup job file data -int RJreset(); // reset restore job data -int dFilesReset(); // reset disk file data and free memory -int bFilesReset(); // reset backup file data, free memory -int rFilesReset(); // reset restore file data, free memory - -cchar * copyFile(cchar *file1, cchar *file2, int mpf); // copy backup file << >> disk file -cchar * checkFile(cchar *file, int compf, double &bcc); // validate file and return length -cchar * setnextVersion(bfrec &rec); // backup file: assign next version -cchar * purgeVersions(bfrec &rec, int fkeep); // backup file: delete expired vers. -cchar * deleteFile(cchar *file); // delete backup file -int setFileVersion(char *file, int vers); // (re)set filespec version in memory -int do_shell(cchar *pname, cchar *command); // do shell command and echo outputs - -// ukopp menu table - -struct menuent { - char menu1[20], menu2[40]; // top-menu, sub-menu - int lock; // lock funcs: no run parallel - int (*mfunc)(cchar *); // processing function -}; - -#define nmenu 40 -struct menuent menus[nmenu] = { -// top-menu sub-menu lock menu-function -{ "button", "root", 1, getroot }, -{ "button", "edit job", 1, BJedit }, -{ "button", "target", 1, chooseTarget }, -{ "button", "clear", 0, clearScreen }, -{ "button", "run job", 1, Backup }, -{ "button", "mount", 1, mount }, -{ "button", "unmount", 1, unmount }, -{ "button", "pause", 0, signalFunc }, -{ "button", "resume", 0, signalFunc }, -{ "button", "kill job", 0, signalFunc }, -{ "button", "quit", 0, quit_ukopp }, -{ "File", "open job", 1, BJfileOpen }, -{ "File", "edit job", 1, BJedit }, -{ "File", "list job", 0, BJlist }, -{ "File", "save job", 0, BJfileSave }, -{ "File", "save job as", 0, BJfileSave }, -{ "File", "run job", 1, Backup }, -{ "File", "quit", 0, quit_ukopp }, -{ "Backup", "backup only", 1, Backup }, -{ "Backup", "synchronize", 1, Synch }, -{ "Verify", "incremental", 1, Verify }, -{ "Verify", "full", 1, Verify }, -{ "Verify", "compare", 1, Verify }, -{ "Report", "get disk files", 1, Report }, -{ "Report", "diffs summary", 1, Report }, -{ "Report", "diffs by directory", 1, Report }, -{ "Report", "diffs by file status", 1, Report }, -{ "Report", "diffs by file", 1, Report }, -{ "Report", "file versions", 1, Report }, -{ "Report", "expired versions", 1, Report }, -{ "Report", "list disk files", 1, Report }, -{ "Report", "list backup files", 1, Report }, -{ "Report", "find files", 1, Report }, -{ "Report", "save screen", 0, saveScreen }, -{ "Restore", "setup restore job", 1, RJedit }, -{ "Restore", "list restore files", 1, RJlist }, -{ "Restore", "restore files", 1, Restore }, -{ "Format", "format device", 1, Format }, -{ "Help", "about", 0, helpFunc }, -{ "Help", "contents", 0, helpFunc } }; - - -// ukopp main program - -int main(int argc, char *argv[]) -{ - GtkWidget *mbar, *tbar; - GtkWidget *mFile, *mBackup, *mVerify, *mReport, *mRestore; - GtkWidget *mFormat, *mHelp; - int ii; - - zinitapp("ukopp",null); // setup app directories - - clrun = 0; // no command line run command - *BJfilespec = 0; // no backup job file - Fgui = 1; // assume GUI mode - - main_argc = argc; // save command line arguments - main_argv = argv; - - for (ii = 1; ii < argc; ii++) // process command line - { - if (strEqu(argv[ii],"-nogui")) Fgui = 0; // command line mode, no GUI - else if (strEqu(argv[ii],"-job") && argc > ii+1) // -job jobfile (load only) - strcpy(BJfilespec,argv[++ii]); - else if (strEqu(argv[ii],"-run") && argc > ii+1) // -run jobfile (load and run) - { strcpy(BJfilespec,argv[++ii]); clrun++; } - else strcpy(BJfilespec,argv[ii]); // assume a job file and load it - } - - if (! Fgui) { // no GUI v.3.6 - mLog = mWin = 0; // outputs go to STDOUT - initfunc(0); // run job - if (devMounted && ukoppMounted) unmount(0); - return 0; // exit - } - - gtk_init(&argc, &argv); // GTK command line options - - mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window - gtk_window_set_title(GTK_WINDOW(mWin),ukopp_title); - gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER); - gtk_window_set_default_size(GTK_WINDOW(mWin),800,500); - - mVbox = gtk_vbox_new(0,0); // vertical packing box - gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window - - mScroll = gtk_scrolled_window_new(0,0); // scrolled window - gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox - - mLog = gtk_text_view_new(); // text edit window - gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window - - monofont = pango_font_description_from_string(normfont); // set fixed pitch font - gtk_widget_modify_font(mLog,monofont); - - mbar = create_menubar(mVbox); // create menu bar - - mFile = add_menubar_item(mbar,"File",menufunc); // add menu bar items - add_submenu_item(mFile,"open job",menufunc); - add_submenu_item(mFile,"edit job",menufunc); - add_submenu_item(mFile,"list job",menufunc); - add_submenu_item(mFile,"save job",menufunc); - add_submenu_item(mFile,"save job as",menufunc); - add_submenu_item(mFile,"run job",menufunc); - add_submenu_item(mFile,"quit",menufunc); - mBackup = add_menubar_item(mbar,"Backup",menufunc); - add_submenu_item(mBackup,"backup only",menufunc); - add_submenu_item(mBackup,"synchronize",menufunc); - mVerify = add_menubar_item(mbar,"Verify",menufunc); - add_submenu_item(mVerify,"incremental",menufunc); - add_submenu_item(mVerify,"full",menufunc); - add_submenu_item(mVerify,"compare",menufunc); - mReport = add_menubar_item(mbar,"Report",menufunc); - add_submenu_item(mReport,"get disk files",menufunc); - add_submenu_item(mReport,"diffs summary",menufunc); - add_submenu_item(mReport,"diffs by directory",menufunc); - add_submenu_item(mReport,"diffs by file status",menufunc); - add_submenu_item(mReport,"diffs by file",menufunc); - add_submenu_item(mReport,"file versions",menufunc); - add_submenu_item(mReport,"expired versions",menufunc); - add_submenu_item(mReport,"list disk files",menufunc); - add_submenu_item(mReport,"list backup files",menufunc); - add_submenu_item(mReport,"find files",menufunc); - add_submenu_item(mReport,"save screen",menufunc); - mRestore = add_menubar_item(mbar,"Restore",menufunc); - add_submenu_item(mRestore,"setup restore job",menufunc); - add_submenu_item(mRestore,"list restore files",menufunc); - add_submenu_item(mRestore,"restore files",menufunc); - mFormat = add_menubar_item(mbar,"Format",menufunc); - add_submenu_item(mFormat,"format device",menufunc); - mHelp = add_menubar_item(mbar,"Help",menufunc); - add_submenu_item(mHelp,"about",menufunc); - add_submenu_item(mHelp,"contents",menufunc); - - tbar = create_toolbar(mVbox); // create toolbar and buttons - - if (getuid() > 0) - add_toolbar_button(tbar,"root","get root privileges","gtk-dialog-authentication",buttonfunc); - else - add_toolbar_button(tbar,"root","you have root privileges","root.png",buttonfunc); - - add_toolbar_button(tbar,"target","select backup device or directory","target.png",buttonfunc); - add_toolbar_button(tbar,"mount","mount target device","mount.png",buttonfunc); - add_toolbar_button(tbar,"unmount","unmount target device","unmount.png",buttonfunc); - add_toolbar_button(tbar,"edit job","edit backup job","edit.png",buttonfunc); - add_toolbar_button(tbar,"run job","run backup job","run.png",buttonfunc); - add_toolbar_button(tbar,"pause","pause running job","gtk-media-pause",buttonfunc); - add_toolbar_button(tbar,"resume","resume running job","gtk-media-play",buttonfunc); - add_toolbar_button(tbar,"kill job","kill running job","gtk-stop",buttonfunc); - add_toolbar_button(tbar,"clear","clear screen","gtk-clear",buttonfunc); - add_toolbar_button(tbar,"quit","quit ukopp","gtk-quit",buttonfunc); - - gtk_widget_show_all(mWin); // show all widgets - - G_SIGNAL(mWin,"destroy",quit_ukopp,0); // connect window destroy event - G_SIGNAL(mWin,"delete_event",quit_ukopp,0); - - watchcursor = gdk_cursor_new(GDK_WATCH); // v.4.1 - mLogwin = gtk_text_view_get_window(GTK_TEXT_VIEW(mLog), // GDK window for mLog - GTK_TEXT_WINDOW_TEXT); - - g_timeout_add(0,initfunc,0); // setup initial call from gtk_main() - gtk_main(); // process window events - return 0; -} - - -// initial function called from gtk_main() at startup - -int initfunc(void *data) -{ - int ii; - const char *home, *appdirk; - time_t datetime; - - datetime = time(0); - wprintf(mLog,"ukopp %s \n",ctime(&datetime)); // v.4.1 - - if (getuid() == 0) // v.4.1 - wprintx(mLog,0,"you have root privileges \n",boldfont); - else { - menufunc(null,"Help"); // show version and license - menufunc(null,"about"); - } - - appdirk = get_zuserdir(); - sprintf(TFbakfiles,"%s/bakfiles",appdirk); // make temp file names - sprintf(TFpoopfile,"%s/poopfile",appdirk); - sprintf(TFjobfile,"%s/jobfile",appdirk); - sprintf(TFdatetime,"%s/datetime",appdirk); - sprintf(TFformatscript,"%s/formatscript.sh",appdirk); - - menuLock = killFlag = pauseFlag = 0; // initialize controls - - BJnnx = 4; // default backup job data - for (ii = 0; ii < BJnnx; ii++) - BJfspec[ii] = zmalloc(60); - home = getenv("HOME"); // get "/home/username" - if (! home) home = "/root"; - - strcpy(BJfspec[0],"# default backup job"); // comment - sprintf(BJfspec[1],"%s/*",home); // /home/username/* - sprintf(BJfspec[2],"%s/*/Trash/*",home); // /home/username/*/Trash/* - sprintf(BJfspec[3],"%s/.thumbnails/*",home); // /home/username/.thumbnails/* - - BJrtype[0] = 1; // comment - BJrtype[1] = 2; // include - BJrtype[2] = 3; // exclude - BJrtype[3] = 3; // exclude - - BJretND[1] = BJretNV[1] = 0; // no retention specs v.3.5 - BJvmode = 0; // no verify - BJvalid = 0; // not validated - - strcpy(BJdev,""); // backup target device (maybe) - strcpy(BJdirk,"/unknown"); // backup target directory, cc - BJdcc = strlen(BJdirk); - - strcpy(RJfrom,"/home/"); // file restore copy-from location - strcpy(RJto,"/home/"); // file restore copy-to location - RJnnx = 0; // no. restore include/exclude recs - RJvalid = 0; // not validated - - BDpoop(); // find devices and mount points - - if (*BJfilespec) BJload(BJfilespec); // load command line job file - else snprintf(BJfilespec,maxfcc,"%s/ukopp.job",get_zuserdir()); // or set default job file - - if (clrun) { - menufunc(null,"File"); // run command line job file - menufunc(null,"run job"); - } - - return 0; -} - - -// process toolbar button events (simulate menu selection) - -void buttonfunc(GtkWidget *, cchar *button) -{ - char button2[20], *pp; - - strncpy0(button2,button,19); - pp = strchr(button2,'\n'); // replace \n with blank - if (pp) *pp = ' '; - - menufunc(0,"button"); - menufunc(0,button2); - return; -} - - -// process menu selection event - -void menufunc(GtkWidget *, cchar *menu) -{ - int ii; - static char menu1[20] = "", menu2[40] = ""; - char command[100]; - - for (ii = 0; ii < nmenu; ii++) - if (strEqu(menu,menus[ii].menu1)) break; // mark top-menu selection - if (ii < nmenu) { strcpy(menu1,menu); return; } - - for (ii = 0; ii < nmenu; ii++) - if (strEqu(menu1,menus[ii].menu1) && - strEqu(menu,menus[ii].menu2)) break; // mark sub-menu selection - - if (ii < nmenu) strcpy(menu2,menu); - else { // no match to menus - wprintf(mLog," *** bad command: %s \n",menu); - return; - } - - if (menuLock && menus[ii].lock) { // no lock funcs can run parallel - zmessageACK(mWin,"a blocking function is active"); - return; - } - - if (! menuLock) - killFlag = pauseFlag = 0; // reset controls - - snprintf(command,99,"\n""command: %s > %s \n",menu1,menu2); - wprintx(mLog,0,command,boldfont); - - if (menus[ii].lock) ++menuLock; - menus[ii].mfunc(menu2); // call menu function - if (menus[ii].lock) --menuLock; - - return; -} - - -// get root privileges if password is OK - -int getroot(cchar * menu) // v.3.8 -{ - if (getuid() == 0) // v.4.1 - wprintx(mLog,0,"\nyou have root privileges \n",boldfont); - else - beroot(main_argc-1,main_argv+1); // does not return - return 0; -} - - -// quit ukopp - -int quit_ukopp(cchar *menu) -{ - int yn; - char logfile[200]; - - if (devMounted && ukoppMounted) unmount(0); // v.3.5.2 - - if (BJedited && Fgui) { // v.4.0 - yn = zmessageYN(mWin,"job file modified, QUIT anyway?"); - if (! yn) return 1; - BJedited = 0; - } - - if (mLog) { - sprintf(logfile,"%s/ukopp.log2",get_zuserdir()); // dump window to log file - wfiledump(mLog,logfile); // v.3.9 - } - - gtk_main_quit(); // tell gtk_main() to quit - return 0; -} - - -// clear logging window - -int clearScreen(cchar *menu) -{ - wclear(mLog); - return 0; -} - - -// kill/pause/resume current function - called from menu function - -int signalFunc(cchar *menu) -{ - if (strEqu(menu,"kill job")) - { - if (! menuLock) { - wprintf(mLog,"\n""ready \n"); - return 0; - } - - if (killFlag) { - wprintf(mLog," *** waiting for function to quit \n"); - return 0; - } - - wprintf(mLog," *** KILL current function \n"); - pauseFlag = 0; - killFlag = 1; - return 0; - } - - if (strEqu(menu,"pause")) { - pauseFlag = 1; - return 0; - } - - if (strEqu(menu,"resume")) { - pauseFlag = 0; - return 0; - } - - else zappcrash("signalFunc: %s",menu); - return 0; -} - - -// check kill and pause flags -// called periodically from long-running functions - -int checkKillPause() -{ - while (pauseFlag) // idle loop while paused - { - zsleep(0.1); - zmainloop(); // process menus - } - - zmainloop(); // keep menus working v.4.0 - - if (! killFlag) return 0; // keep running - return 1; // die now and reset killFlag -} - - -// find all disk devices and mount points via Linux utilities - -int BDpoop() // v.3.3 new udevinfo format -{ - int ii, jj, contx = 0, pii, pjj, err; - int diskf, filsysf, usbf, Nth, Nmounted; - char *buff, diskdev1[40], diskdesc1[60], work[100]; - cchar *pp1, *pp2; - - Ndisk = diskf = filsysf = usbf = 0; - - err = system("udevadm --version >/dev/null 2>&1"); // keep up with dynamic Linux v.3.4 - if (! err) strcpy(work,"udevadm info -e"); // new Linux command - else strcpy(work,"udevinfo -e"); // old Linux command - - while ((buff = command_output(contx,work))) - { - if (strnEqu(buff,"P: ",3)) { // start new device - if (diskf && filsysf) { // if last device = formatted disk - strncpy0(diskdev[Ndisk],diskdev1,39); // save /dev/devid - strncpy0(diskdesc[Ndisk],diskdesc1,59); // save description - if (usbf) strcat(diskdesc[Ndisk]," (USB)"); // note if USB device - strcpy(diskmp[Ndisk],"(not mounted)"); // mount point TBD - Ndisk++; - if (Ndisk == maxdisk) { - wprintf(mLog," *** exceeded %d devices \n",maxdisk); - break; - } - } - - diskf = filsysf = usbf = 0; // clear new device flags - } - - if (strnEqu(buff,"N: ",3)) { - strcpy(diskdev1,"/dev/"); - strncat(diskdev1,buff+3,14); // save /dev/devid - } - - if (strnEqu(buff,"E: ",3)) { - pp1 = strstr(buff,"ID_TYPE=disk"); - if (pp1) diskf = 1; // device is a disk - pp1 = strstr(buff,"ID_FS_TYPE="); - if (pp1) filsysf = 1; // device has a file system - pp1 = strstr(buff,"ID_BUS=usb"); - if (pp1) usbf = 1; // device is a USB device - pp1 = strstr(buff,"ID_MODEL="); - if (pp1) strncpy0(diskdesc1,pp1+9,59); // save description - } - } - - if (! Ndisk) { - wprintf(mLog," no devices found \n"); - return 0; - } - - contx = Nmounted = 0; - - while ((buff = command_output(contx,"cat /proc/mounts"))) // get mounted disk info v.3.2 - { - if (strnNeq(buff,"/dev/",5)) continue; // not a /dev/xxx record - - Nth = 1; - pp1 = strField(buff,' ',Nth++); // parse /dev/xxx /media/xxx - pp2 = strField(buff,' ',Nth++); - - for (ii = 0; ii < Ndisk; ii++) // look for matching device - { - if (strNeq(pp1,diskdev[ii])) continue; - strncpy0(diskmp[ii],pp2,59); // copy its mount point - strTrim(diskmp[ii]); - Nmounted++; - break; - } - } - - #define swap(name,ii,jj) { \ - strcpy(work,name[ii]); \ - strcpy(name[ii],name[jj]); \ - strcpy(name[jj],work); } - - for (ii = 0; ii < Ndisk; ii++) // sort USB and mounted devices - for (jj = ii + 1; jj < Ndisk; jj++) // to the top of the list - { - pii = pjj = 0; - if (strstr(diskdesc[ii],"(USB)")) pii += 2; - if (! strEqu(diskmp[ii],"(not mounted)")) pii += 1; - if (strstr(diskdesc[jj],"(USB)")) pjj += 2; - if (! strEqu(diskmp[jj],"(not mounted)")) pjj += 1; - if (pjj > pii) { - swap(diskdev,jj,ii); - swap(diskmp,jj,ii); - swap(diskdesc,jj,ii); - } - } - - return Nmounted; -} - - -// choose backup device or enter a target directory -// update backup job target device and directory - -int chooseTarget(cchar *) // overhauled v.3.2 -{ - int ii, zstat; - char text[300]; - zdialog *zd; - cchar *instruct = "Select target device or directory"; - const char *errmess = 0; - - BDpoop(); // refresh available devices - - zd = zdialog_new("Choose Backup Target",mWin,"OK","cancel",null); - zdialog_add_widget(zd,"vbox","vb1","dialog",0,"space=10"); - zdialog_add_widget(zd,"label","lab1","vb1",instruct); // select backup device ... - zdialog_add_widget(zd,"comboE","target","vb1",BJdirk); // [_______________________][v] - - for (ii = 0; ii < Ndisk; ii++) - { // load combo box with device poop - strcpy(text,diskdev[ii]); // /dev/xxx /media/xxx (description) - strncatv(text,299," ",diskmp[ii]," (",diskdesc[ii],")",null); - zdialog_cb_app(zd,"target",text); - } - - zdialog_resize(zd,300,0); // v.4.1 - zdialog_run(zd,0,"mouse"); // run dialog posn v.4.1 - zstat = zdialog_wait(zd); - if (zstat != 1) { - zdialog_free(zd); - return 0; - } - - zdialog_fetch(zd,"target",text,299); // get device or target directory - - zdialog_free(zd); // kill dialog - - errmess = parseTarget(text); // parse selected device, directory - wprintf(mLog," new target: %s %s \n",BJdev,BJdirk); - if (errmess) wprintf(mLog," *** %s \n",errmess); - - BJedited++; // v.4.1 - return 0; -} - - -// job file open dialog - get backup job data from a file -// return 1 if OK, else 0 - -int BJfileOpen(cchar *menu) -{ - char *file; - - file = zgetfile1("open backup job","open",BJfilespec,"hidden"); // get file from user - if (file) { - strncpy0(BJfilespec,file,maxfcc-2); - zfree(file); - BJload(BJfilespec); // load job file, set BJvalid - } - - return 0; -} - - -// job file save dialog - save backup job data to a file -// return 1 if OK, else 0 - -int BJfileSave(cchar *menu) -{ - char *file; - int yn; - - if (! BJvalid && Fgui) { - yn = zmessageYN(mWin,"backup job has errors, save anyway?"); // v.3.5 - if (! yn) return 0; - } - - if (strEqu(menu,"save job")) { - BJstore(BJfilespec); - return 0; - } - - file = zgetfile1("save backup job","save",BJfilespec,"hidden"); - if (file) { - strncpy0(BJfilespec,file,maxfcc-2); - zfree(file); - BJstore(BJfilespec); - } - - return 0; -} - - -// backup job data <<< jobfile - -int BJload(cchar *jobfile) -{ - FILE *fid; - char *pp, *fspec, buff[1000]; - const char *errmess, *jobname; - int rtype, days, vers, nerrs = 0; - - snprintf(buff,999,"\n""loading job file: %s \n",jobfile); - wprintx(mLog,0,buff,boldfont); - - fid = fopen(jobfile,"r"); // open job file - if (! fid) { - wprintf(mLog," *** cannot open job file: %s \n",jobfile); - return 0; - } - - BJreset(); // reset all job data - - while (true) - { - pp = fgets_trim(buff,999,fid,1); // read next job record - if (! pp) break; // EOF - - wprintf(mLog," %s \n",buff); // output - - if (strnEqu(pp,"target",6)) { - errmess = parseTarget(buff); // target /dev/xxx /xxxxxxx - if (errmess) wprintf(mLog," *** %s \n",errmess); - continue; - } - - if (strnEqu(pp,"verify",6)) { - errmess = parseVerify(buff); // verify xxxxxx - if (errmess) wprintf(mLog," *** %s \n",errmess); - if (errmess) nerrs++; - continue; - } - - errmess = parseNXrec(buff,rtype,fspec,days,vers); // comment/include/exclude - if (errmess) wprintf(mLog," *** %s \n",errmess); - if (errmess) nerrs++; - BJfspec[BJnnx] = fspec; - BJrtype[BJnnx] = rtype; - BJretND[BJnnx] = days; - BJretNV[BJnnx] = vers; - BJnnx++; - - if (BJnnx == maxnx) { - wprintf(mLog," *** max job records exceeded \n"); - nerrs++; - break; - } - } - - fclose(fid); // close file - - if (nerrs == 0) { - BJvalid = 1; // job valid if no errors - jobname = strrchr(BJfilespec,'/') + 1; - snprintf(buff,100,"%s %s",ukopp_title,jobname); // put job name in window title v.4.0 - if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff); - BJedited = 0; - } - - return 1; -} - - -// backup job data >>> jobfile -// return 1 if OK, else 0 - -int BJstore(cchar *jobfile) -{ - FILE *fid; - char buff[100]; - cchar *jobname; - - fid = fopen(jobfile,"w"); // open file - if (! fid) { - wprintf(mLog," *** cannot open job file: %s \n",jobfile); - return 0; - } - - for (int ii = 0; ii < BJnnx; ii++) - { - if (BJrtype[ii] == 1) - fprintf(fid,"%s \n",BJfspec[ii]); // comment - - if (BJrtype[ii] == 2) { - if (BJretND[ii] + BJretNV[ii] > 0) // include /filespec (nd,nv) - fprintf(fid,"include %s%s%d,%d%s\n", - BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); - else fprintf(fid,"include %s\n",BJfspec[ii]); - } - - if (BJrtype[ii] == 3) // exclude /filespec - fprintf(fid,"exclude %s\n",BJfspec[ii]); - } - - fprintf(fid,"verify %s \n",vertype[BJvmode]); // verify xxxx - fprintf(fid,"target %s %s \n",BJdev,BJdirk); // target /dev/xxx /xxxxxxx - fclose(fid); - - if (strNeq(jobfile,TFjobfile)) { // if not job file in temp storage, - jobname = strrchr(jobfile,'/') + 1; - snprintf(buff,100,"%s %s",ukopp_title,jobname); // put job name in window title v.4.0 - if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff); - BJedited = 0; - } - - return 1; -} - - -// list backup job data to log window - -int BJlist(cchar *menu) -{ - wprintf(mLog,"\n backup job file: %s \n",BJfilespec); // job file v.21 - - for (int ii = 0; ii < BJnnx; ii++) - { - if (BJrtype[ii] == 1) // comment - wprintf(mLog," %s \n",BJfspec[ii]); - - if (BJrtype[ii] == 2) { // include /filespec (nd,nv) - if (BJretND[ii] + BJretNV[ii] > 0) - wprintf(mLog," include %s%s%d days, %d vers%s \n", - BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); - else wprintf(mLog," include %s \n",BJfspec[ii]); - } - - if (BJrtype[ii] == 3) // exclude /filespec - wprintf(mLog," exclude %s \n",BJfspec[ii]); - } - - wprintf(mLog," verify %s \n",vertype[BJvmode]); // verify xxxx - wprintf(mLog," target %s %s \n",BJdev,BJdirk); // target /dev/xxx /xxxxxx - - return 0; -} - - -// edit dialog for backup job data - -int BJedit_fchooser(cchar *dirk); -zdialog *BJedit_fchooser_zd = 0; - -char orgBJdev[40]; // v.4.1 -char orgBJdirk[200]; - -int BJedit(cchar *menu) -{ - int BJedit_dialog_event(zdialog *zd, const char *event); - - zdialog *zd; - char text[300]; - - zd = zdialog_new("Edit Backup Job",mWin,"browse","clear","done","cancel",null); - - zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=4"); // target: /dev/xxx /xxxxx [choose] - zdialog_add_widget(zd,"label","labtarg","hb1","backup target: "); // v.3.5 - zdialog_add_widget(zd,"label","target","hb1","/dev/xxx /xxxxxx"); - zdialog_add_widget(zd,"button","choosetarg","hb1","choose target"); - - zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=4"); // verify: (o) none (o) incr (o) ... - zdialog_add_widget(zd,"label","labverify","hb2","verify method: "); // v.3.5 - zdialog_add_widget(zd,"radio","vnone","hb2","none"); - zdialog_add_widget(zd,"radio","vincr","hb2","incremental","space=10"); - zdialog_add_widget(zd,"radio","vfull","hb2","full","space=10"); - zdialog_add_widget(zd,"radio","vcomp","hb2","compare","space=10"); - - zdialog_add_widget(zd,"hsep","sep2","dialog"); // edit box for job recs - zdialog_add_widget(zd,"label","labinex","dialog","Include / Exclude"); - zdialog_add_widget(zd,"frame","frminex","dialog",0,"expand"); - zdialog_add_widget(zd,"scrwin","scrwinex","frminex"); - zdialog_add_widget(zd,"edit","edinex","scrwinex"); - - snprintf(text,299,"%s %s",BJdev,BJdirk); // stuff current target v.3.5 - zdialog_stuff(zd,"target",text); - - strncpy0(orgBJdev,BJdev,40); // save in case of cancel v.4.1 - strncpy0(orgBJdirk,BJdirk,200); - - zdialog_stuff(zd,"vnone",0); // stuff verify mode v.3.5 - zdialog_stuff(zd,"vincr",0); - zdialog_stuff(zd,"vfull",0); - zdialog_stuff(zd,"vcomp",0); - if (BJvmode == 0) zdialog_stuff(zd,"vnone",1); - if (BJvmode == 1) zdialog_stuff(zd,"vincr",1); - if (BJvmode == 2) zdialog_stuff(zd,"vfull",1); - if (BJvmode == 3) zdialog_stuff(zd,"vcomp",1); - - editwidget = zdialog_widget(zd,"edinex"); - wclear(editwidget); // stuff include/exclude recs - - for (int ii = 0; ii < BJnnx; ii++) - { - if (BJrtype[ii] == 1) // comment - wprintf(editwidget,"%s\n",BJfspec[ii]); - - if (BJrtype[ii] == 2) { // include /filespec (nd,nv) - if (BJretND[ii] + BJretNV[ii] > 0) - wprintf(editwidget,"include %s%s%d,%d%s\n", - BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); - else wprintf(editwidget,"include %s\n",BJfspec[ii]); - } - - if (BJrtype[ii] == 3) // exclude /filespec - wprintf(editwidget,"exclude %s\n",BJfspec[ii]); - } - - zdialog_resize(zd,400,400); - zdialog_run(zd,BJedit_dialog_event,"40/10"); // run dialog posn v.4.1 - zdialog_wait(zd); // wait for completion - return 0; -} - - -// job edit dialog event function - -int BJedit_dialog_event(zdialog *zd, const char *event) -{ - int rtype, days, vers, nerrs = 0; - char *pp, *fspec, text[300]; - cchar *errmess = 0, *jobname; - int zstat, nn, ftf = 1; - - if (strEqu(event,"choosetarg")) { // set new target device, directory - chooseTarget(0); - snprintf(text,299,"%s %s",BJdev,BJdirk); - zdialog_stuff(zd,"target",text); - return 0; - } - - zstat = zd->zstat; // zdialog complete? - if (! zstat) { - BJedited++; // no, manual edit was done v.4.1 - return 0; - } - - zd->zstat = 0; // dialog may continue - - if (zstat == 2) { - wclear(editwidget); // clear include/exclude recs - BJedited++; // v.4.1 - return 0; - } - - if (zstat == 1) { // browse, do file-chooser dialog - if (! BJedit_fchooser_zd) - BJedit_fchooser("/home"); - return 0; - } - - if (BJedit_fchooser_zd) // kill file chooser dialog if active - zdialog_free(BJedit_fchooser_zd); - - if (zstat != 3) { // cancel or kill - zdialog_free(zd); - strcpy(BJdev,orgBJdev); // restore original target v.4.1 - strcpy(BJdirk,orgBJdirk); - BJedited = 0; - return 0; - } - - if (! BJedited) { // done - zdialog_free(zd); // no edits made v.4.1 - return 0; - } - - BJreset(); // reset job data - - zdialog_fetch(zd,"target",text,299); // get device or target directory - wprintf(mLog," target: %s \n",text); - errmess = parseTarget(text); // v.3.5 - if (errmess) wprintf(mLog," *** %s \n",errmess); - - BJvmode = 0; - zdialog_fetch(zd,"vincr",nn); // get verify mode v.3.5 - if (nn) BJvmode = 1; - zdialog_fetch(zd,"vfull",nn); - if (nn) BJvmode = 2; - zdialog_fetch(zd,"vcomp",nn); - if (nn) BJvmode = 3; - - for (BJnnx = 0; BJnnx < maxnx; BJnnx++) // get include/exclude records - { - pp = wscanf(editwidget,ftf); - if (! pp) break; - - errmess = parseNXrec(pp,rtype,fspec,days,vers); - if (errmess) { - wprintf(mLog,"%s \n *** %s \n",pp,errmess); - nerrs++; - } - - BJfspec[BJnnx] = fspec; - BJrtype[BJnnx] = rtype; - BJretND[BJnnx] = days; - BJretNV[BJnnx] = vers; - } - - if (nerrs == 0) BJvalid = 1; // valid job if no errors - - jobname = strrchr(BJfilespec,'/') + 1; - snprintf(text,100,"%s %s (*)",ukopp_title,jobname); // (*) in title for edited job v.4.0 - gtk_window_set_title(GTK_WINDOW(mWin),text); - zdialog_free(zd); // destroy dialog - - return 0; -} - - -// file chooser dialog for backup job edit - -int BJedit_fchooser(cchar *dirk) // v.3.5 -{ - int BJedit_fchooser_event(zdialog *zd, const char *event); - - BJedit_fchooser_zd = zdialog_new("Choose Files for Backup",mWin,"Done",null); - zdialog *zd = BJedit_fchooser_zd; - - zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand"); - zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); - zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5"); - zdialog_add_widget(zd,"label","space","hb1",0,"expand"); - zdialog_add_widget(zd,"button","incl","hb1","include","space=5"); - zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5"); - zdialog_add_widget(zd,"check","showhf","hb1","Show hidden","space=10"); - zdialog_add_widget(zd,"label","space","hb2",0,"expand"); - zdialog_add_widget(zd,"label","lab1","hb2","Retain old files: Days: "); - zdialog_add_widget(zd,"spin","days","hb2","0|9999|1|0"); - zdialog_add_widget(zd,"label","lab2","hb2"," Versions: "); - zdialog_add_widget(zd,"spin","vers","hb2","0|9999|1|0"); - zdialog_add_widget(zd,"label","space","hb2",0,"space=5"); - - fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); - GtkWidget *frame = zdialog_widget(zd,"fr1"); - gtk_container_add(GTK_CONTAINER(frame),fc_widget); - gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); - gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); - gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),1); - - zdialog_stuff(zd,"showhf",1); - zdialog_resize(zd,550,500); - zdialog_run(zd,BJedit_fchooser_event); - zdialog_wait(zd); - zdialog_free(zd); - BJedit_fchooser_zd = 0; - return 0; -} - - -int BJedit_fchooser_event(zdialog *zd, const char *event) -{ - GSList *flist = 0; - struct stat64 filestat; - char *file1, *file2; - int ii, err, showhf, days, vers; - - if (strEqu(event,"showhf")) // show/hide hidden files - { - zdialog_fetch(zd,"showhf",showhf); - gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),showhf); - } - - if (strEqu(event,"incl") || strEqu(event,"excl")) // include or exclude - { - flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); - - for (ii = 0; ; ii++) // process selected files - { - file1 = (char *) g_slist_nth_data(flist,ii); - if (! file1) break; - - file2 = strdupz(file1,2); // extra space for wildcard - g_free(file1); - - err = lstat64(file2,&filestat); - if (err) { - wprintf(mLog," *** error: %s file: %s \n",strerror(errno),file2); - continue; - } - - if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard - - zdialog_fetch(zd,"days",days); // get corresp. retention specs - zdialog_fetch(zd,"vers",vers); // from dialog - - if (strEqu(event,"incl")) { // include /filespec (dd,vv) v.3.5 - if (days || vers) - wprintf(editwidget,"include %s%s%d,%d%s\n",file2,RSEP1,days,vers,RSEP2); - else wprintf(editwidget,"include %s""\n",file2); - } - if (strEqu(event,"excl")) - wprintf(editwidget,"exclude %s""\n",file2); - - zfree(file2); - BJedited++; - } - - gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); - g_slist_free(flist); - } - - return 0; -} - - -// parse and validate a comment/include/exclude record -// filespec* means a /path.../filename with wildcards -// # comment (or blank line) -// include filespec* [ (days,vers) ] // v.3.5 -// exclude filespec* - -cchar * parseNXrec(const char *jobrec, int &rtype, char *&fspec, int &days, int &vers) -{ - int nn, Nth = 1; - const char *pp1, *pp2; - - rtype = days = vers = -1; - fspec = null; - - pp1 = strField(jobrec,' ',Nth++); - - if (! pp1 || *pp1 == '#') { // comment or blank line - rtype = 1; - if (pp1) fspec = strdupz(pp1); - else fspec = strdupz(""); - return 0; - } - - if (strEqu(pp1,"include")) { // include /filespec (nd,nv) - rtype = 2; - pp1 = jobrec + 7; - while(*pp1 == ' ') pp1++; - pp2 = strstr(pp1,RSEP1); - if (! pp2) { // assume no (nd,nv) - fspec = strdupz(pp1,4); // v.4.1 - days = vers = 0; - } - else { // parse (nd,nv) v.3.5 - nn = sscanf(pp2,RSEP1" %d , %d "RSEP2,&days,&vers); - if (nn != 2 || days < 0 || days > 9999 || vers < 0 || vers > 9999) - return "invalid retention spec, use \" (nn,nn)\" "; - fspec = strdupz(pp1,4); // v.4.1 - fspec[pp2-pp1] = 0; - } - strTrim2(fspec); // strip trailing blanks v.4.0 - pp1 = fspec; - if (*pp1 == '"') { - pp1++; // allow quoted filespec v.4.1 - pp2 = pp1 + strlen(pp1); - if (pp2[-1] != '"') return "filespec closing quote missing"; - } - if (*pp1 != '/') return "filespec missing /topdir/"; - pp1 = strchr(pp1+1,'/'); - if (!pp1) return "filespec missing /topdir/"; - pp2 = strchr(fspec,'*'); - if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed"; - pp2 = strchr(fspec,'?'); - if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed"; - return 0; - } - - if (strEqu(pp1,"exclude")) { // exclude /filespec - rtype = 3; - pp1 = jobrec + 7; - while(*pp1 == ' ') pp1++; - fspec = strdupz(pp1,4); // v.4.1 - strTrim2(fspec); // strip trailing blanks v.4.0 - pp1 = fspec; - if (*pp1 == '"') { // allow quoted filespec v.4.1 - pp2 = pp1 + strlen(pp1); - if (pp2[-1] != '"') return "filespec closing quote missing"; - } - return 0; - } - - return "unrecognized record type"; -} - - -// parse a verify record: verify xxxxx - -cchar * parseVerify(const char *text) // v.3.5 -{ - const char *pp; - - BJvmode = 0; - - pp = strField(text,' ',1); - if (! pp || strNeq(pp,"verify")) return "bad verify record"; - - pp = strField(text,' ',2); - if (! pp) return "missing verify type"; - - BJvmode = -1; - if (strEqu(pp,"none")) BJvmode = 0; - if (strnEqu(pp,"incr",4)) BJvmode = 1; - if (strEqu(pp,"full")) BJvmode = 2; - if (strnEqu(pp,"comp",4)) BJvmode = 3; - if (BJvmode >= 0) return 0; - - BJvmode = 0; - return "bad verify mode"; -} - - -// parse a target record and set backup device and directory accordingly -// format: [ target ] [ /dev/xxx ] [ /directory ] - -cchar * parseTarget(const char *text) // more robust v.3.5 -{ - int ii, err, cc, yn, Nth = 1; - int direxists = 0, dirempty = 0; - char ch; - const char *pp; - DIR *dirf; - struct dirent *ppd; - struct stat dstat; - - bFilesReset(); // no files at backup location - *BJdev = *BJdirk = BJdcc = 0; // reset target poop - - pp = strField(text,' ',Nth++); - - if (pp && strEqu(pp,"target")) // skip "target" - pp = strField(text,' ',Nth++); - - if (pp && strnEqu(pp,"/dev/",5)) { - strncpy0(BJdev,pp,39); // have /dev/xxxx - pp = strField(text,' ',Nth++); - } - - if (pp && *pp == '/') { - strncpy0(BJdirk,pp,199); // have /directory/... - BJdcc = strlen(BJdirk); - } - - if (! *BJdev && ! *BJdirk) return "no backup target specified"; - - BDpoop(); // refresh known device data - - if (*BJdev) { // if device is specified - for (ii = 0; ii < Ndisk; ii++) - if (strEqu(BJdev,diskdev[ii])) break; // look for device - if (ii == Ndisk) return "target device not found"; - } - - if (*BJdev && ! *BJdirk) { // get mount point for device - for (ii = 0; ii < Ndisk; ii++) - if (strEqu(BJdev,diskdev[ii])) break; - if (ii < Ndisk && *diskmp[ii] == '/') strcpy(BJdirk,diskmp[ii]); - } - - if (! *BJdev && *BJdirk) { // get device for mount point - for (ii = 0; ii < Ndisk; ii++) - if (strEqu(BJdirk,diskmp[ii])) break; - if (ii < Ndisk) strcpy(BJdev,diskdev[ii]); - } - - if (*BJdev && ! *BJdirk) { // if no directory specified, - strcpy(BJdirk,"/media"); // set a default for device - strcpy(BJdirk+6,BJdev+4); // e.g. /media/sdf1 - } - - BJdcc = strlen(BJdirk); // set target directory cc - - err = stat(BJdirk,&dstat); // determine if directory - if (! err && S_ISDIR(dstat.st_mode)) direxists = 1; // exists in file system - - if (direxists) { // determine if directory is empty - dirempty = 1; - dirf = opendir(BJdirk); - if (dirf) { - while (true) { - ppd = readdir(dirf); - if (! ppd) break; - if (ppd->d_name[0] == '.') continue; - dirempty = 0; - break; - } - closedir(dirf); - } - } - - if (direxists) { // directory exists - if (*BJdev) { // if device is specified, - for (ii = 0; ii < Ndisk; ii++) // find where it is mounted - if (strEqu(BJdev,diskdev[ii])) break; - if (ii == Ndisk || *diskmp[ii] != '/') { // device not mounted - if (dirempty) { - wprintf(mLog,"target is valid and not mounted \n"); // mount to existing empty - return 0; // directory is allowed - } - else return "target directory is in use"; // directory is not empty - } - else { // device is mounted - cc = strlen(diskmp[ii]); - if (! strnEqu(diskmp[ii],BJdirk,cc)) - return "target directory not on target device"; // somewhere else - ch = BJdirk[cc]; - if (ch && ch != '/') - return "target directory not on target device"; - devMounted = 1; // device mounted at directory - strcpy(mountdev,BJdev); // save for later unmount() - strcpy(mountdirk,BJdirk); - wprintf(mLog,"target is valid and mounted \n"); - return 0; - } - } - else { // device not specified - wprintf(mLog,"target directory is valid \n"); - return 0; - } - } - else { // directory does not exist - if (*BJdev) { - wprintf(mLog,"target is valid and not mounted \n"); // can be created at mount time - return 0; - } - else { - yn = zmessageYN(mWin,"target directory does not exist\n" // offer to create if missing - "create target directory?"); // v.4.1 - if (! yn) return "target directory does not exist"; - err = mkdir(BJdirk,0751); - if (err) return strerror(errno); - else return "target directory created"; - } - } -} - - -// Delete backup files exceeding age and version limits. -// Copy new and modified disk files to backup location. - -int Backup(cchar *menu) -{ - char work[100]; - int vmode = 0, terr = 0, ii, jj, yn; - int upvers = 0, deleted = 0; - char disp, *dfile = 0; - const char *errmess = 0; - double bsecs, bbytes, bspeed; - double time0; - - if (! BJvalid) { - if (Fgui) zmessageACK(mWin,"backup job has errors (open or edit)"); - else wprintf(mLog,"backup job has errors \n"); - return 0; - } - - if (! mount(0)) return 0; // validate and mount target v.3.2 - - Report("diffs summary"); // refresh all file data, report diffs - - if (Fgui) { - yn = zmessageYN(mWin,"backup target: %s %s \n" - "%d files (%s) will be copied to \n" - "(or deleted from) backup media \n" "continue?", - BJdev,BJdirk,Mfiles,formatKBMB(Mbytes,3)); // confirm backup target v.23 - if (! yn) return 0; - } - - snprintf(work,99,"\n""begin backup \n"); - wprintx(mLog,0,work,boldfont); - wprintf(mLog," files: %d bytes: %s \n",Mfiles,formatKBMB(Mbytes,3)); // files and bytes to copy - - if (Mfiles == 0) { - wprintf(mLog," *** nothing to back-up \n"); - return 0; - } - - wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk); - - if (strEqu(menu,"backup only")) vmode = 0; // backup command, no auto verify - if (strEqu(menu,"run job")) vmode = BJvmode; - - wprintf(mLog," assign new version numbers to modified backup files \n" - " and purge expired versions from backup location \n\n"); - - gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 - - for (ii = 0; ii < Bnf; ii++) // scan files at backup location - { - disp = Brec[ii].disp; - dfile = Brec[ii].file; - - errmess = null; - - if (disp == 'm' || disp == 'd') { // modified or deleted, - errmess = setnextVersion(Brec[ii]); // rename to next version number - Brec[ii].err = -1; // mark file gone - if (disp =='m') upvers++; // update counts - if (disp =='d') deleted++; - } - - if (! errmess) errmess = purgeVersions(Brec[ii],1); // purge expired file versions - // (exclude last version) v.4.0 - if (errmess) { - wprintf(mLog," %s \n *** %s \n",dfile,errmess); // log error v.4.3 - terr++; - if (terr > 100) goto backup_fail; - } - } - - wprintf(mLog," %d backup files were assigned new versions \n",upvers); - wprintf(mLog," %d backup files were deleted \n",deleted); - wprintf(mLog," %d expired versions (%s) were purged \n\n",Pfiles,formatKBMB(Pbytes,3)); - Pbytes = Pfiles = 0; - - start_timer(time0); // start timer - bbytes = Mbytes; - - BJstore(TFjobfile); // copy job file to temp file - writeDT(); // create date-time temp file - - wprintf(mLog,-2," %s \n",BD_JOBFILE); - errmess = copyFile(TFjobfile,BD_JOBFILE,2); // copy job file to backup location - if (errmess) goto backup_fail; - - wprintf(mLog,-2," %s \n",BD_DATETIME); - errmess = copyFile(TFdatetime,BD_DATETIME,2); // copy date-time file - if (errmess) goto backup_fail; - - wprintf(mLog," copying new and modified files from disk to backup location \n\n"); - - for (ii = 0; ii < Dnf; ii++) // scan all disk files - { - disp = Drec[ii].disp; - dfile = Drec[ii].file; - Drec[ii].finc = 0; // not included yet - - if (disp == 'n' || disp == 'm') // new or modified file - { - wprintf(mLog," %s \n",dfile); - errmess = copyFile(dfile,dfile,2); // copy disk file to backup v.4.3 - if (errmess) { - Drec[ii].err = 1; // copy failed - wprintf(mLog," *** %s \n",errmess); // log error v.4.3 - terr++; - if (terr > 100) goto backup_fail; - } - else { // copy OK - Drec[ii].finc = 1; // set included file flag - } - - if (checkKillPause()) goto backup_fail; // killed by user - } - - jj = Drec[ii].bindx; // purge last version now - if (jj >= 0) purgeVersions(Brec[jj],0); // bugfix v.4.1 - } - - if (terr) wprintf(mLog," *** %d files were not copied \n",terr); - - synch_poop("backup"); // synch owner and permissions data - - bsecs = get_timer(time0); // output perf. statistics - wprintf(mLog," backup time: %.1f secs \n",bsecs); - bspeed = bbytes/mega/bsecs; - wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed); - wprintf(mLog," backup complete \n"); - - if (vmode) // do verify if requested - { - wprintf(mLog,"\n"); - sleep(2); - - if (vmode == 1) Verify("incr"); - else if (vmode == 2) Verify("full"); - else if (vmode == 3) Verify("comp"); - - wprintx(mLog,0," backup OK \n",boldfont); // repeat backup status - // v.4.1 - if (fverrs + fcerrs == 0) - wprintx(mLog,0," verify OK \n",boldfont); // then verify status - else - wprintx(mLog,0," verify had errors \n",boldfont); - } - - wprintf(mLog," ready \n"); - - if (ukoppMounted) unmount(0); // leave unmounted v.3.5.2 - - gdk_window_set_cursor(mLogwin,0); // normal cursor - return 0; - -backup_fail: - if (terr > 100) wprintf(mLog," too many errors, giving up \n"); - else if (errmess) wprintf(mLog," %s \n",errmess); - wprintx(mLog,0," *** BACKUP FAILED \n",boldfont); - - bFilesReset(); - killFlag = 0; - gdk_window_set_cursor(mLogwin,0); // normal cursor - return 0; -} - - -// synchronize disk and backup files // v.25 -// bi-directional copy of new and newer files - -int Synch(cchar *menu) -{ - int ii, yn, dii, bii, comp; - char disp, *dfile = 0; - time_t btime, dtime; - const char *errmess = 0; - - if (! BJvalid) { - wprintf(mLog," *** job data has errors \n"); - return 0; - } - - if (! mount(0)) return 0; // validate and mount target v.3.2 - - if (Fgui) { - yn = zmessageYN(mWin,"backup target: %s %s \n continue?",BJdev,BJdirk); // confirm backup target - if (! yn) return 0; - } - wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk); - - dGetFiles(); // get disk files of backup job - if (bGetFiles() < 0) goto synch_exit; // get files in backup location - setFileDisps(); // compare and set dispositions - - wprintf(mLog,"\n begin synchronize \n"); - - gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 - - BJstore(TFjobfile); // copy job file to temp file - writeDT(); // create date-time temp file - - wprintf(mLog,-2," %s \n",BD_JOBFILE); - errmess = copyFile(TFjobfile,BD_JOBFILE,2); // copy job file to backup location - if (errmess) goto synch_exit; - - wprintf(mLog,-2," %s \n",BD_DATETIME); - errmess = copyFile(TFdatetime,BD_DATETIME,2); // copy date-time file - if (errmess) goto synch_exit; - - for (ii = 0; ii < Dnf; ii++) // copy new disk files >> backup loc. - { - disp = Drec[ii].disp; - dfile = Drec[ii].file; - if (disp != 'n') continue; - wprintf(mLog," disk >> backup: %s \n",dfile); - errmess = copyFile(dfile,dfile,2); - if (errmess) wprintf(mLog," *** %s \n",errmess); - else Drec[ii].finc = 1; - if (checkKillPause()) goto synch_exit; // killed by user - } - - for (ii = 0; ii < Bnf; ii++) // copy new backup files >> disk - { // (aka "deleted" disk files) - disp = Brec[ii].disp; - dfile = Brec[ii].file; - if (disp != 'd') continue; - wprintf(mLog," backup >> disk: %s \n",dfile); - errmess = copyFile(dfile,dfile,1); - if (errmess) wprintf(mLog," *** %s \n",errmess); - else Brec[ii].finc = 1; - if (checkKillPause()) goto synch_exit; - } - - dii = bii = 0; - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = strcmp(Drec[dii].file, Brec[bii].file); - - if (comp < 0) { dii++; continue; } // next disk file - if (comp > 0) { bii++; continue; } // next backup file - - disp = Drec[dii].disp; - dfile = Drec[dii].file; - - if (disp == 'm') // screen for modified status - { - btime = int(Brec[bii].mtime); - dtime = int(Drec[dii].mtime); - - if (btime > dtime) { // copy newer backup file >> disk - wprintf(mLog," backup >> disk: %s \n",dfile); - errmess = copyFile(dfile,dfile,1); - if (errmess) wprintf(mLog," *** %s \n",errmess); - else Brec[bii].finc = 1; - } - - else { // copy newer disk file >> backup - wprintf(mLog," disk >> backup: %s \n",dfile); - errmess = copyFile(dfile,dfile,2); - if (errmess) wprintf(mLog," *** %s \n",errmess); - else Drec[dii].finc = 1; - } - } - - dii++; // next disk and backup files - bii++; - - if (checkKillPause()) goto synch_exit; // killed by user - } - - errmess = null; - synch_poop("synch"); // synch owner and permissions data - - Verify("incremental"); // verify all files copied - -synch_exit: - if (errmess) wprintf(mLog," *** %s \n",errmess); - wprintf(mLog," ready \n"); // v.3.6 - killFlag = 0; - gdk_window_set_cursor(mLogwin,0); // normal cursor - return 0; -} - - -// verify integrity of backup files - -int Verify(cchar *menu) -{ - int ii, vers, comp, vfiles; - int dfiles1 = 0, dfiles2 = 0; - char filespec[maxfcc]; - const char *errmess = 0; - double secs, dcc1, vbytes, vspeed; - double mtime, diff; - double time0; - struct stat64 filestat; - - vfiles = fverrs = fcerrs = 0; - vbytes = 0.0; - if (! mount(0)) return 0; // validate and mount target v.3.2 - - start_timer(time0); - - gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 - - if (strnEqu(menu,"incremental",4)) // verify new/modified files only - { - wprintx(mLog,0,"\n""Verify files copied in prior backup or synch \n",boldfont); - - for (ii = 0; ii < Dnf; ii++) // scan disk file list - { - if (! Drec[ii].finc) continue; // file included in last backup - strncpy0(filespec,Drec[ii].file,maxfcc-1); - wprintf(mLog," %s \n",filespec); // output filespec - - errmess = checkFile(filespec,1,dcc1); // compare disk/backup files, get length - if (errmess) { - wprintf(mLog," *** %s \n\n",errmess); // log and count errors - if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure - else fverrs++; - } - - vfiles++; // count files and bytes - vbytes += dcc1; - if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 - - if (checkKillPause()) goto verify_exit; // killed by user - } - - for (ii = 0; ii < Bnf; ii++) // scan backup file list v.25 - { - if (! Brec[ii].finc) continue; // file included in last backup - strncpy0(filespec,Brec[ii].file,maxfcc-1); - wprintf(mLog," %s \n",filespec); // output filespec - - errmess = checkFile(filespec,1,dcc1); // compare disk/backup files, get length - if (errmess) { - wprintf(mLog," *** %s \n\n",errmess); // log and count errors - if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure - else fverrs++; - } - - vfiles++; // count files and bytes - vbytes += dcc1; - if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 - - if (checkKillPause()) goto verify_exit; // killed by user - } - } - - if (strEqu(menu,"full")) // verify all files are readable - { - wprintx(mLog,0,"\n""Read and verify ALL backup files \n\n",boldfont); - - bGetFiles(); // get all files at backup location - wprintf(mLog," %d backup files \n",Bnf); - if (! Bnf) goto verify_exit; - - for (ii = 0; ii < Bnf; ii++) // scan backup file list - { - strncpy0(filespec,Brec[ii].file,maxfcc-10); // /directory.../filename - - if (Brec[ii].err == 0) - { // check current file - wprintf(mLog,-2," %s \n",filespec); - errmess = checkFile(filespec,0,dcc1); // verify file, get length - if (errmess) { - wprintf(mLog,-1," *** %s \n",errmess); // log and count error - wprintf(mLog,"\n"); - fverrs++; - } - vfiles++; // count files and bytes - vbytes += dcc1; - } - - if (Brec[ii].lover) - for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++) // check previous versions - { - setFileVersion(filespec,vers); // append version if > 0 - wprintf(mLog,-2," %s \n",filespec); - errmess = checkFile(filespec,0,dcc1); // verify file, get length - if (errmess) { - wprintf(mLog,-1," *** %s \n",errmess); // log and count error - wprintf(mLog,"\n"); - fverrs++; - } - vfiles++; // count files and bytes - vbytes += dcc1; - } - - if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 - - if (checkKillPause()) goto verify_exit; // killed by user - } - } - - if (strnEqu(menu,"compare",4)) // compare backup files to disk files - { - wprintx(mLog,0,"\n Read and verify ALL backup files. \n",boldfont); - wprintf(mLog," Compare to correspending disk files (if present). \n\n"); - - bGetFiles(); // get all files at backup location - wprintf(mLog," %d backup files \n",Bnf); - if (! Bnf) goto verify_exit; - - for (ii = 0; ii < Bnf; ii++) // scan backup file list - { - strncpy0(filespec,Brec[ii].file,maxfcc-10); // /directory.../filename - - if (Brec[ii].err == 0) - { // check current file - comp = 0; - if (lstat64(filespec,&filestat) == 0) { // corresponding disk file exists - mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; - diff = fabs(mtime - Brec[ii].mtime); // compare disk and backup mod times - if (diff < MODTIMETOLR) comp = 1; // equal within file system resolution - dfiles1++; // count matching disk names - dfiles2 += comp; // count matching mod times - } - - wprintf(mLog,-2," %s \n",filespec); - errmess = checkFile(filespec,comp,dcc1); // verify, get length, compare disk - if (errmess) { - wprintf(mLog,-1," *** %s \n",errmess); // log and count error - wprintf(mLog,"\n"); - if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure - else fverrs++; - } - vfiles++; // count files and bytes - vbytes += dcc1; - } - - if (Brec[ii].lover) - for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++) // check previous versions - { - setFileVersion(filespec,vers); // append version if > 0 - wprintf(mLog,-2," %s \n",filespec); - errmess = checkFile(filespec,0,dcc1); // verify file, get length - if (errmess) { - wprintf(mLog,-1," *** %s \n",errmess); // log and count error - wprintf(mLog,"\n"); - fverrs++; - } - vfiles++; // count files and bytes - vbytes += dcc1; - } - - if (checkKillPause()) goto verify_exit; // killed by user - if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 - } - } - - wprintf(mLog," backup files: %d (%s) \n",vfiles,formatKBMB(vbytes,3)); - wprintf(mLog," backup file read errors: %d \n",fverrs); - - if (strnEqu(menu,"incremental",4)) - wprintf(mLog," compare failures: %d \n",fcerrs); - - if (strnEqu(menu,"compare",4)) { - wprintf(mLog," matching disk names: %d mod times: %d \n",dfiles1,dfiles2); - wprintf(mLog," compare failures: %d \n",fcerrs); - } - - secs = get_timer(time0); - wprintf(mLog," verify time: %.1f secs \n",secs); - vspeed = vbytes/mega/secs; - wprintf(mLog," verify speed: %.2f MB/sec \n",vspeed); - -verify_exit: - if (fverrs + fcerrs) wprintx(mLog,0," *** THERE WERE ERRORS *** \n",boldfont); - else wprintx(mLog,0," NO ERRORS \n",boldfont); // v.3.9 - wprintf(mLog," ready \n"); // v.3.6 - killFlag = 0; - gdk_window_set_cursor(mLogwin,0); // normal cursor - return 0; -} - - -// various kinds of reports - -int Report(cchar *menu) -{ - char *fspec1; - char fspec2[200], bfile[maxfcc]; - char *pslash, *pdirk, ppdirk[maxfcc]; - char header[100]; - int ii, kfiles, knew, kdel, kmod; - int dii, bii, comp, err; - double nbytes, mb1, mb2, fage; - int vers, lover, hiver, nexpv; - int age, loage, hiage; - struct tm tmdt; - time_t btime, dtime; - char bmod[20], dmod[20]; - const char *copy; - struct stat64 filestat; - - // get all disk files in backup job - // report file and byte counts per include and exclude record - - if (strEqu(menu, "get disk files")) - { - dGetFiles(); // get all files on disk - - wprintx(mLog,0,"\n"" files bytes filespec retention (days, vers) \n",boldfont); - - for (ii = 0; ii < BJnnx; ii++) { // formatted report - if (BJfspec[ii]) { - if (BJfiles[ii]) { - if (BJrtype[ii] == 2) // include: add retention v.4.0 - wprintf(mLog," %6d %9s %s (%d, %d) \n", - BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii],BJretND[ii],BJretNV[ii]); - else - wprintf(mLog," %6d %9s %s \n",BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii]); - } - else if (BJrtype[ii] > 1) { - wprintx(mLog,0," NO FILES",boldfont); - wprintf(mLog," %s \n",BJfspec[ii]); - } - else - wprintf(mLog," %s \n",BJfspec[ii]); - } - } - - wprintf(mLog," %6d %9s TOTALS \n", Dnf, formatKBMB(Dbytes,3)); - goto report_exit; - } - - // report disk / backup differences: new, modified, and deleted files - - if (strEqu(menu, "diffs summary")) - { - dGetFiles(); - if (bGetFiles() < 0) goto report_exit; - setFileDisps(); - - wprintf(mLog,"\n disk files: %d backup files: %d \n",Dnf,Bnf); - wprintx(mLog,0,"\n Differences between files on disk and backup files: \n",boldfont); - wprintf(mLog," %6d disk files not found on backup (new files) \n",nnew); - wprintf(mLog," %6d files with different data (modified files) \n",nmod); - wprintf(mLog," %6d backup files not found on disk (deleted files) \n",ndel); - wprintf(mLog," %6d files with identical data (unchanged files) \n",nunc); - wprintf(mLog," Total differences: %d files (%s new + modified) \n\n",Mfiles,formatKBMB(Mbytes,3)); - goto report_exit; - } - - // report disk / backup differences per directory level - - if (strEqu(menu, "diffs by directory")) - { - dGetFiles(); - if (bGetFiles() < 0) goto report_exit; - setFileDisps(); - - SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first - SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D'); - - wprintx(mLog,0,"\n differences by directory \n",boldfont); - - wprintx(mLog,0," new mod del bytes directory \n",boldfont); - - nbytes = kfiles = knew = kmod = kdel = 0; - dii = bii = 0; - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = filecomp(Drec[dii].file, Brec[bii].file); - - if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file - else pdirk = Drec[dii].file; - - pslash = (char *) strrchr(pdirk,'/'); // isolate directory - if (pslash) *pslash = 0; - if (strNeq(pdirk,ppdirk)) { // if directory changed, output - if (kfiles > 0) // totals from prior directory - wprintf(mLog," %5d %5d %5d %8s %s \n", - knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk); - nbytes = kfiles = knew = kmod = kdel = 0; // reset totals - strcpy(ppdirk,pdirk); // start new directory - } - if (pslash) *pslash = '/'; - - if (comp < 0) { // unmatched disk file: new - knew++; // count new files - kfiles++; - nbytes += Drec[dii].size; - dii++; - } - - else if (comp > 0) { // unmatched backup file - if (Brec[bii].disp == 'd') { - kdel++; // count deleted files - kfiles++; - } - bii++; - } - - else if (comp == 0) { // file present on disk and backup - if (Drec[dii].disp == 'm') kmod++; // count modified files - if (Drec[dii].disp == 'n') knew++; // count new files (backup disp is 'v') - if (Drec[dii].disp != 'u') { - kfiles++; // count unless unchanged - nbytes += Drec[dii].size; - } - dii++; - bii++; - } - } - - if (kfiles > 0) wprintf(mLog," %5d %5d %5d %s %s \n", // totals from last directory - knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk); - - SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore straight ascii sort - SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A'); - goto report_exit; - } - - // report disk / backup differences by file status and directory - - if (strEqu(menu, "diffs by file status")) // new v.4.1 - { - dGetFiles(); - if (bGetFiles() < 0) goto report_exit; - setFileDisps(); - - SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first - SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D'); - - wprintx(mLog,0,"\n new files by directory \n",boldfont); // report new files - wprintx(mLog,0," files bytes directory \n",boldfont); - - nbytes = knew = 0; - dii = bii = 0; - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = filecomp(Drec[dii].file, Brec[bii].file); - - if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file - else pdirk = Drec[dii].file; - - pslash = (char *) strrchr(pdirk,'/'); // isolate directory - if (pslash) *pslash = 0; - if (strNeq(pdirk,ppdirk)) { // if directory changed, output - if (knew > 0) // totals from prior directory - wprintf(mLog," %6d %8s %s \n", - knew,formatKBMB(nbytes,3),ppdirk); - nbytes = knew = 0; // reset totals - strcpy(ppdirk,pdirk); // start new directory - } - if (pslash) *pslash = '/'; - - if (comp < 0) { // unmatched disk file: new - knew++; // count new files - nbytes += Drec[dii].size; - dii++; - } - - else if (comp > 0) // unmatched backup file: deleted - bii++; - - else if (comp == 0) { // file present on disk and backup - if (Drec[dii].disp == 'n') { // count new files (backup disp is 'v') - knew++; - nbytes += Drec[dii].size; - } - dii++; - bii++; - } - } - - if (knew > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory - knew,formatKBMB(nbytes,3),ppdirk); - - wprintx(mLog,0,"\n modified files by directory \n",boldfont); // report modified files - wprintx(mLog,0," files bytes directory \n",boldfont); - - nbytes = kmod = 0; - dii = bii = 0; - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = filecomp(Drec[dii].file, Brec[bii].file); - - if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file - else pdirk = Drec[dii].file; - - pslash = (char *) strrchr(pdirk,'/'); // isolate directory - if (pslash) *pslash = 0; - if (strNeq(pdirk,ppdirk)) { // if directory changed, output - if (kmod > 0) // totals from prior directory - wprintf(mLog," %6d %8s %s \n", - kmod,formatKBMB(nbytes,3),ppdirk); - nbytes = kmod = 0; // reset totals - strcpy(ppdirk,pdirk); // start new directory - } - if (pslash) *pslash = '/'; - - if (comp < 0) // unmatched disk file: new - dii++; - - else if (comp > 0) // unmatched backup file: deleted - bii++; - - else if (comp == 0) { // file present on disk and backup - if (Drec[dii].disp == 'm') { // count modified files - kmod++; - nbytes += Drec[dii].size; - } - dii++; - bii++; - } - } - - if (kmod > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory - kmod,formatKBMB(nbytes,3),ppdirk); - - wprintx(mLog,0,"\n deleted files by directory \n",boldfont); // report deleted files - wprintx(mLog,0," files bytes directory \n",boldfont); - - nbytes = kdel = 0; - dii = bii = 0; - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = filecomp(Drec[dii].file, Brec[bii].file); - - if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file - else pdirk = Drec[dii].file; - - pslash = (char *) strrchr(pdirk,'/'); // isolate directory - if (pslash) *pslash = 0; - if (strNeq(pdirk,ppdirk)) { // if directory changed, output - if (kdel > 0) // totals from prior directory - wprintf(mLog," %6d %8s %s \n", - kdel,formatKBMB(nbytes,3),ppdirk); - nbytes = kdel = 0; // reset totals - strcpy(ppdirk,pdirk); // start new directory - } - if (pslash) *pslash = '/'; - - if (comp < 0) // unmatched disk file: new - dii++; - - else if (comp > 0) { // unmatched backup file: deleted - if (Brec[bii].disp == 'd') { - kdel++; // count deleted files - nbytes += Brec[bii].size; - } - bii++; - } - - else if (comp == 0) { // file present on disk and backup - dii++; - bii++; - } - } - - if (kdel > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory - kdel,formatKBMB(nbytes,3),ppdirk); - - SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore straight ascii sort - SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A'); - goto report_exit; - } - - // report disk / backup differences by file - - if (strEqu(menu, "diffs by file")) - { - dGetFiles(); - if (bGetFiles() < 0) goto report_exit; - setFileDisps(); - - wprintx(mLog,0,"\n Detailed list of disk:backup differences: \n",boldfont); - - snprintf(header,99,"\n %d disk files not found on backup \n",nnew); - wprintx(mLog,0,header,boldfont); - - for (ii = 0; ii < Dnf; ii++) - { - if (Drec[ii].disp != 'n') continue; - wprintf(mLog," %s \n",Drec[ii].file); - } - - snprintf(header,99,"\n %d backup files not found on disk \n",ndel); - wprintx(mLog,0,header,boldfont); - - for (ii = 0; ii < Bnf; ii++) - { - if (Brec[ii].disp != 'd') continue; - wprintf(mLog," %s \n",Brec[ii].file); - } - - snprintf(header,99,"\n %d files with different data \n",nmod); - wprintx(mLog,0,header,boldfont); - - wprintx(mLog,0," backup mod date copy disk mod date filespec \n",boldfont); - - dii = bii = 0; - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { // revised v.25 - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = strcmp(Drec[dii].file, Brec[bii].file); - - if (comp < 0) { dii++; continue; } // next disk file - if (comp > 0) { bii++; continue; } // next backup file - - if (Drec[dii].disp == 'm') // screen for modified status - { - btime = int(Brec[bii].mtime); // mod time on backup - dtime = int(Drec[dii].mtime); // mod time on disk - - copy = "<<<<"; // copy direction, disk to backup - if (btime > dtime) copy = "!!!!"; // flag if backup to disk - - tmdt = *localtime(&btime); - snprintf(bmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900, - tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min); - - tmdt = *localtime(&dtime); - snprintf(dmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900, - tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min); - - wprintf(mLog," %s %s %s %s \n",bmod,copy,dmod,Drec[dii].file); - } - - dii++; // next disk and backup files - bii++; - } - - goto report_exit; - } - - // report versions and expired versions per file - - if (strEqu(menu, "file versions")) - { - Report("diffs summary"); - if (Bnf < 1) goto report_exit; - - wprintx(mLog,0,"\n lover hiver nxver loage hiage bytes expired filespec \n",boldfont); - - for (ii = 0; ii < Bnf; ii++) - { - lover = Brec[ii].lover; - hiver = Brec[ii].hiver; - nexpv = Brec[ii].nexpv; - if (! lover) continue; - - strcpy(bfile,BJdirk); - strcat(bfile,Brec[ii].file); - loage = hiage = -1; - mb1 = mb2 = 0.0; - - for (vers = lover; vers <= hiver; vers++) // loop file versions - { - setFileVersion(bfile,vers); - err = lstat64(bfile,&filestat); // check file exists on backup - if (err) continue; - - fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file age in days - age = fage; // remove fraction - if (loage < 0) loage = hiage = age; - if (age < loage) loage = age; - if (age > hiage) hiage = age; - - mb1 += filestat.st_size; // accumulate total bytes - if (vers < lover + nexpv) // v.4.0 - mb2 += filestat.st_size; // and expired version bytes - } - - wprintf(mLog," %5d %5d %5d %5d %5d %8s %8s %s \n", - lover,hiver,nexpv,loage,hiage,formatKBMB(mb1,3),formatKBMB(mb2,3),Brec[ii].file); - } - - goto report_exit; - } - - // report expired file versions (will be purged) - - if (strEqu(menu, "expired versions")) - { - Report("diffs summary"); - if (Bnf < 1) goto report_exit; - - wprintx(mLog,0,"\n expired files (will purge from backup location) \n",boldfont); - wprintx(mLog,0,"\n vers age bytes filespec \n",boldfont); - - for (ii = 0; ii < Bnf; ii++) - { - lover = Brec[ii].lover; - hiver = Brec[ii].hiver; - nexpv = Brec[ii].nexpv; - if (! nexpv) continue; - - strcpy(bfile,BJdirk); - strcat(bfile,Brec[ii].file); - mb1 = 0.0; - - for (vers = lover; vers < lover + nexpv; vers++) // loop expired file versions v.4.0 - { - setFileVersion(bfile,vers); - err = lstat64(bfile,&filestat); // check file exists on backup - if (err) continue; - fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // age in days, size in MB - age = fage; - mb1 = filestat.st_size; - wprintf(mLog," %5d %5d %8s %s \n",vers,age,formatKBMB(mb1,3),Brec[ii].file); - } - } - - goto report_exit; - } - - // list all files in backup job set - - if (strEqu(menu, "list disk files")) - { - wprintx(mLog,0," List all files in backup file set: \n",boldfont); - - dGetFiles(); - wprintf(mLog," %d files found \n",Dnf); - - for (ii = 0; ii < Dnf; ii++) - wprintf(mLog," %s \n",Drec[ii].file); - - goto report_exit; - } - - // list all files on backup - - if (strEqu(menu, "list backup files")) - { - wprintx(mLog,0," List all files at backup location \n",boldfont); - if (bGetFiles() < 0) goto report_exit; - - for (ii = 0; ii < Bnf; ii++) - { - if (Brec[ii].lover) wprintf(mLog," %s (vers %d-%d) \n", - Brec[ii].file, Brec[ii].lover, Brec[ii].hiver); - else wprintf(mLog," %s \n",Brec[ii].file); - } - - goto report_exit; - } - - // search disk and backup file list for match with wild search pattern - - if (strEqu(menu, "find files")) - { - wprintx(mLog,0," Find files matching wildcard pattern \n",boldfont); - - dGetFiles(); - bGetFiles(); - if (!(Dnf + Bnf)) goto report_exit; - - fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:","/dir*/file* "); - if (! fspec1) goto report_exit; - strncpy0(fspec2,fspec1,199); - zfree(fspec1); - strTrim(fspec2); - if (! *fspec2) goto report_exit; - - wprintf(mLog,"\n matching disk files: \n"); - - for (ii = 0; ii < Dnf; ii++) - if (MatchWild(fspec2,Drec[ii].file) == 0) - wprintf(mLog," %s \n",Drec[ii].file); - - wprintf(mLog,"\n matching backup files: \n"); - - for (ii = 0; ii < Bnf; ii++) - { - if (MatchWild(fspec2,Brec[ii].file) == 0) { - if (Brec[ii].hiver) wprintf(mLog," %s (vers %d-%d) \n", - Brec[ii].file, Brec[ii].lover, Brec[ii].hiver); - else wprintf(mLog," %s \n",Brec[ii].file); - } - } - - goto report_exit; - } - -report_exit: - wprintf(mLog," ready \n"); // v.3.6 - return 0; -} - - -// edit dialog for file restore - -int RJedit_fchooser(cchar *dirk); -zdialog *RJedit_fchooser_zd = 0; - -int RJedit(cchar *menu) -{ - int RJedit_dialog_event(zdialog *zd, cchar *event); - - zdialog *zd; - - wprintf(mLog,"\n Restore files from backup \n"); - - if (bGetFiles() < 0) return 0; // get files in backup location - wprintf(mLog," %d backup files found \n",Bnf); - if (! Bnf) return 0; - - zd = zdialog_new("restore files from backup",mWin,"browse","done","cancel",null); - zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); - zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog"); - zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5|expand"); - zdialog_add_widget(zd,"label","labfrom","vb1","copy-from backup"); // copy-from backup [_____________] - zdialog_add_widget(zd,"label","labto","vb1","copy-to disk"); // copy-to disk [_____________] - zdialog_add_widget(zd,"entry","entfrom","vb2",RJfrom); - zdialog_add_widget(zd,"entry","entto","vb2",RJto); - zdialog_add_widget(zd,"hsep","hsep1","dialog"); - zdialog_add_widget(zd,"label","labf","dialog","files to restore"); // files to restore - zdialog_add_widget(zd,"frame","framef","dialog",0,"expand"); // scrolling edit window - zdialog_add_widget(zd,"scrwin","scrf","framef"); - zdialog_add_widget(zd,"edit","editf","scrf"); - - editwidget = zdialog_widget(zd,"editf"); - - for (int ii = 0; ii < RJnnx; ii++) // get restore include/exclude recs, - { // pack into file selection edit box - if (RJrtype[ii] == 2) - wprintf(editwidget,"include %s\n",RJfspec[ii]); - if (RJrtype[ii] == 3) - wprintf(editwidget,"exclude %s\n",RJfspec[ii]); - } - - zdialog_resize(zd,400,400); - zdialog_run(zd,RJedit_dialog_event,"20/20"); // run dialog posn v.4.1 - zdialog_wait(zd); - return 0; -} - - -// dialog completion function -// get restore job data from dialog widgets and validate - -int RJedit_dialog_event(zdialog *zd, cchar *event) -{ - DIR *pdirk; - char *pp, *fspec, rdirk[300]; - int ftf = 1, cc, rtype, nerrs = 0; - int zstat, days, vers; - const char *errmess = 0; - - zstat = zd->zstat; - if (! zstat) return 0; // wait for dialog end - - zd->zstat = 0; // this dialog continues - - if (RJedit_fchooser_zd) // kill file chooser dialog if active - zdialog_free(RJedit_fchooser_zd); - - if (zstat == 1) - { // browse button, file-chooser dialog - zdialog_fetch(zd,"entfrom",RJfrom,299); // copy-from location /dirk/xxx/.../ - strTrim(RJfrom); - strcpy(rdirk,BJdirk); // start at /media/xxx/dirk/xxx/ - strncat(rdirk,RJfrom,299); - RJedit_fchooser(rdirk); // do file chooser dialog - return 0; - } - - if (zstat != 1 && zstat != 2) { // cancel or destroy - zdialog_free(zd); - return 0; - } - - RJreset(); // edit done, reset job data - - zdialog_fetch(zd,"entfrom",RJfrom,299); // copy-from location /dirk/xxx/.../ - strTrim(RJfrom); - - strcpy(rdirk,BJdirk); // validate copy-from location - strncat(rdirk,RJfrom,299); // /media/xxx/dirk/... - pdirk = opendir(rdirk); - if (! pdirk) { - zmessageACK(0,"invalid copy-from location"); // v.4.1 - nerrs++; - } - else closedir(pdirk); - - cc = strlen(RJfrom); // insure '/' at end - if (RJfrom[cc-1] != '/') strcat(RJfrom,"/"); - - zdialog_fetch(zd,"entto",RJto,299); // copy-to location /dirk/yyy/.../ - strTrim(RJto); - - pdirk = opendir(RJto); // validate copy-to location - if (! pdirk) { - zmessageACK(0,"invalid copy-to location"); // v.4.1 - nerrs++; - } - else closedir(pdirk); - - cc = strlen(RJto); // insure '/' at end - if (RJto[cc-1] != '/') strcat(RJto,"/"); - - for (RJnnx = 0; RJnnx < maxnx; RJnnx++) // include/exclude recs from edit box - { - pp = wscanf(editwidget,ftf); // next record from edit widget - if (! pp) break; - wprintf(mLog," %s \n",pp); - - errmess = parseNXrec(pp,rtype,fspec,days,vers); // validate include/exclude rec. - if (errmess) { - zmessageACK(0,"%s \n %s",pp,errmess); // v.4.1 - nerrs++; - } - - RJrtype[RJnnx] = rtype; // save job record - RJfspec[RJnnx] = fspec; - } - - if (RJnnx == maxnx) { - zmessageACK(0,"max job records exceeded"); - nerrs++; - } - - if (nerrs == 0) RJvalid = 1; - - if (RJvalid) { // all OK - rGetFiles(); // get files to restore - zdialog_free(zd); // destroy dialog - } - else zd->zstat = 0; // errors, keep dialog open v.4.1 - - return 0; -} - - -// file chooser dialog for restore job edit - -int RJedit_fchooser(cchar *dirk) // v.3.5 -{ - int RJedit_fchooser_event(zdialog *zd, const char *event); - - RJedit_fchooser_zd = zdialog_new("Choose Files to Restore",mWin,"Done",null); - zdialog *zd = RJedit_fchooser_zd; - - zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand"); - zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); - zdialog_add_widget(zd,"label","space","hb1",0,"expand"); - zdialog_add_widget(zd,"check","hidden","hb1","show hidden","space=5"); - zdialog_add_widget(zd,"button","incl","hb1","include","space=5"); - zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5"); - - fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); - GtkWidget *frame = zdialog_widget(zd,"fr1"); - gtk_container_add(GTK_CONTAINER(frame),fc_widget); - - gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); - gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); - - zdialog_resize(zd,550,500); - zdialog_run(zd,RJedit_fchooser_event,"50/50"); // posn v.4.1 - zdialog_wait(zd); - zdialog_free(zd); - RJedit_fchooser_zd = 0; - return 0; -} - - -int RJedit_fchooser_event(zdialog *zd, const char *event) -{ - GSList *flist = 0; - struct stat64 filestat; - char *file1, *file2, rdirk[300]; - int ii, rdcc, err; - - if (strEqu(event,"hidden")) { // show/hide hidden files v.3.7.1 - zdialog_fetch(zd,"hidden",ii); - gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),ii); - } - - if (strEqu(event,"incl") || strEqu(event,"excl")) // include or exclude - { - strcpy(rdirk,BJdirk); // copy-from location v.3.9 - strncat(rdirk,RJfrom,299); // /media/xxx/dirk/... - rdcc = strlen(rdirk); - - flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); - - for (ii = 0; ; ii++) // process selected files - { - file1 = (char *) g_slist_nth_data(flist,ii); - if (! file1) break; - - if (! strnEqu(rdirk,file1,rdcc)) { // check file in backup location v.3.9 - zmessageACK(0,"not within copy-from directory: \n %s",file1); // v.4.1 - continue; - } - - err = lstat64(file1,&filestat); - if (err) { - zmessageACK(0,"error: %s file: \n %s",strerror(errno),file1); - continue; - } - - file2 = strdupz(file1,2); // extra space for wildcard - g_free(file1); - - if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard - - if (strEqu(event,"incl")) - wprintf(editwidget,"include %s""\n",file2 + BJdcc); // omit backup mount point - if (strEqu(event,"excl")) - wprintf(editwidget,"exclude %s""\n",file2 + BJdcc); - zfree(file2); - } - - gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); - g_slist_free(flist); - } - - return 0; -} - - -// list and validate backup files to be restored - -int RJlist(cchar *menu) -{ - int cc1, cc2, errs = 0; - char *file1, file2[maxfcc]; - - if (! RJvalid) wprintf(mLog," *** restore job has errors \n"); - if (! Rnf) goto rjlist_exit; - - wprintf(mLog,"\n copy %d files from backup: %s \n",Rnf, RJfrom); - wprintf(mLog," to directory: %s \n",RJto); - wprintf(mLog,"\n resulting files will be the following: \n"); - - cc1 = strlen(RJfrom); // from: /dirk/xxx/.../ - cc2 = strlen(RJto); // to: /dirk/yyy/.../ - - for (int ii = 0; ii < Rnf; ii++) - { - file1 = Rrec[ii].file; - - if (! strnEqu(file1,RJfrom,cc1)) { - wprintf(mLog," *** not within copy-from: %s \n",file1); - errs++; - continue; - } - - strcpy(file2,RJto); - strcpy(file2+cc2,file1+cc1); - wprintf(mLog," %s \n",file2); - } - - if (errs) { - wprintf(mLog," *** %d errors \n",errs); - RJvalid = 0; - } - -rjlist_exit: - wprintf(mLog," ready \n"); // v.3.6 - return 0; -} - - -// restore files based on data from restore dialog - -int Restore(cchar *menu) -{ - int ii, nn, ccf; - char dfile[maxfcc]; - const char *errmess = 0; - - if (! RJvalid || ! Rnf) { - wprintf(mLog," *** restore job has errors \n"); - goto restore_exit; - } - - nn = zmessageYN(mWin,"Restore %d files from: %s%s \n to: %s \n" - "Proceed with file restore ?",Rnf,BJdirk,RJfrom,RJto); - if (! nn) goto restore_exit; - - snprintf(dfile,maxfcc-2,"\n""begin restore of %d files to: %s \n",Rnf,RJto); - wprintx(mLog,0,dfile,boldfont); - - gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 - - ccf = strlen(RJfrom); // from: /media/xxx/filespec - - for (ii = 0; ii < Rnf; ii++) - { - strcpy(dfile,RJto); // to: /destination/filespec - strcat(dfile,Rrec[ii].file + ccf); - wprintf(mLog," %s \n",dfile); - errmess = copyFile(Rrec[ii].file,dfile,1); - if (errmess) wprintf(mLog," *** %s \n",errmess); - else Rrec[ii].finc = 1; - if (checkKillPause()) goto restore_exit; - } - - synch_poop("restore"); // synch owner and permissions data - -restore_exit: - wprintf(mLog," ready \n"); // v.3.6 - killFlag = 0; - gdk_window_set_cursor(mLogwin,0); // normal cursor - return 0; -} - - -// format disk backup device with vfat or ext2 file system -// uses existing partitions only - no changes to partition table // v.3.3.1 - -int Format(cchar *menu) -{ - int ii, jj, zstat, yn, contx = 0; - char text[200], device[20], filesys[20], label[20], *crec; - zdialog *zd; - FILE *fid; - - wprintf(mLog,"\n Format a backup device \n"); - - zd = zdialog_new("format backup device",mWin,"start","cancel",null); - zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); - zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog"); - zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand"); // backup device [________][v] - zdialog_add_widget(zd,"label","labdev","vb1"," backup device"); // device label [________] - zdialog_add_widget(zd,"comboE","entdev","vb2"); // file system [________][v] - zdialog_add_widget(zd,"label","lablab","vb1"," device label"); - zdialog_add_widget(zd,"entry","entlab","vb2","ukopp"); - zdialog_add_widget(zd,"label","labfs","vb1"," file system"); - zdialog_add_widget(zd,"comboE","entfs","vb2","ext2"); - - unmount(0); // unmount mounted device - BDpoop(); // refresh available devices - - for (ii = 0; ii < Ndisk; ii++) // load combo box with device - { - strcpy(text,diskdev[ii]); // /dev/xxxx description - strncatv(text,199," ",diskdesc[ii],null); - zdialog_cb_app(zd,"entdev",text); - } - - zdialog_cb_app(zd,"entfs","ext2"); // load combo box with file systems - zdialog_cb_app(zd,"entfs","vfat"); - - zdialog_resize(zd,300,0); - zdialog_run(zd); // run dialog - zstat = zdialog_wait(zd); - zdialog_free(zd); - if (zstat != 1) return 0; - - zdialog_fetch(zd,"entdev",device,19); // get chosen device and file system - zdialog_fetch(zd,"entfs",filesys,19); - zdialog_fetch(zd,"entlab",label,19); - - for (ii = 1; device[ii] > ' '; ii++); // strip off device description - if (ii > 19) ii = 19; - device[ii] = 0; - - yn = zmessageYN(mWin,"device: %s label: %s file sys: %s \n" - "WARNING: all data will be lost! \n" - "Proceed with formatting?",device,label,filesys); - if (! yn) goto format_exit; - - wprintf(mLog," formatting %s with file system %s \n",device,filesys); - - fid = fopen(TFformatscript,"w"); - if (! fid) { - wprintf(mLog," *** cannot create format script file \n"); - goto format_exit; - } - - gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 - - fprintf(fid,"umount %s \n",device); // unmount /dev/xxxx - fprintf(fid,"sleep 2 \n"); - if (*filesys == 'v') - fprintf(fid,"mkfs -t vfat -F 32 -n %s %s \n",label,device); // make vfat file system - if (*filesys == 'e') - fprintf(fid,"mkfs -t ext2 -L %s %s \n",label,device); // or ext2 file system - fprintf(fid,"exit 0 \n"); - fclose(fid); - chmod(TFformatscript,0744); - - while ((crec = command_output(contx,TFformatscript))) // v.3.3.1 - { - zsleep(0.1); // throttle a little - for (ii = jj = 0; crec[jj]; jj++) - { // get rid of weird characters - if (crec[jj] < ' ') continue; // in mkfs output - crec[ii] = crec[jj]; - ii++; - } - crec[ii] = 0; - wprintf(mLog," format: %s \n",crec); // print command output - } - -format_exit: - wprintf(mLog," ready \n"); // v.3.6 - gdk_window_set_cursor(mLogwin,0); // normal cursor - return 0; -} - - -// display help/about or help/contents - -int helpFunc(cchar *menu) -{ - if (strEqu(menu,"about")) { - wprintf(mLog," %s \n",ukopp_title); - wprintf(mLog," free software: %s \n",ukopp_license); - } - - if (strEqu(menu,"contents")) showz_userguide(); - return 0; -} - - -// Mount target device. Return 1 if success, else 0. -// menu caller: menu arg is present -// internal caller: menu arg is 0 - -int mount(cchar *menu) // more error checking v.3.5 -{ - int ii, err, cc; - char ch, work[300]; - const char *errmess; - struct stat statb; - - bFilesReset(); // clear file data at backup location - BDpoop(); // refresh device data - - snprintf(work,299,"%s %s",BJdev,BJdirk); - errmess = parseTarget(work); // target device and directory - if (errmess) { // in conflict with current - wprintf(mLog," *** %s \n",errmess); // mount status - return 0; - } - - for (ii = 0; ii < Ndisk; ii++) // see if device is mounted - if (strEqu(BJdev,diskdev[ii])) break; - if (ii < Ndisk && *diskmp[ii] == '/') { // yes - cc = strlen(diskmp[ii]); - if (strnEqu(diskmp[ii],BJdirk,cc)) { - ch = BJdirk[cc]; - if (! ch || ch == '/') { - devMounted = 1; // target directory is on device - if (menu) wprintf(mLog," already mounted \n"); - return 1; - } - } - wprintf(mLog," *** target directory not on device \n"); - } - - err = stat(BJdirk,&statb); // directory exists? - if (err && *BJdev) { // device but no directory - snprintf(work,299,"mkdir -p %s",BJdirk); // create mount point - err = do_shell("mkdir",work); - if (err) return 0; - ukoppMpoint++; // remember created by me - } - - if (! err && ! *BJdev) return 1; // no device, directory OK, use it - - snprintf(work,299,"mount -noatime %s %s",BJdev,BJdirk); // mount device at target directory - err = do_shell("mount",work); - if (err) return 0; - - ukoppMounted++; // remember mounted by me - devMounted = 1; - strcpy(mountdev,BJdev); // save mount poop - strcpy(mountdirk,BJdirk); - return 1; -} - - -// unmount target device - -int unmount(cchar *menu) // revised v.3.5.1 -{ - int err; - char work[200]; - struct stat statb; - - bFilesReset(); // no files at backup location - - sleep(1); - if (*mountdirk) snprintf(work,199,"umount %s",mountdirk); // unmount unconditionally - else snprintf(work,199,"umount %s",BJdev); - do_shell("umount",work); - - sleep(1); - err = stat(mountdirk,&statb); // remove directory - if (! err && ukoppMpoint) { // only if it exists - snprintf(work,199,"rmdir %s",mountdirk); // and created by me - do_shell("rmdir",work); - } - - devMounted = ukoppMounted = ukoppMpoint = 0; - *mountdev = *mountdirk = 0; - BDpoop(); // refresh device data - return 0; -} - - -// save logging window as text file - -int saveScreen(cchar *menu) -{ - wfilesave(mLog); - return 0; -} - - -// backup helper function -// write date and time to temp file - -int writeDT() -{ - time_t dt1; - char *dt2; - FILE *fid; - int cc; - - time(&dt1); - dt2 = ctime(&dt1); // get string date-time - cc = strlen(dt2); - if (cc && (dt2[cc-1] == '\n')) dt2[cc-1] = 0; // save without trailing \n - - fid = fopen(TFdatetime,"w"); - if (! fid) zappcrash("cannot open scratch file %s",TFdatetime); - - fprintf(fid,"%s \n",dt2); - fclose(fid); - return 0; -} - - -// synchronize owner and permissions data using poopfile at backup location v.26 -// - for files copied backup >> disk, set owner and permissions from poopfile -// - refresh poopfile data from disk files -// mode is "backup" "restore" or "synch" - -int synch_poop(const char *mode) -{ - int ii, err, nn, uid, gid, perms; - int cc, ccf, cct; - char file[maxfcc], file2[maxfcc]; - char dirk[maxfcc], pdirk[maxfcc]; - char *pp, poopfile[100]; - const char *errmess = 0; - FILE *fid; - struct stat64 dstat; - - if (strEqu(mode,"synch")) // set poop for updated disk files - { - strcpy(poopfile,BJdirk); - strcat(poopfile,BD_POOPFILE); - fid = fopen(poopfile,"r"); // open poopfile - if (! fid) { - wprintf(mLog," *** no owner/permissions file: %s \n",poopfile); - return 0; - } - - ii = 0; - - while (true) // read poopfile records - { - nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file); // uid, gid, perms, file or directory - if (nn == EOF) break; - if (nn != 4) continue; - - cc = strlen(file); - - while (ii < Bnf) // match poopfile file or directory - { // to backup files copied to disk - nn = strncmp(Brec[ii].file,file,cc); // (logic assumes ascii sort) - if (nn >= 0) break; - ii++; - } - - if (ii == Bnf) break; // EOL - if (nn > 0) continue; // file not in backup file list - if (Brec[ii].finc == 0) continue; // file not copied to disk - - wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file); - err = chown(file,uid,gid); - if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); - err = chmod(file,perms); - if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); - } - - fclose(fid); - } - - if (strEqu(mode,"restore")) // set poop for restored disk files - { - strcpy(poopfile,BJdirk); - strcat(poopfile,BD_POOPFILE); - fid = fopen(poopfile,"r"); - if (! fid) { - wprintf(mLog," *** no owner/permissions file: %s \n",poopfile); - return 0; - } - - ccf = strlen(RJfrom); - cct = strlen(RJto); - ii = 0; - - while (true) - { - nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file); - if (nn == EOF) break; - if (nn != 4) continue; - - cc = strlen(file); - if (cc <= ccf) continue; - - while (ii < Rnf) - { - nn = strncmp(Rrec[ii].file,file,cc); - if (nn >= 0) break; - ii++; - } - - if (ii == Rnf) break; - if (nn > 0) continue; - if (Rrec[ii].finc == 0) continue; - - strcpy(file2,RJto); // offset restore 'from' and 'to' paths - strcpy(file2 + cct, file + ccf); - - wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file2); - err = chown(file2,uid,gid); - if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); - err = chmod(file2,perms); - if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); - } - - fclose(fid); - } - - if (strEqu(mode,"backup") || strEqu(mode,"synch")) // make new poop file from disk files - { - fid = fopen(TFpoopfile,"w"); - if (! fid) zappcrash("cannot open temp file %s",TFpoopfile); - - *pdirk = 0; // no prior directory - - for (ii = 0; ii < Dnf; ii++) - { - strcpy(dirk,Drec[ii].file); // next file on disk - pp = dirk; - - while (true) // set directory owner & permissions - { - pp = strchr(pp+1,'/'); // next (last) directory level - if (! pp) break; - cc = pp - dirk + 1; // cc incl. '/' - if (strncmp(dirk,pdirk,cc) == 0) continue; // matches prior, skip - - *pp = 0; // terminate this directory level - - err = lstat64(dirk,&dstat); // get owner and permissions v.3.0 - if (err) { - wprintf(mLog," *** error: %s file: %s \n",strerror(errno),dirk); - break; - } - - dstat.st_mode = dstat.st_mode & 0777; - - fprintf(fid,"%4d:%4d %3o %s/\n", // output uid:gid perms directory/ - dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk); - - *pp = '/'; // restore '/' - } - - strcpy(pdirk,dirk); // prior = this directory - - strcpy(file,Drec[ii].file); // disk file, again - - err = lstat64(file,&dstat); // get owner and permissions v.3.0 - if (err) { - wprintf(mLog," *** error: %s file: %s \n",strerror(errno),file); - continue; - } - - dstat.st_mode = dstat.st_mode & 0777; - - fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid perms file - dstat.st_uid, dstat.st_gid, dstat.st_mode, file); - } - - fclose(fid); - - errmess = copyFile(TFpoopfile,BD_POOPFILE,2); // copy file owner/permissions file - if (errmess) wprintf(mLog," *** poopfile error: %s \n",errmess); - } - - return 0; -} - - -// get all disk files specified by include/exclude records -// save in Drec[] array - -int dGetFiles() -{ - const char *fsp, *psep2; - char *fspec, *pp, *psep1; - int ftf, wstat, cc, err, dups; - int rtype, ii, jj, st, nfiles; - int fcc, vers; - double nbytes; - struct stat64 filestat; - - dFilesReset(); - wprintx(mLog,0,"\n""generating backup file set \n",boldfont); - - for (ii = 0; ii < BJnnx; ii++) // process include/exclude recs - { - BJfiles[ii] = 0; // initz. include/exclude rec stats - BJbytes[ii] = 0.0; - - rtype = BJrtype[ii]; - - if (*BJfspec[ii] == '"') { // v.4.1 - fspec = strdupz(BJfspec[ii]+1); // unquote quoted filespec - pp = strrchr(fspec,'"'); - if (pp) *pp = 0; - } - else fspec = strdupz(BJfspec[ii]); - - if (rtype == 2 || rtype == 3) // include or exclude filespec - { - err = stat64(fspec,&filestat); - if (! err && S_ISDIR(filestat.st_mode)) { // if directory, append /* v.4.1 - cc = strlen(fspec) - 1; // (BJfspec has the extra length) - if (fspec[cc] != '/') cc++; - strcpy(fspec+cc,"/*"); - } - } - - if (rtype == 2) // include filespec - { - ftf = 1; - - while (1) - { - fsp = SearchWild(fspec,ftf); // find matching files - if (! fsp) break; - - Drec[Dnf].file = strdupz(fsp); - - err = lstat64(fsp,&filestat); // check accessibility - if (! err) { - Drec[Dnf].err = 0; - if (! S_ISREG(filestat.st_mode) && // reg. files + symlinks only v.3.0 - ! S_ISLNK(filestat.st_mode)) continue; - } - else Drec[Dnf].err = errno; // save file error status - - fcc = strlen(fsp); - psep1 = (char *) strstr(fsp+fcc-10,VSEP1); // look for file version v.3.2 - if (psep1) { // (char *) fix gcc error v.3.4.1 - vers = 0; - st = convSI(psep1+2,vers,&psep2); // if format not valid, take - if (st < 2) vers = 1; // as non-versioned file - if (strNeq(psep2,VSEP2)) vers = 0; - if (*(psep2+1)) vers = 0; // VSEP2 must be at end - if (vers) { - wprintf(mLog," *** omit versioned file: %s \n",fsp); - continue; - } - } - - Drec[Dnf].jindx = ii; // save pointer to include record - Drec[Dnf].size = filestat.st_size; // save file size - Drec[Dnf].mtime = filestat.st_mtime // save last mod time - + filestat.st_mtim.tv_nsec * nano; // (nanosec resolution) - if (err) Drec[Dnf].size = Drec[Dnf].mtime = 0; // inaccessible file - Drec[Dnf].finc = 0; // not copied yet - Drec[Dnf].bindx = -1; // no link to backup record yet v.4.0 - - BJfiles[ii]++; // count included files and bytes - BJbytes[ii] += Drec[Dnf].size; - - if (++Dnf == maxfs) { - wprintf(mLog," *** max files exceeded \n"); - goto errret; // v.4.4 - } - } - } - - if (rtype == 3) // exclude filespec - { - for (jj = 0; jj < Dnf; jj++) // check all included files (SO FAR) - { - if (! Drec[jj].file) continue; - wstat = MatchWild(fspec,Drec[jj].file); - if (wstat != 0) continue; - BJfiles[ii]--; // un-count excluded file and bytes - BJbytes[ii] -= Drec[jj].size; - zfree(Drec[jj].file); // clear file data entry - Drec[jj].file = 0; - Drec[jj].err = 0; - } - } - - zfree(fspec); // v.4.1 - } // end of include/exclude recs - - for (ii = 0; ii < Dnf; ii++) // list and remove error files - { // (after excluded files removed) - if (Drec[ii].err) { - wprintf(mLog," *** %s omit: %s \n",strerror(Drec[ii].err),Drec[ii].file); - jj = Drec[ii].jindx; - BJfiles[jj]--; // un-count file and bytes - BJbytes[jj] -= Drec[ii].size; - zfree(Drec[ii].file); - Drec[ii].file = 0; - } - } - - ii = jj = 0; // repack file arrays after deletions - while (ii < Dnf) - { - if (Drec[ii].file == 0) ii++; - else { - if (ii > jj) { - if (Drec[jj].file) zfree(Drec[jj].file); - Drec[jj] = Drec[ii]; - Drec[ii].file = 0; - } - ii++; - jj++; - } - } - - Dnf = jj; // final file count in backup set - - Dbytes = 0.0; - for (ii = 0; ii < Dnf; ii++) Dbytes += Drec[ii].size; // compute total bytes from files - - nfiles = 0; - nbytes = 0.0; - - for (ii = 0; ii < BJnnx; ii++) // compute total files and bytes - { // from include/exclude recs - nfiles += BJfiles[ii]; - nbytes += BJbytes[ii]; - } - - wprintf(mLog," disk files: %d %s \n",nfiles,formatKBMB(nbytes,3)); - - if ((nfiles != Dnf) || (Dbytes != nbytes)) { // must match - wprintf(mLog," *** bug: nfiles: %d Dnf: %d \n",nfiles,Dnf); - wprintf(mLog," nbytes: %.0f Dbytes: %.0f \n",nbytes,Dbytes); - goto errret; - } - - SortFileList((char *) Drec,sizeof(dfrec),Dnf,'A'); // sort Drec[Dnf] by Drec[].file - - for (ii = dups = 0; ii < Dnf-1; ii++) // look for duplicate files - if (strEqu(Drec[ii].file,Drec[ii+1].file)) { - wprintf(mLog," *** duplicate file: %s \n",Drec[ii].file); - dups++; - } - - if (dups) goto errret; - return 0; - -errret: - BJvalid = 0; - dFilesReset(); - return 0; -} - - -// get existing files at backup location, save in Brec[] array -// return -1 if error, else count of backup files -// -// Linux sort command: -// '.' sorts before ' ' (0x2E < 0x20, which is crazy) -// Workaround implemented. - -int bGetFiles() -{ - int gcc, fcc, err, vers, vfound, jj; - int bb, bbp, rtype, noret = 0; - int lover, hiver, retND, retNV; - char command[300], *pp, *psep1; - char bfile[maxfcc], *bfile2; - double fage; - const char *psep2; - FILE *fid; - struct stat64 filestat; - - bFilesReset(); // reset backup file list - if (! mount(0)) return 0; // validate and mount target v.3.2 - - wprintx(mLog,0,"\n""find all files at backup location \n",boldfont); - - sprintf(command,"find %s -type f -or -type l >%s",BJdirk,TFbakfiles); // backup filespecs to temp file v.3.0 - err = do_shell("find",command); - if (err) return -1; - - // read filespecs into memory and use memory sort instead of linux sort utility - // (apparently cannot do a straight ascii sort, even with LC_ALL=C) - - gcc = strlen(BD_UKOPPDIRK); // directory for ukopp special files - - fid = fopen(TFbakfiles,"r"); // read file list - if (! fid) zappcrash("cannot open scratch file %s",TFbakfiles); - - for (bb = 0; bb < maxfs; ) // loop all files at backup location - { - pp = fgets_trim(bfile,maxfcc-1,fid); // next file - if (! pp) break; // eof - - bfile2 = bfile + BJdcc; // remove backup mount point - if (strnEqu(bfile2,BD_UKOPPDIRK,gcc)) continue; - - fcc = strlen(bfile2); - if (fcc > maxfcc-BJdcc-10) { // cannot handle files near limit - wprintf(mLog," *** filespec too big, omit: %s...",bfile2); - wprintf(mLog,"\n"); - continue; - } - - err = lstat64(bfile,&filestat); // check accessibility - if (err) { - wprintf(mLog," *** %s, omit: %s",strerror(errno),bfile2); - wprintf(mLog,"\n"); - continue; - } - else if (! S_ISREG(filestat.st_mode) && // reg. files and symlinks only v.3.0 - ! S_ISLNK(filestat.st_mode)) continue; - - // build memory record for file data - - Brec[bb].file = strdupz(bfile2); // filespec - Brec[bb].err = 0; - Brec[bb].size = filestat.st_size; // file size - Brec[bb].mtime = filestat.st_mtime // last mod time - + filestat.st_mtim.tv_nsec * nano; - Brec[bb].lover = Brec[bb].hiver = Brec[bb].nexpv = 0; // no versions yet - Brec[bb].finc = 0; // no backup yet - bb++; - } - - fclose (fid); - - Bnf = bb; - wprintf(mLog," %6d backup files \n",Bnf); - - if (Bnf == maxfs) { - wprintf(mLog," *** max files exceeded \n"); - bFilesReset(); - return -1; - } - - SortFileList((char *) Brec,sizeof(bfrec),Bnf,'A'); // sort Brec[Bnf] by Brec[].file - - for (bb = 0, bbp = -1; bb < Bnf; bb++) // loop all files - { - bfile2 = Brec[bb].file; - fcc = strlen(bfile2); - - vers = 0; - psep1 = strstr(bfile2+fcc-10,VSEP1); // look for file version - if (psep1) { - err = convSI(psep1+2,vers,1,9999,&psep2); // if format not valid, - if (err > 1) vers = 0; // assume a current file (vers 0) - if (strNeq(psep2,VSEP2)) vers = 0; - if (*(psep2+1)) vers = 0; // VSEP2 must be at end v.3.2 - if (vers) *psep1 = 0; // remove version from file name - } - - if (! vers) // a current file, not prior version - { - bbp++; // add new file record - Brec[bbp] = Brec[bb]; // copy all data - } - - if (vers) // a prior version, 1-9999 - { - if (bbp > -1 && strEqu(Brec[bbp].file,bfile2)) { // look back for match with prior file - if (Brec[bbp].lover == 0) Brec[bbp].lover = vers; // set first version found - if (vers < Brec[bbp].lover) Brec[bbp].lover = vers; // (10) sorts before (9) - if (vers > Brec[bbp].hiver) Brec[bbp].hiver = vers; // track lowest and highest vers. found - zfree(bfile2); // free duplicate filespec - } - else { // version present, but no curr. file - bbp++; // add new file record - Brec[bbp] = Brec[bb]; // copy all data - Brec[bbp].err = -1; // mark file (vers 0) not present - Brec[bbp].size = Brec[bbp].mtime = 0; - Brec[bbp].lover = Brec[bbp].hiver = vers; // track prior versions present - } - } - } - - Bnf = bbp + 1; - - for (bb = 0; bb < Bnf; bb++) // loop all files at backup location - { - strcpy(bfile,BJdirk); - strcat(bfile,Brec[bb].file); - bfile2 = bfile + BJdcc; - - if (BJnnx > 0) { - for (jj = 0; jj < BJnnx; jj++) { // find matching backup include rec. - rtype = BJrtype[jj]; - if (rtype != 2) continue; - if (MatchWild(BJfspec[jj],bfile2) == 0) break; - } - if (jj == BJnnx) { // this file not in backup set - Brec[bb].retND = Brec[bb].retNV = 0; // no retention specs - noret++; - } - else { - Brec[bb].retND = BJretND[jj]; // get corresp. retention specs v.3.5 - Brec[bb].retNV = BJretNV[jj]; - } - } - - if (Brec[bb].err == 0) { - Cfiles++; // count curr. version files - Cbytes += Brec[bb].size; // and total bytes - } - - if (Brec[bb].lover == 0) continue; // no versions present - - lover = Brec[bb].lover; // version range found - hiver = Brec[bb].hiver; - retND = Brec[bb].retND; // retention days - retNV = Brec[bb].retNV; // retention versions - - if (! retND) retND = -1; // zero days retention, defeat test - vfound = 0; // versions found - - for (vers = hiver; vers >= lover; vers--) // loop file version, high to low - { // v.4.0 - setFileVersion(bfile,vers); - err = lstat64(bfile,&filestat); // check file exists on backup - if (err) { - wprintf(mLog," *** version %d missing: %s \n",vers,bfile2); - continue; - } - - vfound++; // this file, versions found - Vfiles++; // total versioned files and bytes - Vbytes += filestat.st_size; - - fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file version age in days - if (fage <= retND || vfound <= retNV) continue; // this version is to be retained - Brec[bb].nexpv++; // count expired versions v.4.0 - Pfiles++; // total expired files and bytes - Pbytes += filestat.st_size; // (to be purged) - } - } - - wprintf(mLog," %6d files not in backup set (unknown retention) \n",noret); - wprintf(mLog," %6d (%s) curr. file versions \n",Cfiles,formatKBMB(Cbytes,3)); - wprintf(mLog," %6d (%s) prior file versions \n",Vfiles,formatKBMB(Vbytes,3)); - wprintf(mLog," %6d (%s) expired prior versions \n",Pfiles,formatKBMB(Pbytes,3)); - - sprintf(command,"df -h %s",mountdirk); // v.4.2 - do_shell("target space",command); - - return Bnf; -} - - -// get all restore files specified by include/exclude records -// save in Rrec[] array - -int rGetFiles() -{ - int ii, jj, cc, rtype, wstat, ninc, nexc; - char *fspec; - - if (! RJvalid) return 0; - rFilesReset(); // clear restore files - if (bGetFiles() < 1) return 0; // get backup files - - wprintf(mLog,"\n generating restore file set \n"); - - for (ii = 0; ii < RJnnx; ii++) // process include/exclude recs - { - rtype = RJrtype[ii]; - fspec = RJfspec[ii]; - - if (rtype == 2) // include filespec - { - wprintf(mLog," include %s \n",fspec); - - for (ninc = jj = 0; jj < Bnf; jj++) // screen all files in backup loc. - { - wstat = MatchWild(fspec,Brec[jj].file); - if (wstat != 0) continue; - if (Brec[jj].err) continue; - Rrec[Rnf].file = strdupz(Brec[jj].file); // add matching files - Rrec[Rnf].finc = 0; - Rnf++; ninc++; - if (Rnf == maxfs) { - wprintf(mLog," *** max files exceeded \n"); - rFilesReset(); // v.4.4 - return 0; - } - } - - wprintf(mLog," %d files added \n",ninc); - } - - if (rtype == 3) // exclude filespec - { - wprintf(mLog," exclude %s \n",fspec); - - for (nexc = jj = 0; jj < Rnf; jj++) // check all included files (SO FAR) - { - if (! Rrec[jj].file) continue; - - wstat = MatchWild(fspec,Rrec[jj].file); - if (wstat != 0) continue; - zfree(Rrec[jj].file); // remove matching files - Rrec[jj].file = 0; - nexc++; - } - - wprintf(mLog," %d files removed \n",nexc); - } - } - - ii = jj = 0; // repack after deletions - while (ii < Rnf) - { - if (Rrec[ii].file == 0) ii++; - else - { - if (ii > jj) - { - if (Rrec[jj].file) zfree(Rrec[jj].file); - Rrec[jj].file = Rrec[ii].file; - Rrec[ii].file = 0; - } - ii++; - jj++; - } - } - - Rnf = jj; - wprintf(mLog," total file count: %d \n",Rnf); - - cc = strlen(RJfrom); // copy from: /dirk/.../ - - for (ii = 0; ii < Rnf; ii++) // get selected backup files to restore - { - if (! strnEqu(Rrec[ii].file,RJfrom,cc)) { - wprintf(mLog," *** not within copy-from; %s \n",Rrec[ii].file); - RJvalid = 0; // mark restore job invalid - continue; - } - } - - SortFileList((char *) Rrec,sizeof(rfrec),Rnf,'A'); // sort Rrec[Rnf] by Rrec[].file - return 0; -} - - -// helper function for backups and reports -// -// compare disk and backup files, set disp in Drec[] and Brec[] arrays: -// n new on disk, not on backup -// d deleted on backup, not on disk -// m modified on both, but not equal -// u unchanged on both, and equal -// v versions on backup, only prev. versions are present - -int setFileDisps() -{ - int dii, bii, comp; - char disp; - double diff; - - dii = bii = 0; - nnew = nmod = nunc = ndel = comp = 0; - Mbytes = 0.0; // total bytes, new and modified files - - while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel - { - if ((dii < Dnf) && (bii == Bnf)) comp = -1; - else if ((dii == Dnf) && (bii < Bnf)) comp = +1; - else comp = strcmp(Drec[dii].file, Brec[bii].file); - - if (comp < 0) { // unmatched disk file - Drec[dii].disp = 'n'; // new - nnew++; // count new files - Mbytes += Drec[dii].size; // accumulate Mbytes - Drec[dii].bindx = -1; // no matching backup file - dii++; - } - - else if (comp > 0) { // unmatched backup file - if (Brec[bii].err == 0) { // if current version is present, - Brec[bii].disp = 'd'; // file was deleted from disk - ndel++; // count deleted files - } - else Brec[bii].disp = 'v'; // only old versions on backup - bii++; - } - - else if (comp == 0) { // file present on disk and backup - Drec[dii].bindx = bii; // link disk to backup record v.4.0 - if (Brec[bii].err == 0) { - diff = Drec[dii].mtime - Brec[bii].mtime; // check if equal mod times - if (fabs(diff) > MODTIMETOLR) disp = 'm'; - else disp = 'u'; // yes, assume unchanged - Drec[dii].disp = Brec[bii].disp = disp; - if (disp == 'u') nunc++; // count unchanged files - if (disp == 'm') nmod++; // count modified files - if (disp == 'm') Mbytes += Drec[dii].size; // and accumulate Mbytes - } - else { - Brec[bii].disp = 'v'; // only old versions on backup - Drec[dii].disp = 'n'; // disk file is logically new - nnew++; // count new files - Mbytes += Drec[dii].size; // accumulate Mbytes - } - dii++; - bii++; - } - } - - Mfiles = nnew + nmod + ndel; - return 0; -} - - -// Sort file list in memory (disk files, backup files, restore files). -// Sort ascii sequence, or sort subdirectories in a directory before files. - -int SortFileList(char *recs, int RL, int NR, char sort) -{ - HeapSortUcomp fcompA, fcompD; // filespec compare funcs - if (sort == 'A') HeapSort(recs,RL,NR,fcompA); // ascii compare - if (sort == 'D') HeapSort(recs,RL,NR,fcompD); // special compare (directories first) - return 0; -} - -int fcompA(cchar *rec1, cchar *rec2) // ascii comparison -{ // current file (no version) sorts first - dfrec *r1 = (dfrec *) rec1; - dfrec *r2 = (dfrec *) rec2; - return strcmp(r1->file,r2->file); -} - -int fcompD(cchar *rec1, cchar *rec2) // special compare filenames -{ // subdirectories in a directory compare - dfrec *r1 = (dfrec *) rec1; // less than files in the directory - dfrec *r2 = (dfrec *) rec2; - return filecomp(r1->file,r2->file); -} - -int filecomp(cchar *file1, cchar *file2) // special compare filenames -{ // subdirectories compare before files - cchar *pp1, *pp10, *pp2, *pp20; - cchar slash = '/'; - int cc1, cc2, comp; - - pp1 = file1; // first directory level or file - pp2 = file2; - - while (true) - { - pp10 = strchr(pp1,slash); // find next slash - pp20 = strchr(pp2,slash); - - if (pp10 && pp20) { // both are directories - cc1 = pp10 - pp1; - cc2 = pp20 - pp2; - if (cc1 < cc2) comp = strncmp(pp1,pp2,cc1); // compare the directories - else comp = strncmp(pp1,pp2,cc2); - if (comp) return comp; - else if (cc1 != cc2) return (cc1 - cc2); - pp1 = pp10 + 1; // equal, check next level - pp2 = pp20 + 1; - continue; - } - - if (pp10 && ! pp20) return -1; // only one is a directory, - if (pp20 && ! pp10) return 1; // the directory is first - - comp = strcmp(pp1,pp2); // both are files, compare - return comp; - } -} - - -// reset all backup job data and free allocated memory - -int BJreset() -{ - for (int ii = 0; ii < BJnnx; ii++) - if (BJfspec[ii]) zfree(BJfspec[ii]); - - BJnnx = BJvalid = 0; - BJvmode = 0; - dFilesReset(); // reset dependent disk file data - return 0; -} - - -// reset all restore job data and free allocated memory - -int RJreset() -{ - for (int ii = 0; ii < RJnnx; ii++) - if (RJfspec[ii]) zfree(RJfspec[ii]); - - RJvalid = RJnnx = 0; - rFilesReset(); // reset dependent disk file data - return 0; -} - - -// reset all file data and free allocated memory - -int dFilesReset() -{ // disk files data - for (int ii = 0; ii < Dnf; ii++) - { - zfree(Drec[ii].file); - Drec[ii].file = 0; - } - - Dnf = 0; - Dbytes = Mbytes = 0.0; - return 0; -} - -int bFilesReset() -{ // backup files data - for (int ii = 0; ii < Bnf; ii++) - { - zfree(Brec[ii].file); - Brec[ii].file = 0; - } - - Bbytes = Bnf = 0; - Cbytes = Cfiles = 0; - Mbytes = Mfiles = 0; - Vbytes = Vfiles = 0; - Pbytes = Pfiles = 0; - return 0; -} - -int rFilesReset() -{ // restore files data - for (int ii = 0; ii < Rnf; ii++) - { - zfree(Rrec[ii].file); - Rrec[ii].file = 0; - } - - Rnf = 0; - return 0; -} - - -// Helper function to copy a file between disk and backup location. -// Owner and permissions are transferred for copied files and directories, -// but this will do nothing in case target is VFAT (Microsoft) file system. - -cchar * copyFile(cchar *sfile, cchar *dfile, int mpf) -{ - char file1[maxfcc], file2[maxfcc]; - int fid1, fid2, err, rcc, dlevs; - char *pp1, *pp2, buff[BIOCC]; - const char *errmess = 0; - struct stat64 fstat1, fstat2; - struct timeval ftimes[2]; - - *file1 = *file2 = 0; - if (mpf == 1) strcpy(file1,BJdirk); // prepend mount point if req. - strcat(file1,sfile); - if (mpf == 2) strcpy(file2,BJdirk); - strcat(file2,dfile); - - pp2 = file2; - dlevs = 0; - - while (true) { // v.25 - pp2 = strchr(pp2+1,'/'); // create missing directory levels - if (! pp2) break; // (check and create from top down) - *pp2 = 0; - err = stat64(file2,&fstat2); - if (err) { - err = mkdir(file2,0731); - if (err) return strerror(errno); - dlevs++; - } - *pp2 = '/'; - } - - while (dlevs) { // v.25 - pp1 = (char *) strrchr(file1,'/'); // for created output directories, - if (! pp1) break; // copy owner and permissions from - pp2 = (char *) strrchr(file2,'/'); // corresponding input directory - if (! pp2) break; // (measured from bottom up) - *pp1 = *pp2 = 0; // (possibly top levels not set) - err = stat64(file1,&fstat1); - if (err) return strerror(errno); - chmod(file2,fstat1.st_mode); - err = chown(file2,fstat1.st_uid,fstat1.st_gid); - if (err) wprintf(mLog,"error: %s \n",wstrerror(err)); - dlevs--; - } - - *file1 = *file2 = 0; - if (mpf == 1) strcpy(file1,BJdirk); // refresh filespecs - strcat(file1,sfile); - if (mpf == 2) strcpy(file2,BJdirk); - strcat(file2,dfile); - - err = lstat64(file1,&fstat1); // get input file attributes v.3.0 - if (err) return strerror(errno); - - if (S_ISLNK(fstat1.st_mode)) { // input file is symlink - rcc = readlink(file1,buff,maxfcc); - if (rcc < 0 || rcc > maxfcc-2) return strerror(errno); - buff[rcc] = 0; - err = symlink(buff,file2); // create output symlink - if (err) return strerror(errno); - ftimes[0].tv_sec = fstat1.st_atime; // get input file access time v.3.0 - ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000; // in microsecs. - ftimes[1].tv_sec = fstat1.st_mtime; - ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000; - lutimes(file2,ftimes); // set output file access time - return 0; - } - - fid1 = open(file1,O_RDONLY+O_NOATIME+O_LARGEFILE); // open input file - if (fid1 == -1) return strerror(errno); - - fid2 = open(file2,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file - if (fid2 == -1) { - errmess = strerror(errno); - close(fid1); - return errmess; - } - - while (true) - { - rcc = read(fid1,buff,BIOCC); // read huge blocks - if (rcc == 0) break; - if (rcc == -1) { - errmess = strerror(errno); - close(fid1); - close(fid2); - return errmess; - } - - rcc = write(fid2,buff,rcc); // write blocks - if (rcc == -1) { - errmess = strerror(errno); - close(fid1); - close(fid2); - return errmess; - } - - if (checkKillPause()) break; - } - - close(fid1); // close input file - err = fsync(fid2); // flush output file v.4.4 - if (err) return strerror(errno); - err = close(fid2); // close output file - if (err) return strerror(errno); - - err = lstat64(file1,&fstat1); // get input file attributes - if (err) return strerror(errno); - - chmod(file2,fstat1.st_mode); // copy owner and permissions - err = chown(file2,fstat1.st_uid,fstat1.st_gid); // from input to output file - if (err) wprintf(mLog,"error: %s \n",wstrerror(err)); - - ftimes[0].tv_sec = fstat1.st_atime; // get input file access time - ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000; // in microsecs. - ftimes[1].tv_sec = fstat1.st_mtime; - ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000; - utimes(file2,ftimes); // set output file access time - - return 0; -} - - -// Verify helper function -// Verify that file on backup medium is readable, return its length. -// Optionally compare backup file to disk file, byte for byte. -// returns error message or null if OK. - -cchar * checkFile(cchar *dfile, int compf, double &tcc) -{ - int vfid = 0, dfid = 0; - int err, vcc, dcc, cmperr = 0; - char vfile[maxfcc], *vbuff = 0, *dbuff = 0; - const char *errmess = 0; - double dtime, vtime; - int open_flagsV = O_RDONLY+O_NOATIME+O_LARGEFILE+O_DIRECT; // bypass cache v.3.5.2 - int open_flagsD = O_RDONLY+O_NOATIME+O_LARGEFILE; // use cache normally v.3.5.3 - struct stat64 filestat; - - tcc = 0.0; - - strcpy(vfile,BJdirk); // prepend mount point - strcat(vfile,dfile); - - lstat64(vfile,&filestat); // if symlink, check readable v.3.1 - if (S_ISLNK(filestat.st_mode)) { - vbuff = (char *) malloc(maxfcc); - vcc = readlink(vfile,vbuff,maxfcc); - if (vcc == -1) errmess = strerror(errno); - goto cleanup; - } - - if (compf) goto comparefiles; - - vfid = open(vfile,open_flagsV); // open for read, large blocks, direct I/O - if (vfid == -1) goto checkerr; - - err = posix_memalign((void**) &vbuff,512,BIOCC); // use aligned buffer - if (err) zappcrash("memory allocation failure"); - - while (1) - { - vcc = read(vfid,vbuff,BIOCC); - if (vcc == 0) break; - if (vcc == -1) { errmess = strerror(errno); break; } - tcc += vcc; // accumulate length - if (checkKillPause()) break; - } - goto cleanup; - -comparefiles: - - vfid = open(vfile,open_flagsV); // open for read, large blocks, direct I/O - if (vfid == -1) goto checkerr; - - dfid = open(dfile,open_flagsD); // disk files, use cached I/O - if (dfid == -1) goto checkerr; - - err = posix_memalign((void**) &vbuff,512,BIOCC); // use aligned buffers - if (err) zappcrash("memory allocation failure"); - err = posix_memalign((void**) &dbuff,512,BIOCC); - if (err) zappcrash("memory allocation failure"); - - while (1) - { - vcc = read(vfid,vbuff,BIOCC); // read two files - if (vcc == -1) { errmess = strerror(errno); goto cleanup; } - - dcc = read(dfid,dbuff,BIOCC); - if (dcc == -1) { errmess = strerror(errno); goto cleanup; } - - if (vcc != dcc) cmperr++; // compare buffers - if (memcmp(vbuff,dbuff,vcc)) cmperr++; - - tcc += vcc; // accumulate length - if (vcc == 0) break; - if (dcc == 0) break; - - if (checkKillPause()) break; - } - - if (vcc != dcc) cmperr++; - - if (cmperr) { // compare error - lstat64(dfile,&filestat); // v.3.0 - dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // file modified since snapshot? - lstat64(vfile,&filestat); // v.3.0 - vtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; - if (fabs(dtime-vtime) < MODTIMETOLR) errmess = "compare error"; // no, a real compare error - } - -cleanup: - if (vfid) close(vfid); - if (dfid) close(dfid); - if (vbuff) free(vbuff); - if (dbuff) free(dbuff); - return errmess; - -checkerr: // v.3.7 - errmess = strerror(errno); - if (errno == EINVAL || errno == ENOTSUP) { - if (Fgui) zmessageACK(mWin,"large block direct I/O not allowed \n %s",errmess); - else wprintf(mLog,"large block direct I/O not allowed \n %s",errmess); - } - if (errno == EPERM) { - if (Fgui) zmessageACK(mWin,"permission denied \n %s",errmess); - else wprintf(mLog,"permission denied \n %s",errmess); - } - goto cleanup; -} - - -// modify filespec to have a specified version -// 0 = no version = current version, +N = previous version -// returns cc of resulting filespec -// warning: filespec must have space for version numbers - -int setFileVersion(char *filespec, int vers) -{ - int fcc, overs, err; - char *psep1; - const char *psep2; - - fcc = strlen(filespec); - psep1 = strstr(filespec+fcc-10,VSEP1); // look for file version v.3.2 - if (psep1) { - overs = 0; - err = convSI(psep1+2,overs,&psep2); // if format not valid, take - if (err < 2) overs = 1; // as non-versioned file - if (strNeq(psep2,VSEP2)) overs = 0; - if (*(psep2+1)) overs = 0; // VSEP2 must be at end - if (overs) *psep1 = 0; - fcc = psep1 - filespec; - } - - if (vers == 0) return fcc; - - if (! psep1) psep1 = filespec + fcc; - strcpy(psep1,VSEP1); - sprintf(psep1+2,"%d",vers); - strcat(psep1+2,VSEP2); - - return fcc + strlen(psep1); -} - - -// rename a backup file to assign the next version number -// update the passed backup file data record, bakrec -// returns error message or null if all OK - -const char * setnextVersion(bfrec &bakrec) -{ - char fspec1[maxfcc], fspec2[maxfcc]; - int vers, err; - - strcpy(fspec1,BJdirk); - strcat(fspec1,bakrec.file); - strcpy(fspec2,fspec1); - - vers = bakrec.hiver + 1; - setFileVersion(fspec2,vers); - - err = rename(fspec1,fspec2); - if (err) return strerror(errno); - - bakrec.hiver = vers; - if (! bakrec.lover) bakrec.lover = vers; // v.4.0 - return null; -} - - -// purge expired file versions in backup location -// bakrec: backup file data record to use and update -// returns error message or null if all OK -// fkeep: if true, keep last version unless disk file was deleted // v.4.0 - -const char * purgeVersions(bfrec &bakrec, int fkeep) -{ - int lover, hiver, loretver, vers; - int err, vfound, vpurged; - int retND, retNV; - double fage; - char fspec[maxfcc]; - const char *mess = null; - struct stat64 filestat; - - strcpy(fspec,BJdirk); // prepend backup location - strcat(fspec,bakrec.file); - - retND = bakrec.retND; - retNV = bakrec.retNV; - lover = bakrec.lover; - hiver = bakrec.hiver; - if (! hiver) return 0; // no versions present - - if (! retND) retND = -1; // zero days retention, defeat test - - if (bakrec.disp == 'd') fkeep = 0; // file no longer in backup job - if (bakrec.disp == 'v') fkeep = 0; // or disk file deleted - - loretver = lover; // lowest retained version - vfound = 0; // actual versions found - vpurged = 0; // versions purged - - for (vers = hiver; vers >= lover; vers--) // loop file versions, high to low - { // v.4.0 - setFileVersion(fspec,vers); - err = lstat64(fspec,&filestat); // check file exists on backup - if (err) continue; - vfound++; // count versions found - - fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file age in days - - if (fage <= retND || vfound <= retNV) { // retain this version - loretver = vers; // remember lowest retained version - continue; - } - - if ((vers == hiver) && fkeep) { // retain last version v.4.0 - loretver = vers; - continue; - } - - mess = deleteFile(fspec); // purge this version - if (mess) break; - vpurged++; - } - - bakrec.nexpv = fkeep; // set 0 or 1 expired versions v.4.0 - bakrec.lover = loretver; // set new low version v.4.0 - if (vpurged == vfound) - bakrec.lover = bakrec.hiver = 0; // no versions remaining v.4.1 - return mess; -} - - -// helper function to delete a file from backup location. -// delete parent directories if they are now empty. - -cchar * deleteFile(cchar *file) -{ - int err; - char dfile[maxfcc], *pp; - - strcpy(dfile,file); - - err = remove(dfile); - if (err) return strerror(errno); - - while ((pp = (char *) strrchr(dfile,'/'))) // delete empty directory - { - *pp = 0; - err = rmdir(dfile); - if (! err) continue; // and parents ... - if (errno == ENOTEMPTY) return 0; - return strerror(errno); - } - - return 0; -} - - -// do shell command (subprocess) and echo outputs to log window -// returns command status: 0 = OK, +N = error - -int do_shell(cchar *pname, cchar *command) -{ - char buff[500], *crec; - int err, contx = 0; - - snprintf(buff,499,"\n""shell: %s \n",command); - wprintx(mLog,0,buff,boldfont); - - while ((crec = command_output(contx,command))) // bug fix: remove colon v.3.2 - { - wprintf(mLog," %s: %s \n",pname,crec); - zsleep(0.1); // throttle output a little - } - - err = command_status(contx); - if (err == 32) err = 0; // ignore Linux "broken pipe" crap - if (err) wprintf(mLog," %s status: %s \n", pname, strerror(err)); - else wprintf(mLog," OK \n"); - return err; -} - - - diff -Nru ukopp-4.4/ukopp-4.4.spec ukopp-4.7/ukopp-4.4.spec --- ukopp-4.4/ukopp-4.4.spec 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/ukopp-4.4.spec 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -# RPM spec file for ukopp - -Name: ukopp -Version: 4.4 -Release: 1 -Summary: backup with retention of file versions -Group: Utility/Archiving -Vendor: kornelix -Packager: kornelix2@googlemail.com -License: GPL3 -Source: %{name}-%{version}.tar.gz -URL: http://kornelix.squarespace.com/%{name} - -%description -Copy files to (removable) disk. Incremental: copies only files that -are newer than existing copies and is therefore quite fast. Specify -directories or files to include or exclude at any level, using a GUI. -Save specifications in a job file for repeated use. Report differences -between source and backup files at summary, directory, or file level. -Optionally retain old file versions in the backup for a specified time -or version count. Optionally verify backup by comparing with source. - -%prep -%setup -q - -%build -make - -%install -make install PREFIX=$RPM_BUILD_ROOT/usr - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%defattr(-,root,root) -/usr/bin/%{name} -/usr/share/%{name} -/usr/share/doc/%{name} -/usr/share/applications/kornelix-%{name}.desktop -/usr/share/man/man1/%{name}.1.gz - - diff -Nru ukopp-4.4/ukopp-4.7.cc ukopp-4.7/ukopp-4.7.cc --- ukopp-4.4/ukopp-4.7.cc 1970-01-01 00:00:00.000000000 +0000 +++ ukopp-4.7/ukopp-4.7.cc 2013-02-27 22:11:30.000000000 +0000 @@ -0,0 +1,4285 @@ +/************************************************************************** + ukopp - disk to disk backup and restore program + + Copyright 2007 2008 2009 2010 2011 2012 2013 Michael Cornelison + source URL: kornelix.com + contact: kornelix2@gmail.com + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***************************************************************************/ + +#include +#include +#include "zfuncs.h" + +#define ukopp_title "ukopp v.4.7" // version +#define ukopp_license "GNU General Public License v.3" + +// parameters and limits + +#define normfont "monospace 8" +#define boldfont "monospace bold 8" +#define BIOCC 512*1024 // read and write I/O buffer size +#define maxnx 200 // max include/exclude in job file +#define maxfs 500000 // max disk files, 500K v.4.4 +#define MODTIMETOLR 2.0 // tolerance for "equal" mod times v.4.1 +#define nano 0.000000001 // nanosecond +#define mega (1024*1024) // computer million +#define VSEP1 " (" // file version appendage format: +#define VSEP2 ")" // /xxxx.../filename (nnn) +#define RSEP1 " (" // file retention appendage format: +#define RSEP2 ")" // /xxxx.../filename (nn,nn) + +// special control files in backup directory + +#define BD_UKOPPDIRK "/ukopp-data" // directory for special files +#define BD_POOPFILE "/ukopp-data/poopfile" // file owner & permissions file +#define BD_JOBFILE "/ukopp-data/jobfile" // backup job file +#define BD_DATETIME "/ukopp-data/datetime" // backup date-time file + +// GTK GUI widgets + +GtkWidget *mWin, *mVbox, *mScroll, *mLog; // main window +GtkWidget *fc_widget; // file-chooser dialog widget +GtkWidget *editwidget; // edit box in file selection dialogs +GdkCursor *watchcursor; +GdkWindow *mLogwin; +PangoFontDescription *monofont; // fixed-width font + +// file scope variables + +int main_argc; // command line args +char **main_argv; + +int killFlag; // tell function to quit +int pauseFlag; // tell function to pause/resume +int menuLock; // menu lock flag +int Fgui; // flag, GUI mode or not +int clrun; // flag, command line 'run' command + +char TFbakfiles[100]; // /home/user/.ukopp/xxx temp. files +char TFjobfile[100], TFpoopfile[100]; +char TFdatetime[100], TFformatscript[100]; + +// disk devices and mount points + +char diskdev[100][40]; // /dev/xxx +char diskdesc[100][60]; // device description +char diskmp[100][60]; // mount point, /media/xxxx +int Ndisk, maxdisk = 99; // max. disks / partitions + +int devMounted = 0; // backup device mounted status +int ukoppMounted = 0; // device was mounted by me +int ukoppMpoint = 0; // mount point was made by me +char mountdev[40]; // current mount data +char mountdirk[200]; + +// backup job data + +char BJfilespec[maxfcc]; // backup job file +int BJnnx; // filespec count, 0...maxnx +int BJrtype[maxnx]; // 1/2/3 = comment/include/exclude +char *BJfspec[maxnx]; // filespec (wild) +int BJretND[maxnx]; // retention days +int BJretNV[maxnx]; // retention versions +int BJfiles[maxnx]; // count of matching disk files +double BJbytes[maxnx]; // matching files byte count +int BJvmode; // 0/1/2/3 = none/incr/full/comp +char BJdev[40] = ""; // backup device (maybe) +char BJdirk[200] = ""; // backup target directory +int BJdcc; // target directory cc +int BJvalid = 0; // backup job valid flag +int BJedited = 0; // job edited and not saved + +const char *vertype[4] = { "none","incremental","full","compare" }; // verify types + +// disk files specified in backup job + +struct dfrec { // disk file record + char *file; // file: /directory.../filename + double size; // byte count + double mtime; // mod time + int err; // fstat() status + int jindx; // index to job data BJfspec[] etc. + int bindx; // index to backup files Brec[] + int finc; // included in curr. backup + char disp; // status: new mod unch +}; + +int Dnf; // actual file count < maxfs +double Dbytes; // disk files, total bytes +dfrec Drec[maxfs]; // disk file data records + +// backup files (copies at backup location) + +struct bfrec { // backup file record + char *file; // file: /directory.../filename + double size; // byte count + double mtime; // mod time + int err; // file fstat() status + int retND; // retention days + int retNV; // retention versions + int lover, hiver; // range of previous versions + int nexpv; // no. expired versions + int finc; // included in curr. backup + char disp; // file status: del mod unch +}; + +int Bnf; // actual file count < maxfs +double Bbytes; // backup files, total bytes +bfrec Brec[maxfs]; // backup file data records + // backup file statistics: +int Cfiles; // curr. version file count +double Cbytes; // and total bytes +int Vfiles; // prior version file count +double Vbytes; // and total bytes +int Pfiles; // expired prior versions +double Pbytes; // and total bytes +int fverrs, fcerrs; // file verify and compare errors + +// disk::backup comparison data + +int nnew, ndel, nmod, nunc; // new, del, mod, unch file counts +int Mfiles; // new + mod + del file count +double Mbytes; // new + mod files, total bytes + +// restore job data + +char RJfrom[300]; // restore copy-from: /directory/.../ +char RJto[300]; // restore copy-to: /directory/.../ +int RJnnx; // filespec count, 0...maxnx +int RJrtype[maxnx]; // record type: include/exclude +char *RJfspec[maxnx]; // filespec of include/exclude +int RJvalid; // restore job valid flag + +// restore file data + +struct rfrec { // restore file record + char *file; // restore filespec: /directory.../file + int finc; // flag, file restore was done +}; + +rfrec Rrec[maxfs]; // restore file data records +int Rnf; // actual file count < maxfs + +// ukopp functions + +int initfunc(void *data); // GTK init function +void buttonfunc(GtkWidget *, cchar *menu); // process toolbar button event +void menufunc(GtkWidget *, cchar *menu); // process menu select event + +int getroot(cchar *); // get root privileges +int quit_ukopp(cchar *); // exit application +int clearScreen(cchar *); // clear logging window +int signalFunc(cchar *); // kill/pause/resume curr. function +int checkKillPause(); // test flags: killFlag and pauseFlag + +int BDpoop(); // get all devices and mount points +int chooseTarget(cchar *); // choose device and mount point + +int BJfileOpen(cchar *); // job file open dialog +int BJfileSave(cchar *); // job file save dialog +int BJload(cchar *fspec); // backup job data <<< file +int BJstore(cchar *fspec); // backup job data >>> file +int BJlist(cchar *); // backup job >>> log window +int BJedit(cchar *); // backup job edit dialog + +cchar * parseNXrec(cchar *, int &, char *&, int &, int &); // parse include/exclude record +cchar * parseTarget(cchar *); // parse target record +cchar * parseVerify(cchar *); // parse verify record + +int Backup(cchar *); // backup function +int Synch(cchar *); // synchronize function +int Verify(cchar *); // verify functions +int Report(cchar *); // report functions + +int RJedit(cchar *); // restore job edit dialog +int RJlist(cchar *); // list backup files to be restored +int Restore(cchar *); // file restore function + +int Format(cchar *); // format disk function +int helpFunc(cchar *); // help function + +int mount(cchar *); // mount target device +int unmount(cchar *); // unmount target device +int saveScreen(cchar *); // save logging window to file +int writeDT(); // write date-time to temp file +int synch_poop(const char *mode); // synch owner and permissions data + +int dGetFiles(); // generate backup files from job data +int bGetFiles(); // get backup file list +int rGetFiles(); // generate file list from restore job +int setFileDisps(); // set file disps: new del mod unch +int SortFileList(char *recs, int RL, int NR, char sort); // sort file list in memory +int filecomp(cchar *file1, cchar *file2); // compare files, directories first + +int BJreset(); // reset backup job file data +int RJreset(); // reset restore job data +int dFilesReset(); // reset disk file data and free memory +int bFilesReset(); // reset backup file data, free memory +int rFilesReset(); // reset restore file data, free memory + +cchar * copyFile(cchar *file1, cchar *file2, int mpf); // copy backup file << >> disk file +cchar * checkFile(cchar *file, int compf, double &bcc); // validate file and return length +cchar * setnextVersion(bfrec &rec); // backup file: assign next version +cchar * purgeVersions(bfrec &rec, int fkeep); // backup file: delete expired vers. +cchar * deleteFile(cchar *file); // delete backup file +int setFileVersion(char *file, int vers); // (re)set filespec version in memory +int do_shell(cchar *pname, cchar *command); // do shell command and echo outputs + +// ukopp menu table + +struct menuent { + char menu1[20], menu2[40]; // top-menu, sub-menu + int lock; // lock funcs: no run parallel + int (*mfunc)(cchar *); // processing function +}; + +#define nmenu 40 +struct menuent menus[nmenu] = { +// top-menu sub-menu lock menu-function +{ "button", "root", 1, getroot }, +{ "button", "edit job", 1, BJedit }, +{ "button", "target", 1, chooseTarget }, +{ "button", "clear", 0, clearScreen }, +{ "button", "run job", 1, Backup }, +{ "button", "mount", 1, mount }, +{ "button", "unmount", 1, unmount }, +{ "button", "pause", 0, signalFunc }, +{ "button", "resume", 0, signalFunc }, +{ "button", "kill job", 0, signalFunc }, +{ "button", "quit", 0, quit_ukopp }, +{ "File", "open job", 1, BJfileOpen }, +{ "File", "edit job", 1, BJedit }, +{ "File", "list job", 0, BJlist }, +{ "File", "save job", 0, BJfileSave }, +{ "File", "save job as", 0, BJfileSave }, +{ "File", "run job", 1, Backup }, +{ "File", "quit", 0, quit_ukopp }, +{ "Backup", "backup only", 1, Backup }, +{ "Backup", "synchronize", 1, Synch }, +{ "Verify", "incremental", 1, Verify }, +{ "Verify", "full", 1, Verify }, +{ "Verify", "compare", 1, Verify }, +{ "Report", "get disk files", 1, Report }, +{ "Report", "diffs summary", 1, Report }, +{ "Report", "diffs by directory", 1, Report }, +{ "Report", "diffs by file status", 1, Report }, +{ "Report", "diffs by file", 1, Report }, +{ "Report", "file versions", 1, Report }, +{ "Report", "expired versions", 1, Report }, +{ "Report", "list disk files", 1, Report }, +{ "Report", "list backup files", 1, Report }, +{ "Report", "find files", 1, Report }, +{ "Report", "save screen", 0, saveScreen }, +{ "Restore", "setup restore job", 1, RJedit }, +{ "Restore", "list restore files", 1, RJlist }, +{ "Restore", "restore files", 1, Restore }, +{ "Format", "format device", 1, Format }, +{ "Help", "about", 0, helpFunc }, +{ "Help", "contents", 0, helpFunc } }; + + +// ukopp main program + +int main(int argc, char *argv[]) +{ + GtkWidget *mbar, *tbar; + GtkWidget *mFile, *mBackup, *mVerify, *mReport, *mRestore; + GtkWidget *mFormat, *mHelp; + int ii; + + zinitapp("ukopp",null); // setup app directories + + clrun = 0; // no command line run command + *BJfilespec = 0; // no backup job file + Fgui = 1; // assume GUI mode + + main_argc = argc; // save command line arguments + main_argv = argv; + + for (ii = 1; ii < argc; ii++) // process command line + { + if (strEqu(argv[ii],"-nogui")) Fgui = 0; // command line mode, no GUI + else if (strEqu(argv[ii],"-job") && argc > ii+1) // -job jobfile (load only) + strcpy(BJfilespec,argv[++ii]); + else if (strEqu(argv[ii],"-run") && argc > ii+1) // -run jobfile (load and run) + { strcpy(BJfilespec,argv[++ii]); clrun++; } + else strcpy(BJfilespec,argv[ii]); // assume a job file and load it + } + + if (! Fgui) { // no GUI v.3.6 + mLog = mWin = 0; // outputs go to STDOUT + initfunc(0); // run job + if (devMounted && ukoppMounted) unmount(0); + return 0; // exit + } + + gtk_init(&argc, &argv); // GTK command line options + + mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create main window + gtk_window_set_title(GTK_WINDOW(mWin),ukopp_title); + gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_CENTER); + gtk_window_set_default_size(GTK_WINDOW(mWin),800,500); + + mVbox = gtk_box_new(VERTICAL,0); // vertical packing box + gtk_container_add(GTK_CONTAINER(mWin),mVbox); // add to main window + + mScroll = gtk_scrolled_window_new(0,0); // scrolled window + gtk_box_pack_end(GTK_BOX(mVbox),mScroll,1,1,0); // add to main window mVbox + + mLog = gtk_text_view_new(); // text edit window + gtk_container_add(GTK_CONTAINER(mScroll),mLog); // add to scrolled window + + monofont = pango_font_description_from_string(normfont); // set fixed pitch font + gtk_widget_modify_font(mLog,monofont); + + mbar = create_menubar(mVbox); // create menu bar + + mFile = add_menubar_item(mbar,"File",menufunc); // add menu bar items + add_submenu_item(mFile,"open job",menufunc); + add_submenu_item(mFile,"edit job",menufunc); + add_submenu_item(mFile,"list job",menufunc); + add_submenu_item(mFile,"save job",menufunc); + add_submenu_item(mFile,"save job as",menufunc); + add_submenu_item(mFile,"run job",menufunc); + add_submenu_item(mFile,"quit",menufunc); + mBackup = add_menubar_item(mbar,"Backup",menufunc); + add_submenu_item(mBackup,"backup only",menufunc); + add_submenu_item(mBackup,"synchronize",menufunc); + mVerify = add_menubar_item(mbar,"Verify",menufunc); + add_submenu_item(mVerify,"incremental",menufunc); + add_submenu_item(mVerify,"full",menufunc); + add_submenu_item(mVerify,"compare",menufunc); + mReport = add_menubar_item(mbar,"Report",menufunc); + add_submenu_item(mReport,"get disk files",menufunc); + add_submenu_item(mReport,"diffs summary",menufunc); + add_submenu_item(mReport,"diffs by directory",menufunc); + add_submenu_item(mReport,"diffs by file status",menufunc); + add_submenu_item(mReport,"diffs by file",menufunc); + add_submenu_item(mReport,"file versions",menufunc); + add_submenu_item(mReport,"expired versions",menufunc); + add_submenu_item(mReport,"list disk files",menufunc); + add_submenu_item(mReport,"list backup files",menufunc); + add_submenu_item(mReport,"find files",menufunc); + add_submenu_item(mReport,"save screen",menufunc); + mRestore = add_menubar_item(mbar,"Restore",menufunc); + add_submenu_item(mRestore,"setup restore job",menufunc); + add_submenu_item(mRestore,"list restore files",menufunc); + add_submenu_item(mRestore,"restore files",menufunc); + mFormat = add_menubar_item(mbar,"Format",menufunc); + add_submenu_item(mFormat,"format device",menufunc); + mHelp = add_menubar_item(mbar,"Help",menufunc); + add_submenu_item(mHelp,"about",menufunc); + add_submenu_item(mHelp,"contents",menufunc); + + tbar = create_toolbar(mVbox); // create toolbar and buttons + gtk_toolbar_set_style(GTK_TOOLBAR(tbar),GTK_TOOLBAR_BOTH); // v.4.5 + + if (getuid() > 0) + add_toolbar_button(tbar,"root","get root privileges","gtk-dialog-authentication",buttonfunc); + else + add_toolbar_button(tbar,"root","you have root privileges","root.png",buttonfunc); + + add_toolbar_button(tbar,"target","select backup device or directory","target.png",buttonfunc); + add_toolbar_button(tbar,"mount","mount target device","mount.png",buttonfunc); + add_toolbar_button(tbar,"unmount","unmount target device","unmount.png",buttonfunc); + add_toolbar_button(tbar,"edit job","edit backup job","edit.png",buttonfunc); + add_toolbar_button(tbar,"run job","run backup job","run.png",buttonfunc); + add_toolbar_button(tbar,"pause","pause running job","gtk-media-pause",buttonfunc); + add_toolbar_button(tbar,"resume","resume running job","gtk-media-play",buttonfunc); + add_toolbar_button(tbar,"kill job","kill running job","gtk-stop",buttonfunc); + add_toolbar_button(tbar,"clear","clear screen","gtk-clear",buttonfunc); + add_toolbar_button(tbar,"quit","quit ukopp","gtk-quit",buttonfunc); + + gtk_widget_show_all(mWin); // show all widgets + + G_SIGNAL(mWin,"destroy",quit_ukopp,0); // connect window destroy event + G_SIGNAL(mWin,"delete_event",quit_ukopp,0); + + watchcursor = gdk_cursor_new(GDK_WATCH); // v.4.1 + mLogwin = gtk_text_view_get_window(GTK_TEXT_VIEW(mLog), // GDK window for mLog + GTK_TEXT_WINDOW_TEXT); + + g_timeout_add(0,initfunc,0); // setup initial call from gtk_main() + gtk_main(); // process window events + return 0; +} + + +// initial function called from gtk_main() at startup + +int initfunc(void *data) +{ + int ii; + const char *home, *appdirk; + time_t datetime; + + datetime = time(0); + wprintf(mLog,"ukopp %s \n",ctime(&datetime)); // v.4.1 + + if (getuid() == 0) // v.4.1 + wprintx(mLog,0,"you have root privileges \n",boldfont); + else { + menufunc(null,"Help"); // show version and license + menufunc(null,"about"); + } + + appdirk = get_zuserdir(); + sprintf(TFbakfiles,"%s/bakfiles",appdirk); // make temp file names + sprintf(TFpoopfile,"%s/poopfile",appdirk); + sprintf(TFjobfile,"%s/jobfile",appdirk); + sprintf(TFdatetime,"%s/datetime",appdirk); + sprintf(TFformatscript,"%s/formatscript.sh",appdirk); + + menuLock = killFlag = pauseFlag = 0; // initialize controls + + BJnnx = 4; // default backup job data + for (ii = 0; ii < BJnnx; ii++) + BJfspec[ii] = zmalloc(60); + home = getenv("HOME"); // get "/home/username" + if (! home) home = "/root"; + + strcpy(BJfspec[0],"# default backup job"); // comment + sprintf(BJfspec[1],"%s/*",home); // /home/username/* + sprintf(BJfspec[2],"%s/*/Trash/*",home); // /home/username/*/Trash/* + sprintf(BJfspec[3],"%s/.thumbnails/*",home); // /home/username/.thumbnails/* + + BJrtype[0] = 1; // comment + BJrtype[1] = 2; // include + BJrtype[2] = 3; // exclude + BJrtype[3] = 3; // exclude + + BJretND[1] = BJretNV[1] = 0; // no retention specs v.3.5 + BJvmode = 0; // no verify + BJvalid = 0; // not validated + + strcpy(BJdev,""); // backup target device (maybe) + strcpy(BJdirk,"/unknown"); // backup target directory, cc + BJdcc = strlen(BJdirk); + + strcpy(RJfrom,"/home/"); // file restore copy-from location + strcpy(RJto,"/home/"); // file restore copy-to location + RJnnx = 0; // no. restore include/exclude recs + RJvalid = 0; // not validated + + BDpoop(); // find devices and mount points + + if (*BJfilespec) BJload(BJfilespec); // load command line job file + else snprintf(BJfilespec,maxfcc,"%s/ukopp.job",get_zuserdir()); // or set default job file + + if (clrun) { + menufunc(null,"File"); // run command line job file + menufunc(null,"run job"); + } + + return 0; +} + + +// process toolbar button events (simulate menu selection) + +void buttonfunc(GtkWidget *, cchar *button) +{ + char button2[20], *pp; + + strncpy0(button2,button,19); + pp = strchr(button2,'\n'); // replace \n with blank + if (pp) *pp = ' '; + + menufunc(0,"button"); + menufunc(0,button2); + return; +} + + +// process menu selection event + +void menufunc(GtkWidget *, cchar *menu) +{ + int ii; + static char menu1[20] = "", menu2[40] = ""; + char command[100]; + + for (ii = 0; ii < nmenu; ii++) + if (strEqu(menu,menus[ii].menu1)) break; // mark top-menu selection + if (ii < nmenu) { strcpy(menu1,menu); return; } + + for (ii = 0; ii < nmenu; ii++) + if (strEqu(menu1,menus[ii].menu1) && + strEqu(menu,menus[ii].menu2)) break; // mark sub-menu selection + + if (ii < nmenu) strcpy(menu2,menu); + else { // no match to menus + wprintf(mLog," *** bad command: %s \n",menu); + return; + } + + if (menuLock && menus[ii].lock) { // no lock funcs can run parallel + zmessageACK(mWin,0,"a blocking function is active"); + return; + } + + if (! menuLock) + killFlag = pauseFlag = 0; // reset controls + + snprintf(command,99,"\n""command: %s > %s \n",menu1,menu2); + wprintx(mLog,0,command,boldfont); + + if (menus[ii].lock) ++menuLock; + menus[ii].mfunc(menu2); // call menu function + if (menus[ii].lock) --menuLock; + + return; +} + + +// get root privileges if password is OK + +int getroot(cchar * menu) // v.3.8 +{ + if (getuid() == 0) // v.4.1 + wprintx(mLog,0,"\nyou have root privileges \n",boldfont); + else + beroot(main_argc-1,main_argv+1); // does not return + return 0; +} + + +// quit ukopp + +int quit_ukopp(cchar *menu) +{ + int yn; + char logfile[200]; + + if (devMounted && ukoppMounted) unmount(0); // v.3.5.2 + + if (BJedited && Fgui) { // v.4.0 + yn = zmessageYN(mWin,"job file modified, QUIT anyway?"); + if (! yn) return 1; + BJedited = 0; + } + + if (mLog) { + sprintf(logfile,"%s/ukopp.log2",get_zuserdir()); // dump window to log file + wfiledump(mLog,logfile); // v.3.9 + } + + gtk_main_quit(); // tell gtk_main() to quit + return 0; +} + + +// clear logging window + +int clearScreen(cchar *menu) +{ + wclear(mLog); + return 0; +} + + +// kill/pause/resume current function - called from menu function + +int signalFunc(cchar *menu) +{ + if (strEqu(menu,"kill job")) + { + if (! menuLock) { + wprintf(mLog,"\n""ready \n"); + return 0; + } + + if (killFlag) { + wprintf(mLog," *** waiting for function to quit \n"); + return 0; + } + + wprintf(mLog," *** KILL current function \n"); + pauseFlag = 0; + killFlag = 1; + return 0; + } + + if (strEqu(menu,"pause")) { + pauseFlag = 1; + return 0; + } + + if (strEqu(menu,"resume")) { + pauseFlag = 0; + return 0; + } + + else zappcrash("signalFunc: %s",menu); + return 0; +} + + +// check kill and pause flags +// called periodically from long-running functions + +int checkKillPause() +{ + while (pauseFlag) // idle loop while paused + { + zsleep(0.1); + zmainloop(); // process menus + } + + zmainloop(); // keep menus working v.4.0 + + if (! killFlag) return 0; // keep running + return 1; // die now and reset killFlag +} + + +// find all disk devices and mount points via Linux utilities + +int BDpoop() // v.3.3 new udevinfo format +{ + int ii, jj, contx = 0, pii, pjj, err; + int diskf, filsysf, usbf, Nth, Nmounted; + char *buff, diskdev1[40], diskdesc1[60], work[100]; + cchar *pp1, *pp2; + + Ndisk = diskf = filsysf = usbf = 0; + + err = system("udevadm --version >/dev/null 2>&1"); // keep up with dynamic Linux v.3.4 + if (! err) strcpy(work,"udevadm info -e"); // new Linux command + else strcpy(work,"udevinfo -e"); // old Linux command + + while ((buff = command_output(contx,work))) + { + if (strnEqu(buff,"P: ",3)) { // start new device + if (diskf && filsysf) { // if last device = formatted disk + strncpy0(diskdev[Ndisk],diskdev1,39); // save /dev/devid + strncpy0(diskdesc[Ndisk],diskdesc1,59); // save description + if (usbf) strcat(diskdesc[Ndisk]," (USB)"); // note if USB device + strcpy(diskmp[Ndisk],"(not mounted)"); // mount point TBD + Ndisk++; + if (Ndisk == maxdisk) { + wprintf(mLog," *** exceeded %d devices \n",maxdisk); + break; + } + } + + diskf = filsysf = usbf = 0; // clear new device flags + } + + if (strnEqu(buff,"N: ",3)) { + strcpy(diskdev1,"/dev/"); + strncat(diskdev1,buff+3,14); // save /dev/devid + } + + if (strnEqu(buff,"E: ",3)) { + pp1 = strstr(buff,"ID_TYPE=disk"); + if (pp1) diskf = 1; // device is a disk + pp1 = strstr(buff,"ID_FS_TYPE="); + if (pp1) filsysf = 1; // device has a file system + pp1 = strstr(buff,"ID_BUS=usb"); + if (pp1) usbf = 1; // device is a USB device + pp1 = strstr(buff,"ID_MODEL="); + if (pp1) strncpy0(diskdesc1,pp1+9,59); // save description + } + } + + if (! Ndisk) { + wprintf(mLog," no devices found \n"); + return 0; + } + + contx = Nmounted = 0; + + while ((buff = command_output(contx,"cat /proc/mounts"))) // get mounted disk info v.3.2 + { + if (strnNeq(buff,"/dev/",5)) continue; // not a /dev/xxx record + + Nth = 1; + pp1 = strField(buff,' ',Nth++); // parse /dev/xxx /media/xxx + pp2 = strField(buff,' ',Nth++); + + for (ii = 0; ii < Ndisk; ii++) // look for matching device + { + if (strNeq(pp1,diskdev[ii])) continue; + strncpy0(diskmp[ii],pp2,59); // copy its mount point + strTrim(diskmp[ii]); + Nmounted++; + break; + } + } + + #define swap(name,ii,jj) { \ + strcpy(work,name[ii]); \ + strcpy(name[ii],name[jj]); \ + strcpy(name[jj],work); } + + for (ii = 0; ii < Ndisk; ii++) // sort USB and mounted devices + for (jj = ii + 1; jj < Ndisk; jj++) // to the top of the list + { + pii = pjj = 0; + if (strstr(diskdesc[ii],"(USB)")) pii += 2; + if (! strEqu(diskmp[ii],"(not mounted)")) pii += 1; + if (strstr(diskdesc[jj],"(USB)")) pjj += 2; + if (! strEqu(diskmp[jj],"(not mounted)")) pjj += 1; + if (pjj > pii) { + swap(diskdev,jj,ii); + swap(diskmp,jj,ii); + swap(diskdesc,jj,ii); + } + } + + return Nmounted; +} + + +// choose backup device or enter a target directory +// update backup job target device and directory + +int chooseTarget(cchar *) // overhauled v.3.2 +{ + int ii, zstat; + char text[300]; + zdialog *zd; + cchar *instruct = "Select target device or directory"; + const char *errmess = 0; + + BDpoop(); // refresh available devices + + zd = zdialog_new("Choose Backup Target",mWin,"OK","cancel",null); + zdialog_add_widget(zd,"vbox","vb1","dialog",0,"space=10"); + zdialog_add_widget(zd,"label","lab1","vb1",instruct); // select backup device ... + zdialog_add_widget(zd,"comboE","target","vb1",BJdirk); // [_______________________][v] + + for (ii = 0; ii < Ndisk; ii++) + { // load combo box with device poop + strcpy(text,diskdev[ii]); // /dev/xxx /media/xxx (description) + strncatv(text,299," ",diskmp[ii]," (",diskdesc[ii],")",null); + zdialog_cb_app(zd,"target",text); + } + + zdialog_resize(zd,300,0); // v.4.1 + zdialog_run(zd,0,"mouse"); // run dialog posn v.4.1 + zstat = zdialog_wait(zd); + if (zstat != 1) { + zdialog_free(zd); + return 0; + } + + zdialog_fetch(zd,"target",text,299); // get device or target directory + + zdialog_free(zd); // kill dialog + + errmess = parseTarget(text); // parse selected device, directory + wprintf(mLog," new target: %s %s \n",BJdev,BJdirk); + if (errmess) wprintf(mLog," *** %s \n",errmess); + + BJedited++; // v.4.1 + return 0; +} + + +// job file open dialog - get backup job data from a file +// return 1 if OK, else 0 + +int BJfileOpen(cchar *menu) +{ + char *file; + + file = zgetfile1("open backup job","open",BJfilespec,"hidden"); // get file from user + if (file) { + strncpy0(BJfilespec,file,maxfcc-2); + zfree(file); + BJload(BJfilespec); // load job file, set BJvalid + } + + return 0; +} + + +// job file save dialog - save backup job data to a file +// return 1 if OK, else 0 + +int BJfileSave(cchar *menu) +{ + char *file; + int yn; + + if (! BJvalid && Fgui) { + yn = zmessageYN(mWin,"backup job has errors, save anyway?"); // v.3.5 + if (! yn) return 0; + } + + if (strEqu(menu,"save job")) { + BJstore(BJfilespec); + return 0; + } + + file = zgetfile1("save backup job","save",BJfilespec,"hidden"); + if (file) { + strncpy0(BJfilespec,file,maxfcc-2); + zfree(file); + BJstore(BJfilespec); + } + + return 0; +} + + +// backup job data <<< jobfile + +int BJload(cchar *jobfile) +{ + FILE *fid; + char *pp, *fspec, buff[1000]; + const char *errmess, *jobname; + int rtype, days, vers, nerrs = 0; + + snprintf(buff,999,"\n""loading job file: %s \n",jobfile); + wprintx(mLog,0,buff,boldfont); + + fid = fopen(jobfile,"r"); // open job file + if (! fid) { + wprintf(mLog," *** cannot open job file: %s \n",jobfile); + return 0; + } + + BJreset(); // reset all job data + + while (true) + { + pp = fgets_trim(buff,999,fid,1); // read next job record + if (! pp) break; // EOF + + wprintf(mLog," %s \n",buff); // output + + if (strnEqu(pp,"target",6)) { + errmess = parseTarget(buff); // target /dev/xxx /xxxxxxx + if (errmess) wprintf(mLog," *** %s \n",errmess); + continue; + } + + if (strnEqu(pp,"verify",6)) { + errmess = parseVerify(buff); // verify xxxxxx + if (errmess) wprintf(mLog," *** %s \n",errmess); + if (errmess) nerrs++; + continue; + } + + errmess = parseNXrec(buff,rtype,fspec,days,vers); // comment/include/exclude + if (errmess) wprintf(mLog," *** %s \n",errmess); + if (errmess) nerrs++; + BJfspec[BJnnx] = fspec; + BJrtype[BJnnx] = rtype; + BJretND[BJnnx] = days; + BJretNV[BJnnx] = vers; + BJnnx++; + + if (BJnnx == maxnx) { + wprintf(mLog," *** max job records exceeded \n"); + nerrs++; + break; + } + } + + fclose(fid); // close file + + if (nerrs == 0) { + BJvalid = 1; // job valid if no errors + jobname = strrchr(BJfilespec,'/') + 1; + snprintf(buff,100,"%s %s",ukopp_title,jobname); // put job name in window title v.4.0 + if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff); + BJedited = 0; + } + + return 1; +} + + +// backup job data >>> jobfile +// return 1 if OK, else 0 + +int BJstore(cchar *jobfile) +{ + FILE *fid; + char buff[100]; + cchar *jobname; + + fid = fopen(jobfile,"w"); // open file + if (! fid) { + wprintf(mLog," *** cannot open job file: %s \n",jobfile); + return 0; + } + + for (int ii = 0; ii < BJnnx; ii++) + { + if (BJrtype[ii] == 1) + fprintf(fid,"%s \n",BJfspec[ii]); // comment + + if (BJrtype[ii] == 2) { + if (BJretND[ii] + BJretNV[ii] > 0) // include /filespec (nd,nv) + fprintf(fid,"include %s%s%d,%d%s\n", + BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); + else fprintf(fid,"include %s\n",BJfspec[ii]); + } + + if (BJrtype[ii] == 3) // exclude /filespec + fprintf(fid,"exclude %s\n",BJfspec[ii]); + } + + fprintf(fid,"verify %s \n",vertype[BJvmode]); // verify xxxx + fprintf(fid,"target %s %s \n",BJdev,BJdirk); // target /dev/xxx /xxxxxxx + fclose(fid); + + if (strNeq(jobfile,TFjobfile)) { // if not job file in temp storage, + jobname = strrchr(jobfile,'/') + 1; + snprintf(buff,100,"%s %s",ukopp_title,jobname); // put job name in window title v.4.0 + if (Fgui) gtk_window_set_title(GTK_WINDOW(mWin),buff); + BJedited = 0; + } + + return 1; +} + + +// list backup job data to log window + +int BJlist(cchar *menu) +{ + wprintf(mLog,"\n backup job file: %s \n",BJfilespec); // job file v.21 + + for (int ii = 0; ii < BJnnx; ii++) + { + if (BJrtype[ii] == 1) // comment + wprintf(mLog," %s \n",BJfspec[ii]); + + if (BJrtype[ii] == 2) { // include /filespec (nd,nv) + if (BJretND[ii] + BJretNV[ii] > 0) + wprintf(mLog," include %s%s%d days, %d vers%s \n", + BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); + else wprintf(mLog," include %s \n",BJfspec[ii]); + } + + if (BJrtype[ii] == 3) // exclude /filespec + wprintf(mLog," exclude %s \n",BJfspec[ii]); + } + + wprintf(mLog," verify %s \n",vertype[BJvmode]); // verify xxxx + wprintf(mLog," target %s %s \n",BJdev,BJdirk); // target /dev/xxx /xxxxxx + + return 0; +} + + +// edit dialog for backup job data + +int BJedit_fchooser(cchar *dirk); +zdialog *BJedit_fchooser_zd = 0; + +char orgBJdev[40]; // v.4.1 +char orgBJdirk[200]; + +int BJedit(cchar *menu) +{ + int BJedit_dialog_event(zdialog *zd, const char *event); + + zdialog *zd; + char text[300]; + + zd = zdialog_new("Edit Backup Job",mWin,"browse","clear","done","cancel",null); + + zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=4"); // target: /dev/xxx /xxxxx [choose] + zdialog_add_widget(zd,"label","labtarg","hb1","backup target: "); // v.3.5 + zdialog_add_widget(zd,"label","target","hb1","/dev/xxx /xxxxxx"); + zdialog_add_widget(zd,"button","choosetarg","hb1","choose target"); + + zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=4"); // verify: (o) none (o) incr (o) ... + zdialog_add_widget(zd,"label","labverify","hb2","verify method: "); // v.3.5 + zdialog_add_widget(zd,"radio","vnone","hb2","none"); + zdialog_add_widget(zd,"radio","vincr","hb2","incremental","space=10"); + zdialog_add_widget(zd,"radio","vfull","hb2","full","space=10"); + zdialog_add_widget(zd,"radio","vcomp","hb2","compare","space=10"); + + zdialog_add_widget(zd,"hsep","sep2","dialog"); // edit box for job recs + zdialog_add_widget(zd,"label","labinex","dialog","Include / Exclude"); + zdialog_add_widget(zd,"frame","frminex","dialog",0,"expand"); + zdialog_add_widget(zd,"scrwin","scrwinex","frminex"); + zdialog_add_widget(zd,"edit","edinex","scrwinex"); + + snprintf(text,299,"%s %s",BJdev,BJdirk); // stuff current target v.3.5 + zdialog_stuff(zd,"target",text); + + strncpy0(orgBJdev,BJdev,40); // save in case of cancel v.4.1 + strncpy0(orgBJdirk,BJdirk,200); + + zdialog_stuff(zd,"vnone",0); // stuff verify mode v.3.5 + zdialog_stuff(zd,"vincr",0); + zdialog_stuff(zd,"vfull",0); + zdialog_stuff(zd,"vcomp",0); + if (BJvmode == 0) zdialog_stuff(zd,"vnone",1); + if (BJvmode == 1) zdialog_stuff(zd,"vincr",1); + if (BJvmode == 2) zdialog_stuff(zd,"vfull",1); + if (BJvmode == 3) zdialog_stuff(zd,"vcomp",1); + + editwidget = zdialog_widget(zd,"edinex"); + wclear(editwidget); // stuff include/exclude recs + + for (int ii = 0; ii < BJnnx; ii++) + { + if (BJrtype[ii] == 1) // comment + wprintf(editwidget,"%s\n",BJfspec[ii]); + + if (BJrtype[ii] == 2) { // include /filespec (nd,nv) + if (BJretND[ii] + BJretNV[ii] > 0) + wprintf(editwidget,"include %s%s%d,%d%s\n", + BJfspec[ii],RSEP1,BJretND[ii],BJretNV[ii],RSEP2); + else wprintf(editwidget,"include %s\n",BJfspec[ii]); + } + + if (BJrtype[ii] == 3) // exclude /filespec + wprintf(editwidget,"exclude %s\n",BJfspec[ii]); + } + + zdialog_resize(zd,400,400); + zdialog_run(zd,BJedit_dialog_event,"40/10"); // run dialog posn v.4.1 + zdialog_wait(zd); // wait for completion + return 0; +} + + +// job edit dialog event function + +int BJedit_dialog_event(zdialog *zd, const char *event) +{ + int rtype, days, vers, nerrs = 0; + char *pp, *fspec, text[300]; + cchar *errmess = 0, *jobname; + int zstat, nn, ftf = 1; + + if (strEqu(event,"choosetarg")) { // set new target device, directory + chooseTarget(0); + snprintf(text,299,"%s %s",BJdev,BJdirk); + zdialog_stuff(zd,"target",text); + return 0; + } + + zstat = zd->zstat; // zdialog complete? + if (! zstat) { + BJedited++; // no, manual edit was done v.4.1 + return 0; + } + + zd->zstat = 0; // dialog may continue + + if (zstat == 2) { + wclear(editwidget); // clear include/exclude recs + BJedited++; // v.4.1 + return 0; + } + + if (zstat == 1) { // browse, do file-chooser dialog + if (! BJedit_fchooser_zd) + BJedit_fchooser("/home"); + return 0; + } + + if (BJedit_fchooser_zd) // kill file chooser dialog if active + zdialog_free(BJedit_fchooser_zd); + + if (zstat != 3) { // cancel or kill + zdialog_free(zd); + strcpy(BJdev,orgBJdev); // restore original target v.4.1 + strcpy(BJdirk,orgBJdirk); + BJedited = 0; + return 0; + } + + if (! BJedited) { // done + zdialog_free(zd); // no edits made v.4.1 + return 0; + } + + BJreset(); // reset job data + + zdialog_fetch(zd,"target",text,299); // get device or target directory + wprintf(mLog," target: %s \n",text); + errmess = parseTarget(text); // v.3.5 + if (errmess) wprintf(mLog," *** %s \n",errmess); + + BJvmode = 0; + zdialog_fetch(zd,"vincr",nn); // get verify mode v.3.5 + if (nn) BJvmode = 1; + zdialog_fetch(zd,"vfull",nn); + if (nn) BJvmode = 2; + zdialog_fetch(zd,"vcomp",nn); + if (nn) BJvmode = 3; + + for (BJnnx = 0; BJnnx < maxnx; ) // get include/exclude records + { + pp = wscanf(editwidget,ftf); + if (! pp) break; + + errmess = parseNXrec(pp,rtype,fspec,days,vers); + if (errmess) { + wprintf(mLog,"%s \n *** %s \n",pp,errmess); + nerrs++; + } + + BJfspec[BJnnx] = fspec; + BJrtype[BJnnx] = rtype; + BJretND[BJnnx] = days; + BJretNV[BJnnx] = vers; + BJnnx++; // v.4.6 + } + + if (nerrs == 0) BJvalid = 1; // valid job if no errors + + jobname = strrchr(BJfilespec,'/') + 1; + snprintf(text,100,"%s %s (*)",ukopp_title,jobname); // (*) in title for edited job v.4.0 + gtk_window_set_title(GTK_WINDOW(mWin),text); + zdialog_free(zd); // destroy dialog + + return 0; +} + + +// file chooser dialog for backup job edit + +int BJedit_fchooser(cchar *dirk) // v.3.5 +{ + int BJedit_fchooser_event(zdialog *zd, const char *event); + + BJedit_fchooser_zd = zdialog_new("Choose Files for Backup",mWin,"Done",null); + zdialog *zd = BJedit_fchooser_zd; + + zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand"); + zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); + zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5"); + zdialog_add_widget(zd,"label","space","hb1",0,"expand"); + zdialog_add_widget(zd,"button","incl","hb1","include","space=5"); + zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5"); + zdialog_add_widget(zd,"check","showhf","hb1","Show hidden","space=10"); + zdialog_add_widget(zd,"label","space","hb2",0,"expand"); + zdialog_add_widget(zd,"label","lab1","hb2","Retain old files: Days: "); + zdialog_add_widget(zd,"spin","days","hb2","0|9999|1|0"); + zdialog_add_widget(zd,"label","lab2","hb2"," Versions: "); + zdialog_add_widget(zd,"spin","vers","hb2","0|9999|1|0"); + zdialog_add_widget(zd,"label","space","hb2",0,"space=5"); + + fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); + GtkWidget *frame = zdialog_widget(zd,"fr1"); + gtk_container_add(GTK_CONTAINER(frame),fc_widget); + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); + gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),1); + + zdialog_stuff(zd,"showhf",1); + zdialog_resize(zd,550,500); + zdialog_run(zd,BJedit_fchooser_event); + zdialog_wait(zd); + zdialog_free(zd); + BJedit_fchooser_zd = 0; + return 0; +} + + +int BJedit_fchooser_event(zdialog *zd, const char *event) +{ + GSList *flist = 0; + struct stat64 filestat; + char *file1, *file2; + int ii, err, showhf, days, vers; + + if (strEqu(event,"showhf")) // show/hide hidden files + { + zdialog_fetch(zd,"showhf",showhf); + gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),showhf); + } + + if (strEqu(event,"incl") || strEqu(event,"excl")) // include or exclude + { + flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); + + for (ii = 0; ; ii++) // process selected files + { + file1 = (char *) g_slist_nth_data(flist,ii); + if (! file1) break; + + file2 = strdupz(file1,2); // extra space for wildcard + g_free(file1); + + err = lstat64(file2,&filestat); + if (err) { + wprintf(mLog," *** error: %s file: %s \n",strerror(errno),file2); + continue; + } + + if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard + + zdialog_fetch(zd,"days",days); // get corresp. retention specs + zdialog_fetch(zd,"vers",vers); // from dialog + + if (strEqu(event,"incl")) { // include /filespec (dd,vv) v.3.5 + if (days || vers) + wprintf(editwidget,"include %s%s%d,%d%s\n",file2,RSEP1,days,vers,RSEP2); + else wprintf(editwidget,"include %s""\n",file2); + } + if (strEqu(event,"excl")) + wprintf(editwidget,"exclude %s""\n",file2); + + zfree(file2); + BJedited++; + } + + gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); + g_slist_free(flist); + } + + return 0; +} + + +// parse and validate a comment/include/exclude record +// filespec* means a /path.../filename with wildcards +// # comment (or blank line) +// include filespec* [ (days,vers) ] // v.3.5 +// exclude filespec* + +cchar * parseNXrec(const char *jobrec, int &rtype, char *&fspec, int &days, int &vers) +{ + int nn, Nth = 1; + const char *pp1, *pp2; + + rtype = days = vers = -1; + fspec = null; + + pp1 = strField(jobrec,' ',Nth++); + + if (! pp1 || *pp1 == '#') { // comment or blank line + rtype = 1; + if (pp1) fspec = strdupz(pp1); + else fspec = strdupz(""); + return 0; + } + + if (strEqu(pp1,"include")) { // include /filespec (nd,nv) + rtype = 2; + pp1 = jobrec + 7; + while(*pp1 == ' ') pp1++; + pp2 = strstr(pp1,RSEP1); + if (! pp2) { // assume no (nd,nv) + fspec = strdupz(pp1,4); // v.4.1 + days = vers = 0; + } + else { // parse (nd,nv) v.3.5 + nn = sscanf(pp2,RSEP1" %d , %d "RSEP2,&days,&vers); + if (nn != 2 || days < 0 || days > 9999 || vers < 0 || vers > 9999) + return "invalid retention spec, use \" (nn,nn)\" "; + fspec = strdupz(pp1,4); // v.4.1 + fspec[pp2-pp1] = 0; + } + strTrim2(fspec); // strip trailing blanks v.4.0 + pp1 = fspec; + if (*pp1 == '"') { + pp1++; // allow quoted filespec v.4.1 + pp2 = pp1 + strlen(pp1); + if (pp2[-1] != '"') return "filespec closing quote missing"; + } + if (*pp1 != '/') return "filespec missing /topdir/"; + pp1 = strchr(pp1+1,'/'); + if (!pp1) return "filespec missing /topdir/"; + pp2 = strchr(fspec,'*'); + if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed"; + pp2 = strchr(fspec,'?'); + if (pp2 && pp2 < pp1) return "wildcards in /topdir/ not allowed"; + return 0; + } + + if (strEqu(pp1,"exclude")) { // exclude /filespec + rtype = 3; + pp1 = jobrec + 7; + while(*pp1 == ' ') pp1++; + fspec = strdupz(pp1,4); // v.4.1 + strTrim2(fspec); // strip trailing blanks v.4.0 + pp1 = fspec; + if (*pp1 == '"') { // allow quoted filespec v.4.1 + pp2 = pp1 + strlen(pp1); + if (pp2[-1] != '"') return "filespec closing quote missing"; + } + return 0; + } + + return "unrecognized record type"; +} + + +// parse a verify record: verify xxxxx + +cchar * parseVerify(const char *text) // v.3.5 +{ + const char *pp; + + BJvmode = 0; + + pp = strField(text,' ',1); + if (! pp || strNeq(pp,"verify")) return "bad verify record"; + + pp = strField(text,' ',2); + if (! pp) return "missing verify type"; + + BJvmode = -1; + if (strEqu(pp,"none")) BJvmode = 0; + if (strnEqu(pp,"incr",4)) BJvmode = 1; + if (strEqu(pp,"full")) BJvmode = 2; + if (strnEqu(pp,"comp",4)) BJvmode = 3; + if (BJvmode >= 0) return 0; + + BJvmode = 0; + return "bad verify mode"; +} + + +// parse a target record and set backup device and directory accordingly +// format: [ target ] [ /dev/xxx ] [ /directory ] + +cchar * parseTarget(const char *text) // more robust v.3.5 +{ + int ii, err, cc, yn, Nth = 1; + int direxists = 0, dirempty = 0; + char ch; + const char *pp; + DIR *dirf; + struct dirent *ppd; + struct stat dstat; + + bFilesReset(); // no files at backup location + *BJdev = *BJdirk = BJdcc = 0; // reset target poop + + pp = strField(text,' ',Nth++); + + if (pp && strEqu(pp,"target")) // skip "target" + pp = strField(text,' ',Nth++); + + if (pp && strnEqu(pp,"/dev/",5)) { + strncpy0(BJdev,pp,39); // have /dev/xxxx + pp = strField(text,' ',Nth++); + } + + if (pp && *pp == '/') { + strncpy0(BJdirk,pp,199); // have /directory/... + BJdcc = strlen(BJdirk); + } + + if (! *BJdev && ! *BJdirk) return "no backup target specified"; + + BDpoop(); // refresh known device data + + if (*BJdev) { // if device is specified + for (ii = 0; ii < Ndisk; ii++) + if (strEqu(BJdev,diskdev[ii])) break; // look for device + if (ii == Ndisk) return "target device not found"; + } + + if (*BJdev && ! *BJdirk) { // get mount point for device + for (ii = 0; ii < Ndisk; ii++) + if (strEqu(BJdev,diskdev[ii])) break; + if (ii < Ndisk && *diskmp[ii] == '/') strcpy(BJdirk,diskmp[ii]); + } + + if (! *BJdev && *BJdirk) { // get device for mount point + for (ii = 0; ii < Ndisk; ii++) + if (strEqu(BJdirk,diskmp[ii])) break; + if (ii < Ndisk) strcpy(BJdev,diskdev[ii]); + } + + if (*BJdev && ! *BJdirk) { // if no directory specified, + strcpy(BJdirk,"/media"); // set a default for device + strcpy(BJdirk+6,BJdev+4); // e.g. /media/sdf1 + } + + BJdcc = strlen(BJdirk); // set target directory cc + + err = stat(BJdirk,&dstat); // determine if directory + if (! err && S_ISDIR(dstat.st_mode)) direxists = 1; // exists in file system + + if (direxists) { // determine if directory is empty + dirempty = 1; + dirf = opendir(BJdirk); + if (dirf) { + while (true) { + ppd = readdir(dirf); + if (! ppd) break; + if (ppd->d_name[0] == '.') continue; + dirempty = 0; + break; + } + closedir(dirf); + } + } + + if (direxists) { // directory exists + if (*BJdev) { // if device is specified, + for (ii = 0; ii < Ndisk; ii++) // find where it is mounted + if (strEqu(BJdev,diskdev[ii])) break; + if (ii == Ndisk || *diskmp[ii] != '/') { // device not mounted + if (dirempty) { + wprintf(mLog,"target is valid and not mounted \n"); // mount to existing empty + return 0; // directory is allowed + } + else return "target directory is not an empty directory"; // directory is not empty + } + else { // device is mounted + cc = strlen(diskmp[ii]); + if (! strnEqu(diskmp[ii],BJdirk,cc)) + return "target directory not on target device"; // somewhere else + ch = BJdirk[cc]; + if (ch && ch != '/') + return "target directory not on target device"; + devMounted = 1; // device mounted at directory + strcpy(mountdev,BJdev); // save for later unmount() + strcpy(mountdirk,BJdirk); + wprintf(mLog,"target is valid and mounted \n"); + return 0; + } + } + else { // device not specified + wprintf(mLog,"target directory is valid \n"); + return 0; + } + } + else { // directory does not exist + if (*BJdev) { + wprintf(mLog,"target is valid and not mounted \n"); // can be created at mount time + return 0; + } + else { + yn = zmessageYN(mWin,"target directory does not exist\n" // offer to create if missing + "create target directory?"); // v.4.1 + if (! yn) return "target directory does not exist"; + err = mkdir(BJdirk,0751); + if (err) return strerror(errno); + else return "target directory created"; + } + } +} + + +// Delete backup files exceeding age and version limits. +// Copy new and modified disk files to backup location. + +int Backup(cchar *menu) +{ + char message[100]; + int vmode = 0, terrs = 0, ii, jj, yn; + int upvers = 0, deleted = 0; + char disp, *dfile = 0; + const char *errmess = 0; + double bsecs, bbytes, bspeed; + double time0; + + if (! BJvalid) { + if (Fgui) zmessageACK(mWin,0,"backup job has errors (open or edit)"); + else wprintf(mLog,"backup job has errors \n"); + return 0; + } + + if (! mount(0)) return 0; // validate and mount target v.3.2 + + Report("diffs summary"); // refresh all file data, report diffs + + if (Fgui) { + yn = zmessageYN(mWin,"backup target: %s %s \n" + "%d files (%s) will be copied to \n" + "(or deleted from) backup media \n" "continue?", + BJdev,BJdirk,Mfiles,formatKBMB(Mbytes,3)); // confirm backup target v.23 + if (! yn) return 0; + } + + snprintf(message,99,"\n""begin backup \n"); + wprintx(mLog,0,message,boldfont); + wprintf(mLog," files: %d bytes: %s \n",Mfiles,formatKBMB(Mbytes,3)); // files and bytes to copy + + if (Mfiles == 0) { + wprintf(mLog," *** nothing to back-up \n"); + return 0; + } + + wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk); + + if (strEqu(menu,"backup only")) vmode = 0; // backup command, no auto verify + if (strEqu(menu,"run job")) vmode = BJvmode; + + wprintf(mLog," assign new version numbers to modified backup files \n" + " and purge expired versions from backup location \n\n"); + + gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 + + for (ii = 0; ii < Bnf; ii++) // scan files at backup location + { + disp = Brec[ii].disp; + dfile = Brec[ii].file; + + errmess = null; + + if (disp == 'm' || disp == 'd') { // modified or deleted, + errmess = setnextVersion(Brec[ii]); // rename to next version number + Brec[ii].err = -1; // mark file gone + if (disp =='m') upvers++; // update counts + if (disp =='d') deleted++; + } + + if (! errmess) errmess = purgeVersions(Brec[ii],1); // purge expired file versions + // (exclude last version) v.4.0 + if (errmess) { + wprintf(mLog," %s \n *** %s \n",dfile,errmess); // log error v.4.3 + terrs++; + if (terrs > 100) goto backup_fail; + } + } + + wprintf(mLog," %d backup files were assigned new versions \n",upvers); + wprintf(mLog," %d backup files were deleted \n",deleted); + wprintf(mLog," %d expired versions (%s) were purged \n\n",Pfiles,formatKBMB(Pbytes,3)); + Pbytes = Pfiles = 0; + + start_timer(time0); // start timer + bbytes = Mbytes; + + BJstore(TFjobfile); // copy job file to temp file + writeDT(); // create date-time temp file + + wprintf(mLog,-2," %s \n",BD_JOBFILE); + errmess = copyFile(TFjobfile,BD_JOBFILE,2); // copy job file to backup location + if (errmess) goto backup_fail; + + wprintf(mLog,-2," %s \n",BD_DATETIME); + errmess = copyFile(TFdatetime,BD_DATETIME,2); // copy date-time file + if (errmess) goto backup_fail; + + wprintf(mLog," copying new and modified files from disk to backup location \n\n"); + + for (ii = 0; ii < Dnf; ii++) // scan all disk files + { + disp = Drec[ii].disp; + dfile = Drec[ii].file; + Drec[ii].finc = 0; // not included yet + + if (disp == 'n' || disp == 'm') // new or modified file + { + wprintf(mLog," %s \n",dfile); + errmess = copyFile(dfile,dfile,2); // copy disk file to backup v.4.3 + if (errmess) { + Drec[ii].err = 1; // copy failed + wprintf(mLog," *** %s \n",errmess); // log error v.4.3 + terrs++; + if (terrs > 100) goto backup_fail; + } + else { // copy OK + Drec[ii].finc = 1; // set included file flag + } + + if (checkKillPause()) goto backup_fail; // killed by user + } + + jj = Drec[ii].bindx; // purge last version now + if (jj >= 0) purgeVersions(Brec[jj],0); // bugfix v.4.1 + } + + if (terrs) wprintf(mLog," *** %d files had backup errors \n",terrs); + + synch_poop("backup"); // synch owner and permissions data + + bsecs = get_timer(time0); // output perf. statistics + wprintf(mLog," backup time: %.1f secs \n",bsecs); + bspeed = bbytes/mega/bsecs; + wprintf(mLog," backup speed: %.2f MB/sec \n",bspeed); + wprintf(mLog," backup complete \n"); + + if (vmode) // do verify if requested + { + wprintf(mLog,"\n"); + sleep(2); + + if (vmode == 1) Verify("incr"); + else if (vmode == 2) Verify("full"); + else if (vmode == 3) Verify("comp"); + + sprintf(message," %d files had backup errors \n",terrs); // repeat backup status v.4.5 + wprintx(mLog,0,message,boldfont); + + terrs = fverrs + fcerrs; + sprintf(message," %d files had verify errors \n",terrs); // add verify status v.4.5 + wprintx(mLog,0,message,boldfont); + } + + wprintf(mLog," ready \n"); + + if (ukoppMounted) unmount(0); // leave unmounted + + gdk_window_set_cursor(mLogwin,0); // normal cursor + return 0; + +backup_fail: + if (terrs > 100) wprintf(mLog," too many errors, giving up \n"); + else if (errmess) wprintf(mLog," %s \n",errmess); + wprintx(mLog,0," *** BACKUP FAILED \n",boldfont); + + bFilesReset(); + killFlag = 0; + gdk_window_set_cursor(mLogwin,0); // normal cursor + return 0; +} + + +// synchronize disk and backup files // v.25 +// bi-directional copy of new and newer files + +int Synch(cchar *menu) +{ + int ii, yn, dii, bii, comp; + char disp, *dfile = 0; + time_t btime, dtime; + const char *errmess = 0; + + if (! BJvalid) { + wprintf(mLog," *** job data has errors \n"); + return 0; + } + + if (! mount(0)) return 0; // validate and mount target v.3.2 + + if (Fgui) { + yn = zmessageYN(mWin,"backup target: %s %s \n continue?",BJdev,BJdirk); // confirm backup target + if (! yn) return 0; + } + wprintf(mLog," using backup directory: %s %s \n",BJdev,BJdirk); + + dGetFiles(); // get disk files of backup job + if (bGetFiles() < 0) goto synch_exit; // get files in backup location + setFileDisps(); // compare and set dispositions + + wprintf(mLog,"\n begin synchronize \n"); + + gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 + + BJstore(TFjobfile); // copy job file to temp file + writeDT(); // create date-time temp file + + wprintf(mLog,-2," %s \n",BD_JOBFILE); + errmess = copyFile(TFjobfile,BD_JOBFILE,2); // copy job file to backup location + if (errmess) goto synch_exit; + + wprintf(mLog,-2," %s \n",BD_DATETIME); + errmess = copyFile(TFdatetime,BD_DATETIME,2); // copy date-time file + if (errmess) goto synch_exit; + + for (ii = 0; ii < Dnf; ii++) // copy new disk files >> backup loc. + { + disp = Drec[ii].disp; + dfile = Drec[ii].file; + if (disp != 'n') continue; + wprintf(mLog," disk >> backup: %s \n",dfile); + errmess = copyFile(dfile,dfile,2); + if (errmess) wprintf(mLog," *** %s \n",errmess); + else Drec[ii].finc = 1; + if (checkKillPause()) goto synch_exit; // killed by user + } + + for (ii = 0; ii < Bnf; ii++) // copy new backup files >> disk + { // (aka "deleted" disk files) + disp = Brec[ii].disp; + dfile = Brec[ii].file; + if (disp != 'd') continue; + wprintf(mLog," backup >> disk: %s \n",dfile); + errmess = copyFile(dfile,dfile,1); + if (errmess) wprintf(mLog," *** %s \n",errmess); + else Brec[ii].finc = 1; + if (checkKillPause()) goto synch_exit; + } + + dii = bii = 0; + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = strcmp(Drec[dii].file, Brec[bii].file); + + if (comp < 0) { dii++; continue; } // next disk file + if (comp > 0) { bii++; continue; } // next backup file + + disp = Drec[dii].disp; + dfile = Drec[dii].file; + + if (disp == 'm') // screen for modified status + { + btime = int(Brec[bii].mtime); + dtime = int(Drec[dii].mtime); + + if (btime > dtime) { // copy newer backup file >> disk + wprintf(mLog," backup >> disk: %s \n",dfile); + errmess = copyFile(dfile,dfile,1); + if (errmess) wprintf(mLog," *** %s \n",errmess); + else Brec[bii].finc = 1; + } + + else { // copy newer disk file >> backup + wprintf(mLog," disk >> backup: %s \n",dfile); + errmess = copyFile(dfile,dfile,2); + if (errmess) wprintf(mLog," *** %s \n",errmess); + else Drec[dii].finc = 1; + } + } + + dii++; // next disk and backup files + bii++; + + if (checkKillPause()) goto synch_exit; // killed by user + } + + errmess = null; + synch_poop("synch"); // synch owner and permissions data + + Verify("incremental"); // verify all files copied + +synch_exit: + if (errmess) wprintf(mLog," *** %s \n",errmess); + wprintf(mLog," ready \n"); // v.3.6 + killFlag = 0; + gdk_window_set_cursor(mLogwin,0); // normal cursor + return 0; +} + + +// verify integrity of backup files + +int Verify(cchar *menu) +{ + int ii, vers, comp, vfiles; + int dfiles1 = 0, dfiles2 = 0; + char filespec[maxfcc]; + const char *errmess = 0; + double secs, dcc1, vbytes, vspeed; + double mtime, diff; + double time0; + struct stat64 filestat; + + vfiles = fverrs = fcerrs = 0; + vbytes = 0.0; + if (! mount(0)) return 0; // validate and mount target v.3.2 + + start_timer(time0); + + gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 + + if (strnEqu(menu,"incremental",4)) // verify new/modified files only + { + wprintx(mLog,0,"\n""Verify files copied in prior backup or synch \n",boldfont); + + for (ii = 0; ii < Dnf; ii++) // scan disk file list + { + if (! Drec[ii].finc) continue; // file included in last backup + strncpy0(filespec,Drec[ii].file,maxfcc-1); + wprintf(mLog," %s \n",filespec); // output filespec + + errmess = checkFile(filespec,1,dcc1); // compare disk/backup files, get length + if (errmess) { + wprintf(mLog," *** %s \n\n",errmess); // log and count errors + if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure + else fverrs++; + } + + vfiles++; // count files and bytes + vbytes += dcc1; + if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 + + if (checkKillPause()) goto verify_exit; // killed by user + } + + for (ii = 0; ii < Bnf; ii++) // scan backup file list v.25 + { + if (! Brec[ii].finc) continue; // file included in last backup + strncpy0(filespec,Brec[ii].file,maxfcc-1); + wprintf(mLog," %s \n",filespec); // output filespec + + errmess = checkFile(filespec,1,dcc1); // compare disk/backup files, get length + if (errmess) { + wprintf(mLog," *** %s \n\n",errmess); // log and count errors + if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure + else fverrs++; + } + + vfiles++; // count files and bytes + vbytes += dcc1; + if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 + + if (checkKillPause()) goto verify_exit; // killed by user + } + } + + if (strEqu(menu,"full")) // verify all files are readable + { + wprintx(mLog,0,"\n""Read and verify ALL backup files \n\n",boldfont); + + bGetFiles(); // get all files at backup location + wprintf(mLog," %d backup files \n",Bnf); + if (! Bnf) goto verify_exit; + + for (ii = 0; ii < Bnf; ii++) // scan backup file list + { + strncpy0(filespec,Brec[ii].file,maxfcc-10); // /directory.../filename + + if (Brec[ii].err == 0) + { // check current file + wprintf(mLog,-2," %s \n",filespec); + errmess = checkFile(filespec,0,dcc1); // verify file, get length + if (errmess) { + wprintf(mLog,-1," *** %s \n",errmess); // log and count error + wprintf(mLog,"\n"); + fverrs++; + } + vfiles++; // count files and bytes + vbytes += dcc1; + } + + if (Brec[ii].lover) + for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++) // check previous versions + { + setFileVersion(filespec,vers); // append version if > 0 + wprintf(mLog,-2," %s \n",filespec); + errmess = checkFile(filespec,0,dcc1); // verify file, get length + if (errmess) { + wprintf(mLog,-1," *** %s \n",errmess); // log and count error + wprintf(mLog,"\n"); + fverrs++; + } + vfiles++; // count files and bytes + vbytes += dcc1; + } + + if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 + + if (checkKillPause()) goto verify_exit; // killed by user + } + } + + if (strnEqu(menu,"compare",4)) // compare backup files to disk files + { + wprintx(mLog,0,"\n Read and verify ALL backup files. \n",boldfont); + wprintf(mLog," Compare to correspending disk files (if present). \n\n"); + + bGetFiles(); // get all files at backup location + wprintf(mLog," %d backup files \n",Bnf); + if (! Bnf) goto verify_exit; + + for (ii = 0; ii < Bnf; ii++) // scan backup file list + { + strncpy0(filespec,Brec[ii].file,maxfcc-10); // /directory.../filename + + if (Brec[ii].err == 0) + { // check current file + comp = 0; + if (lstat64(filespec,&filestat) == 0) { // corresponding disk file exists + mtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; + diff = fabs(mtime - Brec[ii].mtime); // compare disk and backup mod times + if (diff < MODTIMETOLR) comp = 1; // equal within file system resolution + dfiles1++; // count matching disk names + dfiles2 += comp; // count matching mod times + } + + wprintf(mLog,-2," %s \n",filespec); + errmess = checkFile(filespec,comp,dcc1); // verify, get length, compare disk + if (errmess) { + wprintf(mLog,-1," *** %s \n",errmess); // log and count error + wprintf(mLog,"\n"); + if (strstr(errmess,"compare")) fcerrs++; // backup - disk compare failure + else fverrs++; + } + vfiles++; // count files and bytes + vbytes += dcc1; + } + + if (Brec[ii].lover) + for (vers = Brec[ii].lover; vers <= Brec[ii].hiver; vers++) // check previous versions + { + setFileVersion(filespec,vers); // append version if > 0 + wprintf(mLog,-2," %s \n",filespec); + errmess = checkFile(filespec,0,dcc1); // verify file, get length + if (errmess) { + wprintf(mLog,-1," *** %s \n",errmess); // log and count error + wprintf(mLog,"\n"); + fverrs++; + } + vfiles++; // count files and bytes + vbytes += dcc1; + } + + if (checkKillPause()) goto verify_exit; // killed by user + if (fverrs + fcerrs > 100) goto verify_exit; // v.3.7 + } + } + + wprintf(mLog," backup files: %d (%s) \n",vfiles,formatKBMB(vbytes,3)); + wprintf(mLog," backup file read errors: %d \n",fverrs); + + if (strnEqu(menu,"incremental",4)) + wprintf(mLog," compare failures: %d \n",fcerrs); + + if (strnEqu(menu,"compare",4)) { + wprintf(mLog," matching disk names: %d mod times: %d \n",dfiles1,dfiles2); + wprintf(mLog," compare failures: %d \n",fcerrs); + } + + secs = get_timer(time0); + wprintf(mLog," verify time: %.1f secs \n",secs); + vspeed = vbytes/mega/secs; + wprintf(mLog," verify speed: %.2f MB/sec \n",vspeed); + +verify_exit: + if (fverrs + fcerrs) wprintx(mLog,0," *** THERE WERE ERRORS *** \n",boldfont); + else wprintx(mLog,0," NO ERRORS \n",boldfont); // v.3.9 + wprintf(mLog," ready \n"); // v.3.6 + killFlag = 0; + gdk_window_set_cursor(mLogwin,0); // normal cursor + return 0; +} + + +// various kinds of reports + +int Report(cchar *menu) +{ + char *fspec1; + char fspec2[200], bfile[maxfcc]; + char *pslash, *pdirk, ppdirk[maxfcc]; + char header[100]; + int ii, kfiles, knew, kdel, kmod; + int dii, bii, comp, err; + double nbytes, mb1, mb2, fage; + int vers, lover, hiver, nexpv; + int age, loage, hiage; + struct tm tmdt; + time_t btime, dtime; + char bmod[20], dmod[20]; + const char *copy; + struct stat64 filestat; + + // get all disk files in backup job + // report file and byte counts per include and exclude record + + if (strEqu(menu, "get disk files")) + { + dGetFiles(); // get all files on disk + + wprintx(mLog,0,"\n"" files bytes filespec retention (days, vers) \n",boldfont); + + for (ii = 0; ii < BJnnx; ii++) { // formatted report + if (BJfspec[ii]) { + if (BJfiles[ii]) { + if (BJrtype[ii] == 2) // include: add retention v.4.0 + wprintf(mLog," %6d %9s %s (%d, %d) \n", + BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii],BJretND[ii],BJretNV[ii]); + else + wprintf(mLog," %6d %9s %s \n",BJfiles[ii],formatKBMB(BJbytes[ii],3),BJfspec[ii]); + } + else if (BJrtype[ii] > 1) { + wprintx(mLog,0," NO FILES",boldfont); + wprintf(mLog," %s \n",BJfspec[ii]); + } + else + wprintf(mLog," %s \n",BJfspec[ii]); + } + } + + wprintf(mLog," %6d %9s TOTALS \n", Dnf, formatKBMB(Dbytes,3)); + goto report_exit; + } + + // report disk / backup differences: new, modified, and deleted files + + if (strEqu(menu, "diffs summary")) + { + dGetFiles(); + if (bGetFiles() < 0) goto report_exit; + setFileDisps(); + + wprintf(mLog,"\n disk files: %d backup files: %d \n",Dnf,Bnf); + wprintx(mLog,0,"\n Differences between files on disk and backup files: \n",boldfont); + wprintf(mLog," %6d disk files not found on backup (new files) \n",nnew); + wprintf(mLog," %6d files with different data (modified files) \n",nmod); + wprintf(mLog," %6d backup files not found on disk (deleted files) \n",ndel); + wprintf(mLog," %6d files with identical data (unchanged files) \n",nunc); + wprintf(mLog," Total differences: %d files (%s new + modified) \n\n",Mfiles,formatKBMB(Mbytes,3)); + goto report_exit; + } + + // report disk / backup differences per directory level + + if (strEqu(menu, "diffs by directory")) + { + dGetFiles(); + if (bGetFiles() < 0) goto report_exit; + setFileDisps(); + + SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first + SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D'); + + wprintx(mLog,0,"\n differences by directory \n",boldfont); + + wprintx(mLog,0," new mod del bytes directory \n",boldfont); + + nbytes = kfiles = knew = kmod = kdel = 0; + dii = bii = 0; + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = filecomp(Drec[dii].file, Brec[bii].file); + + if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file + else pdirk = Drec[dii].file; + + pslash = (char *) strrchr(pdirk,'/'); // isolate directory + if (pslash) *pslash = 0; + if (strNeq(pdirk,ppdirk)) { // if directory changed, output + if (kfiles > 0) // totals from prior directory + wprintf(mLog," %5d %5d %5d %8s %s \n", + knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk); + nbytes = kfiles = knew = kmod = kdel = 0; // reset totals + strcpy(ppdirk,pdirk); // start new directory + } + if (pslash) *pslash = '/'; + + if (comp < 0) { // unmatched disk file: new + knew++; // count new files + kfiles++; + nbytes += Drec[dii].size; + dii++; + } + + else if (comp > 0) { // unmatched backup file + if (Brec[bii].disp == 'd') { + kdel++; // count deleted files + kfiles++; + } + bii++; + } + + else if (comp == 0) { // file present on disk and backup + if (Drec[dii].disp == 'm') kmod++; // count modified files + if (Drec[dii].disp == 'n') knew++; // count new files (backup disp is 'v') + if (Drec[dii].disp != 'u') { + kfiles++; // count unless unchanged + nbytes += Drec[dii].size; + } + dii++; + bii++; + } + } + + if (kfiles > 0) wprintf(mLog," %5d %5d %5d %s %s \n", // totals from last directory + knew,kmod,kdel,formatKBMB(nbytes,3),ppdirk); + + SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore straight ascii sort + SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A'); + goto report_exit; + } + + // report disk / backup differences by file status and directory + + if (strEqu(menu, "diffs by file status")) // new v.4.1 + { + dGetFiles(); + if (bGetFiles() < 0) goto report_exit; + setFileDisps(); + + SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'D'); // re-sort, directories first + SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'D'); + + wprintx(mLog,0,"\n new files by directory \n",boldfont); // report new files + wprintx(mLog,0," files bytes directory \n",boldfont); + + nbytes = knew = 0; + dii = bii = 0; + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = filecomp(Drec[dii].file, Brec[bii].file); + + if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file + else pdirk = Drec[dii].file; + + pslash = (char *) strrchr(pdirk,'/'); // isolate directory + if (pslash) *pslash = 0; + if (strNeq(pdirk,ppdirk)) { // if directory changed, output + if (knew > 0) // totals from prior directory + wprintf(mLog," %6d %8s %s \n", + knew,formatKBMB(nbytes,3),ppdirk); + nbytes = knew = 0; // reset totals + strcpy(ppdirk,pdirk); // start new directory + } + if (pslash) *pslash = '/'; + + if (comp < 0) { // unmatched disk file: new + knew++; // count new files + nbytes += Drec[dii].size; + dii++; + } + + else if (comp > 0) // unmatched backup file: deleted + bii++; + + else if (comp == 0) { // file present on disk and backup + if (Drec[dii].disp == 'n') { // count new files (backup disp is 'v') + knew++; + nbytes += Drec[dii].size; + } + dii++; + bii++; + } + } + + if (knew > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory + knew,formatKBMB(nbytes,3),ppdirk); + + wprintx(mLog,0,"\n modified files by directory \n",boldfont); // report modified files + wprintx(mLog,0," files bytes directory \n",boldfont); + + nbytes = kmod = 0; + dii = bii = 0; + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = filecomp(Drec[dii].file, Brec[bii].file); + + if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file + else pdirk = Drec[dii].file; + + pslash = (char *) strrchr(pdirk,'/'); // isolate directory + if (pslash) *pslash = 0; + if (strNeq(pdirk,ppdirk)) { // if directory changed, output + if (kmod > 0) // totals from prior directory + wprintf(mLog," %6d %8s %s \n", + kmod,formatKBMB(nbytes,3),ppdirk); + nbytes = kmod = 0; // reset totals + strcpy(ppdirk,pdirk); // start new directory + } + if (pslash) *pslash = '/'; + + if (comp < 0) // unmatched disk file: new + dii++; + + else if (comp > 0) // unmatched backup file: deleted + bii++; + + else if (comp == 0) { // file present on disk and backup + if (Drec[dii].disp == 'm') { // count modified files + kmod++; + nbytes += Drec[dii].size; + } + dii++; + bii++; + } + } + + if (kmod > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory + kmod,formatKBMB(nbytes,3),ppdirk); + + wprintx(mLog,0,"\n deleted files by directory \n",boldfont); // report deleted files + wprintx(mLog,0," files bytes directory \n",boldfont); + + nbytes = kdel = 0; + dii = bii = 0; + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = filecomp(Drec[dii].file, Brec[bii].file); + + if (comp > 0) pdirk = Brec[bii].file; // get disk or backup file + else pdirk = Drec[dii].file; + + pslash = (char *) strrchr(pdirk,'/'); // isolate directory + if (pslash) *pslash = 0; + if (strNeq(pdirk,ppdirk)) { // if directory changed, output + if (kdel > 0) // totals from prior directory + wprintf(mLog," %6d %8s %s \n", + kdel,formatKBMB(nbytes,3),ppdirk); + nbytes = kdel = 0; // reset totals + strcpy(ppdirk,pdirk); // start new directory + } + if (pslash) *pslash = '/'; + + if (comp < 0) // unmatched disk file: new + dii++; + + else if (comp > 0) { // unmatched backup file: deleted + if (Brec[bii].disp == 'd') { + kdel++; // count deleted files + nbytes += Brec[bii].size; + } + bii++; + } + + else if (comp == 0) { // file present on disk and backup + dii++; + bii++; + } + } + + if (kdel > 0) wprintf(mLog," %6d %8s %s \n", // totals from last directory + kdel,formatKBMB(nbytes,3),ppdirk); + + SortFileList((char *) Drec, sizeof(dfrec), Dnf, 'A'); // restore straight ascii sort + SortFileList((char *) Brec, sizeof(bfrec), Bnf, 'A'); + goto report_exit; + } + + // report disk / backup differences by file + + if (strEqu(menu, "diffs by file")) + { + dGetFiles(); + if (bGetFiles() < 0) goto report_exit; + setFileDisps(); + + wprintx(mLog,0,"\n Detailed list of disk:backup differences: \n",boldfont); + + snprintf(header,99,"\n %d disk files not found on backup \n",nnew); + wprintx(mLog,0,header,boldfont); + + for (ii = 0; ii < Dnf; ii++) + { + if (Drec[ii].disp != 'n') continue; + wprintf(mLog," %s \n",Drec[ii].file); + } + + snprintf(header,99,"\n %d backup files not found on disk \n",ndel); + wprintx(mLog,0,header,boldfont); + + for (ii = 0; ii < Bnf; ii++) + { + if (Brec[ii].disp != 'd') continue; + wprintf(mLog," %s \n",Brec[ii].file); + } + + snprintf(header,99,"\n %d files with different data \n",nmod); + wprintx(mLog,0,header,boldfont); + + wprintx(mLog,0," backup mod date copy disk mod date filespec \n",boldfont); + + dii = bii = 0; + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { // revised v.25 + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = strcmp(Drec[dii].file, Brec[bii].file); + + if (comp < 0) { dii++; continue; } // next disk file + if (comp > 0) { bii++; continue; } // next backup file + + if (Drec[dii].disp == 'm') // screen for modified status + { + btime = int(Brec[bii].mtime); // mod time on backup + dtime = int(Drec[dii].mtime); // mod time on disk + + copy = "<<<<"; // copy direction, disk to backup + if (btime > dtime) copy = "!!!!"; // flag if backup to disk + + tmdt = *localtime(&btime); + snprintf(bmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900, + tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min); + + tmdt = *localtime(&dtime); + snprintf(dmod,19,"%4d.%02d.%02d-%02d:%02d",tmdt.tm_year+1900, + tmdt.tm_mon+1,tmdt.tm_mday,tmdt.tm_hour,tmdt.tm_min); + + wprintf(mLog," %s %s %s %s \n",bmod,copy,dmod,Drec[dii].file); + } + + dii++; // next disk and backup files + bii++; + } + + goto report_exit; + } + + // report versions and expired versions per file + + if (strEqu(menu, "file versions")) + { + Report("diffs summary"); + if (Bnf < 1) goto report_exit; + + wprintx(mLog,0,"\n lover hiver nxver loage hiage bytes expired filespec \n",boldfont); + + for (ii = 0; ii < Bnf; ii++) + { + lover = Brec[ii].lover; + hiver = Brec[ii].hiver; + nexpv = Brec[ii].nexpv; + if (! lover) continue; + + strcpy(bfile,BJdirk); + strcat(bfile,Brec[ii].file); + loage = hiage = -1; + mb1 = mb2 = 0.0; + + for (vers = lover; vers <= hiver; vers++) // loop file versions + { + setFileVersion(bfile,vers); + err = lstat64(bfile,&filestat); // check file exists on backup + if (err) continue; + + fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file age in days + age = fage; // remove fraction + if (loage < 0) loage = hiage = age; + if (age < loage) loage = age; + if (age > hiage) hiage = age; + + mb1 += filestat.st_size; // accumulate total bytes + if (vers < lover + nexpv) // v.4.0 + mb2 += filestat.st_size; // and expired version bytes + } + + wprintf(mLog," %5d %5d %5d %5d %5d %8s %8s %s \n", + lover,hiver,nexpv,loage,hiage,formatKBMB(mb1,3),formatKBMB(mb2,3),Brec[ii].file); + } + + goto report_exit; + } + + // report expired file versions (will be purged) + + if (strEqu(menu, "expired versions")) + { + Report("diffs summary"); + if (Bnf < 1) goto report_exit; + + wprintx(mLog,0,"\n expired files (will purge from backup location) \n",boldfont); + wprintx(mLog,0,"\n vers age bytes filespec \n",boldfont); + + for (ii = 0; ii < Bnf; ii++) + { + lover = Brec[ii].lover; + hiver = Brec[ii].hiver; + nexpv = Brec[ii].nexpv; + if (! nexpv) continue; + + strcpy(bfile,BJdirk); + strcat(bfile,Brec[ii].file); + mb1 = 0.0; + + for (vers = lover; vers < lover + nexpv; vers++) // loop expired file versions v.4.0 + { + setFileVersion(bfile,vers); + err = lstat64(bfile,&filestat); // check file exists on backup + if (err) continue; + fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // age in days, size in MB + age = fage; + mb1 = filestat.st_size; + wprintf(mLog," %5d %5d %8s %s \n",vers,age,formatKBMB(mb1,3),Brec[ii].file); + } + } + + goto report_exit; + } + + // list all files in backup job set + + if (strEqu(menu, "list disk files")) + { + wprintx(mLog,0," List all files in backup file set: \n",boldfont); + + dGetFiles(); + wprintf(mLog," %d files found \n",Dnf); + + for (ii = 0; ii < Dnf; ii++) + wprintf(mLog," %s \n",Drec[ii].file); + + goto report_exit; + } + + // list all files on backup + + if (strEqu(menu, "list backup files")) + { + wprintx(mLog,0," List all files at backup location \n",boldfont); + if (bGetFiles() < 0) goto report_exit; + + for (ii = 0; ii < Bnf; ii++) + { + if (Brec[ii].lover) wprintf(mLog," %s (vers %d-%d) \n", + Brec[ii].file, Brec[ii].lover, Brec[ii].hiver); + else wprintf(mLog," %s \n",Brec[ii].file); + } + + goto report_exit; + } + + // search disk and backup file list for match with wild search pattern + + if (strEqu(menu, "find files")) + { + wprintx(mLog,0," Find files matching wildcard pattern \n",boldfont); + + dGetFiles(); + bGetFiles(); + if (!(Dnf + Bnf)) goto report_exit; + + fspec1 = zdialog_text(mWin,"enter (wildcard) filespec:","/dir*/file* "); + if (! fspec1) goto report_exit; + strncpy0(fspec2,fspec1,199); + zfree(fspec1); + strTrim(fspec2); + if (! *fspec2) goto report_exit; + + wprintf(mLog,"\n matching disk files: \n"); + + for (ii = 0; ii < Dnf; ii++) + if (MatchWild(fspec2,Drec[ii].file) == 0) + wprintf(mLog," %s \n",Drec[ii].file); + + wprintf(mLog,"\n matching backup files: \n"); + + for (ii = 0; ii < Bnf; ii++) + { + if (MatchWild(fspec2,Brec[ii].file) == 0) { + if (Brec[ii].hiver) wprintf(mLog," %s (vers %d-%d) \n", + Brec[ii].file, Brec[ii].lover, Brec[ii].hiver); + else wprintf(mLog," %s \n",Brec[ii].file); + } + } + + goto report_exit; + } + +report_exit: + wprintf(mLog," ready \n"); // v.3.6 + return 0; +} + + +// edit dialog for file restore + +int RJedit_fchooser(cchar *dirk); +zdialog *RJedit_fchooser_zd = 0; + +int RJedit(cchar *menu) +{ + int RJedit_dialog_event(zdialog *zd, cchar *event); + + zdialog *zd; + + wprintf(mLog,"\n Restore files from backup \n"); + + if (bGetFiles() < 0) return 0; // get files in backup location + wprintf(mLog," %d backup files found \n",Bnf); + if (! Bnf) return 0; + + zd = zdialog_new("restore files from backup",mWin,"browse","done","cancel",null); + zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); + zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog"); + zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=5|expand"); + zdialog_add_widget(zd,"label","labfrom","vb1","copy-from backup"); // copy-from backup [_____________] + zdialog_add_widget(zd,"label","labto","vb1","copy-to disk"); // copy-to disk [_____________] + zdialog_add_widget(zd,"entry","entfrom","vb2",RJfrom); + zdialog_add_widget(zd,"entry","entto","vb2",RJto); + zdialog_add_widget(zd,"hsep","hsep1","dialog"); + zdialog_add_widget(zd,"label","labf","dialog","files to restore"); // files to restore + zdialog_add_widget(zd,"frame","framef","dialog",0,"expand"); // scrolling edit window + zdialog_add_widget(zd,"scrwin","scrf","framef"); + zdialog_add_widget(zd,"edit","editf","scrf"); + + editwidget = zdialog_widget(zd,"editf"); + + for (int ii = 0; ii < RJnnx; ii++) // get restore include/exclude recs, + { // pack into file selection edit box + if (RJrtype[ii] == 2) + wprintf(editwidget,"include %s\n",RJfspec[ii]); + if (RJrtype[ii] == 3) + wprintf(editwidget,"exclude %s\n",RJfspec[ii]); + } + + zdialog_resize(zd,400,400); + zdialog_run(zd,RJedit_dialog_event,"20/20"); // run dialog posn v.4.1 + zdialog_wait(zd); + return 0; +} + + +// dialog completion function +// get restore job data from dialog widgets and validate + +int RJedit_dialog_event(zdialog *zd, cchar *event) +{ + DIR *pdirk; + char *pp, *fspec, rdirk[300]; + int ftf = 1, cc, rtype, nerrs = 0; + int zstat, days, vers; + const char *errmess = 0; + + zstat = zd->zstat; + if (! zstat) return 0; // wait for dialog end + + zd->zstat = 0; // this dialog continues + + if (RJedit_fchooser_zd) // kill file chooser dialog if active + zdialog_free(RJedit_fchooser_zd); + + if (zstat == 1) + { // browse button, file-chooser dialog + zdialog_fetch(zd,"entfrom",RJfrom,299); // copy-from location /dirk/xxx/.../ + strTrim(RJfrom); + strcpy(rdirk,BJdirk); // start at /media/xxx/dirk/xxx/ + strncat(rdirk,RJfrom,299); + RJedit_fchooser(rdirk); // do file chooser dialog + return 0; + } + + if (zstat != 1 && zstat != 2) { // cancel or destroy + zdialog_free(zd); + return 0; + } + + RJreset(); // edit done, reset job data + + zdialog_fetch(zd,"entfrom",RJfrom,299); // copy-from location /dirk/xxx/.../ + strTrim(RJfrom); + + strcpy(rdirk,BJdirk); // validate copy-from location + strncat(rdirk,RJfrom,299); // /media/xxx/dirk/... + pdirk = opendir(rdirk); + if (! pdirk) { + zmessageACK(0,0,"invalid copy-from location"); // v.4.1 + nerrs++; + } + else closedir(pdirk); + + cc = strlen(RJfrom); // insure '/' at end + if (RJfrom[cc-1] != '/') strcat(RJfrom,"/"); + + zdialog_fetch(zd,"entto",RJto,299); // copy-to location /dirk/yyy/.../ + strTrim(RJto); + + pdirk = opendir(RJto); // validate copy-to location + if (! pdirk) { + zmessageACK(0,0,"invalid copy-to location"); // v.4.1 + nerrs++; + } + else closedir(pdirk); + + cc = strlen(RJto); // insure '/' at end + if (RJto[cc-1] != '/') strcat(RJto,"/"); + + for (RJnnx = 0; RJnnx < maxnx; RJnnx++) // include/exclude recs from edit box + { + pp = wscanf(editwidget,ftf); // next record from edit widget + if (! pp) break; + wprintf(mLog," %s \n",pp); + + errmess = parseNXrec(pp,rtype,fspec,days,vers); // validate include/exclude rec. + if (errmess) { + zmessageACK(0,0,"%s \n %s",pp,errmess); // v.4.1 + nerrs++; + } + + RJrtype[RJnnx] = rtype; // save job record + RJfspec[RJnnx] = fspec; + } + + if (RJnnx == maxnx) { + zmessageACK(0,0,"max job records exceeded"); + nerrs++; + } + + if (nerrs == 0) RJvalid = 1; + + if (RJvalid) { // all OK + rGetFiles(); // get files to restore + zdialog_free(zd); // destroy dialog + } + else zd->zstat = 0; // errors, keep dialog open v.4.1 + + return 0; +} + + +// file chooser dialog for restore job edit + +int RJedit_fchooser(cchar *dirk) // v.3.5 +{ + int RJedit_fchooser_event(zdialog *zd, const char *event); + + RJedit_fchooser_zd = zdialog_new("Choose Files to Restore",mWin,"Done",null); + zdialog *zd = RJedit_fchooser_zd; + + zdialog_add_widget(zd,"frame","fr1","dialog",0,"expand"); + zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); + zdialog_add_widget(zd,"label","space","hb1",0,"expand"); + zdialog_add_widget(zd,"check","hidden","hb1","show hidden","space=5"); + zdialog_add_widget(zd,"button","incl","hb1","include","space=5"); + zdialog_add_widget(zd,"button","excl","hb1","exclude","space=5"); + + fc_widget = gtk_file_chooser_widget_new(GTK_FILE_CHOOSER_ACTION_OPEN); + GtkWidget *frame = zdialog_widget(zd,"fr1"); + gtk_container_add(GTK_CONTAINER(frame),fc_widget); + + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(fc_widget),dirk); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(fc_widget),1); + + zdialog_resize(zd,550,500); + zdialog_run(zd,RJedit_fchooser_event,"50/50"); // posn v.4.1 + zdialog_wait(zd); + zdialog_free(zd); + RJedit_fchooser_zd = 0; + return 0; +} + + +int RJedit_fchooser_event(zdialog *zd, const char *event) +{ + GSList *flist = 0; + struct stat64 filestat; + char *file1, *file2, rdirk[300]; + int ii, rdcc, err; + + if (strEqu(event,"hidden")) { // show/hide hidden files v.3.7.1 + zdialog_fetch(zd,"hidden",ii); + gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(fc_widget),ii); + } + + if (strEqu(event,"incl") || strEqu(event,"excl")) // include or exclude + { + strcpy(rdirk,BJdirk); // copy-from location v.3.9 + strncat(rdirk,RJfrom,299); // /media/xxx/dirk/... + rdcc = strlen(rdirk); + + flist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(fc_widget)); + + for (ii = 0; ; ii++) // process selected files + { + file1 = (char *) g_slist_nth_data(flist,ii); + if (! file1) break; + + if (! strnEqu(rdirk,file1,rdcc)) { // check file in backup location v.3.9 + zmessageACK(0,0,"not in copy-from directory: \n %s",file1); // v.4.1 + continue; + } + + err = lstat64(file1,&filestat); + if (err) { + zmessageACK(0,0,"error: %s file: \n %s",strerror(errno),file1); + continue; + } + + file2 = strdupz(file1,2); // extra space for wildcard + g_free(file1); + + if (S_ISDIR(filestat.st_mode)) strcat(file2,"/*"); // if directory, append wildcard + + if (strEqu(event,"incl")) + wprintf(editwidget,"include %s""\n",file2 + BJdcc); // omit backup mount point + if (strEqu(event,"excl")) + wprintf(editwidget,"exclude %s""\n",file2 + BJdcc); + zfree(file2); + } + + gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(fc_widget)); + g_slist_free(flist); + } + + return 0; +} + + +// list and validate backup files to be restored + +int RJlist(cchar *menu) +{ + int cc1, cc2, errs = 0; + char *file1, file2[maxfcc]; + + if (! RJvalid) wprintf(mLog," *** restore job has errors \n"); + if (! Rnf) goto rjlist_exit; + + wprintf(mLog,"\n copy %d files from backup: %s \n",Rnf, RJfrom); + wprintf(mLog," to directory: %s \n",RJto); + wprintf(mLog,"\n resulting files will be the following: \n"); + + cc1 = strlen(RJfrom); // from: /dirk/xxx/.../ + cc2 = strlen(RJto); // to: /dirk/yyy/.../ + + for (int ii = 0; ii < Rnf; ii++) + { + file1 = Rrec[ii].file; + + if (! strnEqu(file1,RJfrom,cc1)) { + wprintf(mLog," *** not within copy-from: %s \n",file1); + errs++; + continue; + } + + strcpy(file2,RJto); + strcpy(file2+cc2,file1+cc1); + wprintf(mLog," %s \n",file2); + } + + if (errs) { + wprintf(mLog," *** %d errors \n",errs); + RJvalid = 0; + } + +rjlist_exit: + wprintf(mLog," ready \n"); // v.3.6 + return 0; +} + + +// restore files based on data from restore dialog + +int Restore(cchar *menu) +{ + int ii, nn, ccf; + char dfile[maxfcc]; + const char *errmess = 0; + + if (! RJvalid || ! Rnf) { + wprintf(mLog," *** restore job has errors \n"); + goto restore_exit; + } + + nn = zmessageYN(mWin,"Restore %d files from: %s%s \n to: %s \n" + "Proceed with file restore ?",Rnf,BJdirk,RJfrom,RJto); + if (! nn) goto restore_exit; + + snprintf(dfile,maxfcc-2,"\n""begin restore of %d files to: %s \n",Rnf,RJto); + wprintx(mLog,0,dfile,boldfont); + + gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 + + ccf = strlen(RJfrom); // from: /media/xxx/filespec + + for (ii = 0; ii < Rnf; ii++) + { + strcpy(dfile,RJto); // to: /destination/filespec + strcat(dfile,Rrec[ii].file + ccf); + wprintf(mLog," %s \n",dfile); + errmess = copyFile(Rrec[ii].file,dfile,1); + if (errmess) wprintf(mLog," *** %s \n",errmess); + else Rrec[ii].finc = 1; + if (checkKillPause()) goto restore_exit; + } + + synch_poop("restore"); // synch owner and permissions data + +restore_exit: + wprintf(mLog," ready \n"); // v.3.6 + killFlag = 0; + gdk_window_set_cursor(mLogwin,0); // normal cursor + return 0; +} + + +// format disk backup device with vfat or ext2 file system +// uses existing partitions only - no changes to partition table // v.3.3.1 + +int Format(cchar *menu) +{ + int ii, jj, zstat, yn, contx = 0; + char text[200], device[20], filesys[20], label[20], *crec; + zdialog *zd; + FILE *fid; + + wprintf(mLog,"\n Format a backup device \n"); + + zd = zdialog_new("format backup device",mWin,"start","cancel",null); + zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=10"); + zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog"); + zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|expand"); // backup device [________][v] + zdialog_add_widget(zd,"label","labdev","vb1"," backup device"); // device label [________] + zdialog_add_widget(zd,"comboE","entdev","vb2"); // file system [________][v] + zdialog_add_widget(zd,"label","lablab","vb1"," device label"); + zdialog_add_widget(zd,"entry","entlab","vb2","ukopp"); + zdialog_add_widget(zd,"label","labfs","vb1"," file system"); + zdialog_add_widget(zd,"comboE","entfs","vb2","ext2"); + + unmount(0); // unmount mounted device + BDpoop(); // refresh available devices + + for (ii = 0; ii < Ndisk; ii++) // load combo box with device + { + strcpy(text,diskdev[ii]); // /dev/xxxx description + strncatv(text,199," ",diskdesc[ii],null); + zdialog_cb_app(zd,"entdev",text); + } + + zdialog_cb_app(zd,"entfs","ext2"); // load combo box with file systems + zdialog_cb_app(zd,"entfs","vfat"); + + zdialog_resize(zd,300,0); + zdialog_run(zd); // run dialog + zstat = zdialog_wait(zd); + zdialog_free(zd); + if (zstat != 1) return 0; + + zdialog_fetch(zd,"entdev",device,19); // get chosen device and file system + zdialog_fetch(zd,"entfs",filesys,19); + zdialog_fetch(zd,"entlab",label,19); + + for (ii = 1; device[ii] > ' '; ii++); // strip off device description + if (ii > 19) ii = 19; + device[ii] = 0; + + yn = zmessageYN(mWin,"device: %s label: %s file sys: %s \n" + "WARNING: all data will be lost! \n" + "Proceed with formatting?",device,label,filesys); + if (! yn) goto format_exit; + + wprintf(mLog," formatting %s with file system %s \n",device,filesys); + + fid = fopen(TFformatscript,"w"); + if (! fid) { + wprintf(mLog," *** cannot create format script file \n"); + goto format_exit; + } + + gdk_window_set_cursor(mLogwin,watchcursor); // busy cursor v.4.1 + + fprintf(fid,"umount %s \n",device); // unmount /dev/xxxx + fprintf(fid,"sleep 2 \n"); + if (*filesys == 'v') + fprintf(fid,"mkfs -t vfat -F 32 -n %s %s \n",label,device); // make vfat file system + if (*filesys == 'e') + fprintf(fid,"mkfs -t ext2 -L %s %s \n",label,device); // or ext2 file system + fprintf(fid,"exit 0 \n"); + fclose(fid); + chmod(TFformatscript,0744); + + while ((crec = command_output(contx,TFformatscript))) // v.3.3.1 + { + zsleep(0.1); // throttle a little + for (ii = jj = 0; crec[jj]; jj++) + { // get rid of weird characters + if (crec[jj] < ' ') continue; // in mkfs output + crec[ii] = crec[jj]; + ii++; + } + crec[ii] = 0; + wprintf(mLog," format: %s \n",crec); // print command output + } + +format_exit: + wprintf(mLog," ready \n"); // v.3.6 + gdk_window_set_cursor(mLogwin,0); // normal cursor + return 0; +} + + +// display help/about or help/contents + +int helpFunc(cchar *menu) +{ + if (strEqu(menu,"about")) { + wprintf(mLog," %s \n",ukopp_title); + wprintf(mLog," free software: %s \n",ukopp_license); + } + + if (strEqu(menu,"contents")) showz_userguide(); + return 0; +} + + +// Mount target device. Return 1 if success, else 0. +// menu caller: menu arg is present +// internal caller: menu arg is 0 + +int mount(cchar *menu) // more error checking v.3.5 +{ + int ii, err, cc; + char ch, work[300]; + const char *errmess; + struct stat statb; + + bFilesReset(); // clear file data at backup location + BDpoop(); // refresh device data + + snprintf(work,299,"%s %s",BJdev,BJdirk); + errmess = parseTarget(work); // target device and directory + if (errmess) { // in conflict with current + wprintf(mLog," *** %s \n",errmess); // mount status + return 0; + } + + for (ii = 0; ii < Ndisk; ii++) // see if device is mounted + if (strEqu(BJdev,diskdev[ii])) break; + if (ii < Ndisk && *diskmp[ii] == '/') { // yes + cc = strlen(diskmp[ii]); + if (strnEqu(diskmp[ii],BJdirk,cc)) { + ch = BJdirk[cc]; + if (! ch || ch == '/') { + devMounted = 1; // target directory is on device + if (menu) wprintf(mLog," already mounted \n"); + return 1; + } + } + wprintf(mLog," *** target directory not on device \n"); + } + + err = stat(BJdirk,&statb); // directory exists? + if (err && *BJdev) { // device but no directory + snprintf(work,299,"mkdir -p %s",BJdirk); // create mount point + err = do_shell("mkdir",work); + if (err) return 0; + ukoppMpoint++; // remember created by me + } + + if (! err && ! *BJdev) return 1; // no device, directory OK, use it + + snprintf(work,299,"mount -noatime %s %s",BJdev,BJdirk); // mount device at target directory + err = do_shell("mount",work); + if (err) return 0; + + ukoppMounted++; // remember mounted by me + devMounted = 1; + strcpy(mountdev,BJdev); // save mount poop + strcpy(mountdirk,BJdirk); + return 1; +} + + +// unmount target device + +int unmount(cchar *menu) // revised v.3.5.1 +{ + int err; + char work[200]; + struct stat statb; + + bFilesReset(); // no files at backup location + + sleep(1); + if (*mountdirk) snprintf(work,199,"umount %s",mountdirk); // unmount unconditionally + else snprintf(work,199,"umount %s",BJdev); + do_shell("umount",work); + + sleep(1); + err = stat(mountdirk,&statb); // remove directory + if (! err && ukoppMpoint) { // only if it exists + snprintf(work,199,"rmdir %s",mountdirk); // and created by me + do_shell("rmdir",work); + } + + devMounted = ukoppMounted = ukoppMpoint = 0; + *mountdev = *mountdirk = 0; + BDpoop(); // refresh device data + return 0; +} + + +// save logging window as text file + +int saveScreen(cchar *menu) +{ + wfilesave(mLog); + return 0; +} + + +// backup helper function +// write date and time to temp file + +int writeDT() +{ + time_t dt1; + char *dt2; + FILE *fid; + int cc; + + time(&dt1); + dt2 = ctime(&dt1); // get string date-time + cc = strlen(dt2); + if (cc && (dt2[cc-1] == '\n')) dt2[cc-1] = 0; // save without trailing \n + + fid = fopen(TFdatetime,"w"); + if (! fid) zappcrash("cannot open scratch file %s",TFdatetime); + + fprintf(fid,"%s \n",dt2); + fclose(fid); + return 0; +} + + +// synchronize owner and permissions data using poopfile at backup location v.26 +// - for files copied backup >> disk, set owner and permissions from poopfile +// - refresh poopfile data from disk files +// mode is "backup" "restore" or "synch" + +int synch_poop(const char *mode) +{ + int ii, err, nn, uid, gid, perms; + int cc, ccf, cct; + char file[maxfcc], file2[maxfcc]; + char dirk[maxfcc], pdirk[maxfcc]; + char *pp, poopfile[100]; + const char *errmess = 0; + FILE *fid; + struct stat64 dstat; + + if (strEqu(mode,"synch")) // set poop for updated disk files + { + strcpy(poopfile,BJdirk); + strcat(poopfile,BD_POOPFILE); + fid = fopen(poopfile,"r"); // open poopfile + if (! fid) { + wprintf(mLog," *** no owner/permissions file: %s \n",poopfile); + return 0; + } + + ii = 0; + + while (true) // read poopfile records + { + nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file); // uid, gid, perms, file or directory + if (nn == EOF) break; + if (nn != 4) continue; + + cc = strlen(file); + + while (ii < Bnf) // match poopfile file or directory + { // to backup files copied to disk + nn = strncmp(Brec[ii].file,file,cc); // (logic assumes ascii sort) + if (nn >= 0) break; + ii++; + } + + if (ii == Bnf) break; // EOL + if (nn > 0) continue; // file not in backup file list + if (Brec[ii].finc == 0) continue; // file not copied to disk + + wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file); + err = chown(file,uid,gid); + if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); + err = chmod(file,perms); + if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); + } + + fclose(fid); + } + + if (strEqu(mode,"restore")) // set poop for restored disk files + { + strcpy(poopfile,BJdirk); + strcat(poopfile,BD_POOPFILE); + fid = fopen(poopfile,"r"); + if (! fid) { + wprintf(mLog," *** no owner/permissions file: %s \n",poopfile); + return 0; + } + + ccf = strlen(RJfrom); + cct = strlen(RJto); + ii = 0; + + while (true) + { + nn = fscanf(fid,"%d:%d %o %[^\n]",&uid,&gid,&perms,file); + if (nn == EOF) break; + if (nn != 4) continue; + + cc = strlen(file); + if (cc <= ccf) continue; + + while (ii < Rnf) + { + nn = strncmp(Rrec[ii].file,file,cc); + if (nn >= 0) break; + ii++; + } + + if (ii == Rnf) break; + if (nn > 0) continue; + if (Rrec[ii].finc == 0) continue; + + strcpy(file2,RJto); // offset restore 'from' and 'to' paths + strcpy(file2 + cct, file + ccf); + + wprintf(mLog," set owner/perms %d:%d %04o %s \n",uid,gid,perms,file2); + err = chown(file2,uid,gid); + if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); + err = chmod(file2,perms); + if (err) wprintf(mLog," *** error: %s \n",strerror(errno)); + } + + fclose(fid); + } + + if (strEqu(mode,"backup") || strEqu(mode,"synch")) // make new poop file from disk files + { + fid = fopen(TFpoopfile,"w"); + if (! fid) zappcrash("cannot open temp file %s",TFpoopfile); + + *pdirk = 0; // no prior directory + + for (ii = 0; ii < Dnf; ii++) + { + strcpy(dirk,Drec[ii].file); // next file on disk + pp = dirk; + + while (true) // set directory owner & permissions + { + pp = strchr(pp+1,'/'); // next (last) directory level + if (! pp) break; + cc = pp - dirk + 1; // cc incl. '/' + if (strncmp(dirk,pdirk,cc) == 0) continue; // matches prior, skip + + *pp = 0; // terminate this directory level + + err = lstat64(dirk,&dstat); // get owner and permissions v.3.0 + if (err) { + wprintf(mLog," *** error: %s file: %s \n",strerror(errno),dirk); + break; + } + + dstat.st_mode = dstat.st_mode & 0777; + + fprintf(fid,"%4d:%4d %3o %s/\n", // output uid:gid perms directory/ + dstat.st_uid, dstat.st_gid, dstat.st_mode, dirk); + + *pp = '/'; // restore '/' + } + + strcpy(pdirk,dirk); // prior = this directory + + strcpy(file,Drec[ii].file); // disk file, again + + err = lstat64(file,&dstat); // get owner and permissions v.3.0 + if (err) { + wprintf(mLog," *** error: %s file: %s \n",strerror(errno),file); + continue; + } + + dstat.st_mode = dstat.st_mode & 0777; + + fprintf(fid,"%4d:%4d %3o %s\n", // output uid:gid perms file + dstat.st_uid, dstat.st_gid, dstat.st_mode, file); + } + + fclose(fid); + + errmess = copyFile(TFpoopfile,BD_POOPFILE,2); // copy file owner/permissions file + if (errmess) wprintf(mLog," *** poopfile error: %s \n",errmess); + } + + return 0; +} + + +// get all disk files specified by include/exclude records +// save in Drec[] array + +int dGetFiles() +{ + const char *fsp, *psep2; + char *fspec, *pp, *psep1; + int ftf, wstat, cc, err, dups; + int rtype, ii, jj, st, nfiles; + int fcc, vers; + double nbytes; + struct stat64 filestat; + + dFilesReset(); + wprintx(mLog,0,"\n""generating backup file set \n",boldfont); + + for (ii = 0; ii < BJnnx; ii++) // process include/exclude recs + { + BJfiles[ii] = 0; // initz. include/exclude rec stats + BJbytes[ii] = 0.0; + + if (! BJfspec[ii]) continue; // v.4.6 + + if (*BJfspec[ii] == '"') { // v.4.1 + fspec = strdupz(BJfspec[ii]+1); // unquote quoted filespec + pp = strrchr(fspec,'"'); + if (pp) *pp = 0; + } + else fspec = strdupz(BJfspec[ii]); + + rtype = BJrtype[ii]; + + if (rtype == 2 || rtype == 3) // include or exclude filespec + { + err = stat64(fspec,&filestat); + if (! err && S_ISDIR(filestat.st_mode)) { // if directory, append /* v.4.1 + cc = strlen(fspec) - 1; // (BJfspec has the extra length) + if (fspec[cc] != '/') cc++; + strcpy(fspec+cc,"/*"); + } + } + + if (rtype == 2) // include filespec + { + ftf = 1; + + while (1) + { + fsp = SearchWild(fspec,ftf); // find matching files + if (! fsp) break; + + Drec[Dnf].file = strdupz(fsp); + + err = lstat64(fsp,&filestat); // check accessibility + if (! err) { + Drec[Dnf].err = 0; + if (! S_ISREG(filestat.st_mode) && // reg. files + symlinks only v.3.0 + ! S_ISLNK(filestat.st_mode)) continue; + } + else Drec[Dnf].err = errno; // save file error status + + fcc = strlen(fsp); + psep1 = (char *) strstr(fsp+fcc-10,VSEP1); // look for file version v.3.2 + if (psep1) { // (char *) fix gcc error v.3.4.1 + vers = 0; + st = convSI(psep1+2,vers,&psep2); // if format not valid, take + if (st < 2) vers = 1; // as non-versioned file + if (strNeq(psep2,VSEP2)) vers = 0; + if (*(psep2+1)) vers = 0; // VSEP2 must be at end + if (vers) { + wprintf(mLog," *** omit versioned file: %s \n",fsp); + continue; + } + } + + Drec[Dnf].jindx = ii; // save pointer to include record + Drec[Dnf].size = filestat.st_size; // save file size + Drec[Dnf].mtime = filestat.st_mtime // save last mod time + + filestat.st_mtim.tv_nsec * nano; // (nanosec resolution) + if (err) Drec[Dnf].size = Drec[Dnf].mtime = 0; // inaccessible file + Drec[Dnf].finc = 0; // not copied yet + Drec[Dnf].bindx = -1; // no link to backup record yet v.4.0 + + BJfiles[ii]++; // count included files and bytes + BJbytes[ii] += Drec[Dnf].size; + + if (++Dnf == maxfs) { + wprintf(mLog," *** max files exceeded \n"); + goto errret; // v.4.4 + } + } + } + + if (rtype == 3) // exclude filespec + { + for (jj = 0; jj < Dnf; jj++) // check all included files (SO FAR) + { + if (! Drec[jj].file) continue; + wstat = MatchWild(fspec,Drec[jj].file); + if (wstat != 0) continue; + BJfiles[ii]--; // un-count excluded file and bytes + BJbytes[ii] -= Drec[jj].size; + zfree(Drec[jj].file); // clear file data entry + Drec[jj].file = 0; + Drec[jj].err = 0; + } + } + + zfree(fspec); // v.4.1 + } // end of include/exclude recs + + for (ii = 0; ii < Dnf; ii++) // list and remove error files + { // (after excluded files removed) + if (Drec[ii].err) { + wprintf(mLog," *** %s omit: %s \n",strerror(Drec[ii].err),Drec[ii].file); + jj = Drec[ii].jindx; + BJfiles[jj]--; // un-count file and bytes + BJbytes[jj] -= Drec[ii].size; + zfree(Drec[ii].file); + Drec[ii].file = 0; + } + } + + ii = jj = 0; // repack file arrays after deletions + while (ii < Dnf) + { + if (Drec[ii].file == 0) ii++; + else { + if (ii > jj) { + if (Drec[jj].file) zfree(Drec[jj].file); + Drec[jj] = Drec[ii]; + Drec[ii].file = 0; + } + ii++; + jj++; + } + } + + Dnf = jj; // final file count in backup set + + Dbytes = 0.0; + for (ii = 0; ii < Dnf; ii++) Dbytes += Drec[ii].size; // compute total bytes from files + + nfiles = 0; + nbytes = 0.0; + + for (ii = 0; ii < BJnnx; ii++) // compute total files and bytes + { // from include/exclude recs + nfiles += BJfiles[ii]; + nbytes += BJbytes[ii]; + } + + wprintf(mLog," disk files: %d %s \n",nfiles,formatKBMB(nbytes,3)); + + if ((nfiles != Dnf) || (Dbytes != nbytes)) { // must match + wprintf(mLog," *** bug: nfiles: %d Dnf: %d \n",nfiles,Dnf); + wprintf(mLog," nbytes: %.0f Dbytes: %.0f \n",nbytes,Dbytes); + goto errret; + } + + SortFileList((char *) Drec,sizeof(dfrec),Dnf,'A'); // sort Drec[Dnf] by Drec[].file + + for (ii = dups = 0; ii < Dnf-1; ii++) // look for duplicate files + if (strEqu(Drec[ii].file,Drec[ii+1].file)) { + wprintf(mLog," *** duplicate file: %s \n",Drec[ii].file); + dups++; + } + + if (dups) goto errret; + return 0; + +errret: + BJvalid = 0; + dFilesReset(); + return 0; +} + + +// get existing files at backup location, save in Brec[] array +// return -1 if error, else count of backup files +// +// Linux sort command: +// '.' sorts before ' ' (0x2E < 0x20, which is crazy) +// Workaround implemented. + +int bGetFiles() +{ + int gcc, fcc, err, vers, vfound, jj; + int bb, bbp, rtype, noret = 0; + int lover, hiver, retND, retNV; + char command[300], *pp, *psep1; + char bfile[maxfcc], *bfile2; + double fage; + const char *psep2; + FILE *fid; + struct stat64 filestat; + + bFilesReset(); // reset backup file list + if (! mount(0)) return 0; // validate and mount target v.3.2 + + wprintx(mLog,0,"\n""find all files at backup location \n",boldfont); + + sprintf(command,"find %s -type f -or -type l >%s",BJdirk,TFbakfiles); // backup filespecs to temp file v.3.0 + err = do_shell("find",command); + if (err) return -1; + + // read filespecs into memory and use memory sort instead of linux sort utility + // (apparently cannot do a straight ascii sort, even with LC_ALL=C) + + gcc = strlen(BD_UKOPPDIRK); // directory for ukopp special files + + fid = fopen(TFbakfiles,"r"); // read file list + if (! fid) zappcrash("cannot open scratch file %s",TFbakfiles); + + for (bb = 0; bb < maxfs; ) // loop all files at backup location + { + pp = fgets_trim(bfile,maxfcc-1,fid); // next file + if (! pp) break; // eof + + bfile2 = bfile + BJdcc; // remove backup mount point + if (strnEqu(bfile2,BD_UKOPPDIRK,gcc)) continue; + + fcc = strlen(bfile2); + if (fcc > maxfcc-BJdcc-10) { // cannot handle files near limit + wprintf(mLog," *** filespec too big, omit: %s...",bfile2); + wprintf(mLog,"\n"); + continue; + } + + err = lstat64(bfile,&filestat); // check accessibility + if (err) { + wprintf(mLog," *** %s, omit: %s",strerror(errno),bfile2); + wprintf(mLog,"\n"); + continue; + } + else if (! S_ISREG(filestat.st_mode) && // reg. files and symlinks only v.3.0 + ! S_ISLNK(filestat.st_mode)) continue; + + // build memory record for file data + + Brec[bb].file = strdupz(bfile2); // filespec + Brec[bb].err = 0; + Brec[bb].size = filestat.st_size; // file size + Brec[bb].mtime = filestat.st_mtime // last mod time + + filestat.st_mtim.tv_nsec * nano; + Brec[bb].lover = Brec[bb].hiver = Brec[bb].nexpv = 0; // no versions yet + Brec[bb].finc = 0; // no backup yet + bb++; + } + + fclose (fid); + + Bnf = bb; + wprintf(mLog," %6d backup files \n",Bnf); + + if (Bnf == maxfs) { + wprintf(mLog," *** max files exceeded \n"); + bFilesReset(); + return -1; + } + + SortFileList((char *) Brec,sizeof(bfrec),Bnf,'A'); // sort Brec[Bnf] by Brec[].file + + for (bb = 0, bbp = -1; bb < Bnf; bb++) // loop all files + { + bfile2 = Brec[bb].file; + fcc = strlen(bfile2); + + vers = 0; + psep1 = strstr(bfile2+fcc-10,VSEP1); // look for file version + if (psep1) { + err = convSI(psep1+2,vers,1,9999,&psep2); // if format not valid, + if (err > 1) vers = 0; // assume a current file (vers 0) + if (strNeq(psep2,VSEP2)) vers = 0; + if (*(psep2+1)) vers = 0; // VSEP2 must be at end v.3.2 + if (vers) *psep1 = 0; // remove version from file name + } + + if (! vers) // a current file, not prior version + { + bbp++; // add new file record + Brec[bbp] = Brec[bb]; // copy all data + } + + if (vers) // a prior version, 1-9999 + { + if (bbp > -1 && strEqu(Brec[bbp].file,bfile2)) { // look back for match with prior file + if (Brec[bbp].lover == 0) Brec[bbp].lover = vers; // set first version found + if (vers < Brec[bbp].lover) Brec[bbp].lover = vers; // (10) sorts before (9) + if (vers > Brec[bbp].hiver) Brec[bbp].hiver = vers; // track lowest and highest vers. found + zfree(bfile2); // free duplicate filespec + } + else { // version present, but no curr. file + bbp++; // add new file record + Brec[bbp] = Brec[bb]; // copy all data + Brec[bbp].err = -1; // mark file (vers 0) not present + Brec[bbp].size = Brec[bbp].mtime = 0; + Brec[bbp].lover = Brec[bbp].hiver = vers; // track prior versions present + } + } + } + + Bnf = bbp + 1; + + for (bb = 0; bb < Bnf; bb++) // loop all files at backup location + { + strcpy(bfile,BJdirk); + strcat(bfile,Brec[bb].file); + bfile2 = bfile + BJdcc; + + if (BJnnx > 0) { + for (jj = 0; jj < BJnnx; jj++) { // find matching backup include rec. + rtype = BJrtype[jj]; + if (rtype != 2) continue; + if (MatchWild(BJfspec[jj],bfile2) == 0) break; + } + if (jj == BJnnx) { // this file not in backup set + Brec[bb].retND = Brec[bb].retNV = 0; // no retention specs + noret++; + } + else { + Brec[bb].retND = BJretND[jj]; // get corresp. retention specs v.3.5 + Brec[bb].retNV = BJretNV[jj]; + } + } + + if (Brec[bb].err == 0) { + Cfiles++; // count curr. version files + Cbytes += Brec[bb].size; // and total bytes + } + + if (Brec[bb].lover == 0) continue; // no versions present + + lover = Brec[bb].lover; // version range found + hiver = Brec[bb].hiver; + retND = Brec[bb].retND; // retention days + retNV = Brec[bb].retNV; // retention versions + + if (! retND) retND = -1; // zero days retention, defeat test + vfound = 0; // versions found + + for (vers = hiver; vers >= lover; vers--) // loop file version, high to low + { // v.4.0 + setFileVersion(bfile,vers); + err = lstat64(bfile,&filestat); // check file exists on backup + if (err) { + wprintf(mLog," *** version %d missing: %s \n",vers,bfile2); + continue; + } + + vfound++; // this file, versions found + Vfiles++; // total versioned files and bytes + Vbytes += filestat.st_size; + + fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file version age in days + if (fage <= retND || vfound <= retNV) continue; // this version is to be retained + Brec[bb].nexpv++; // count expired versions v.4.0 + Pfiles++; // total expired files and bytes + Pbytes += filestat.st_size; // (to be purged) + } + } + + wprintf(mLog," %6d files not in backup set (unknown retention) \n",noret); + wprintf(mLog," %6d (%s) curr. file versions \n",Cfiles,formatKBMB(Cbytes,3)); + wprintf(mLog," %6d (%s) prior file versions \n",Vfiles,formatKBMB(Vbytes,3)); + wprintf(mLog," %6d (%s) expired prior versions \n",Pfiles,formatKBMB(Pbytes,3)); + + sprintf(command,"df -h %s",mountdirk); // v.4.2 + do_shell("target space",command); + + return Bnf; +} + + +// get all restore files specified by include/exclude records +// save in Rrec[] array + +int rGetFiles() +{ + int ii, jj, cc, rtype, wstat, ninc, nexc; + char *fspec; + + if (! RJvalid) return 0; + rFilesReset(); // clear restore files + if (bGetFiles() < 1) return 0; // get backup files + + wprintf(mLog,"\n generating restore file set \n"); + + for (ii = 0; ii < RJnnx; ii++) // process include/exclude recs + { + rtype = RJrtype[ii]; + fspec = RJfspec[ii]; + + if (rtype == 2) // include filespec + { + wprintf(mLog," include %s \n",fspec); + + for (ninc = jj = 0; jj < Bnf; jj++) // screen all files in backup loc. + { + wstat = MatchWild(fspec,Brec[jj].file); + if (wstat != 0) continue; + if (Brec[jj].err) continue; + Rrec[Rnf].file = strdupz(Brec[jj].file); // add matching files + Rrec[Rnf].finc = 0; + Rnf++; ninc++; + if (Rnf == maxfs) { + wprintf(mLog," *** max files exceeded \n"); + rFilesReset(); // v.4.4 + return 0; + } + } + + wprintf(mLog," %d files added \n",ninc); + } + + if (rtype == 3) // exclude filespec + { + wprintf(mLog," exclude %s \n",fspec); + + for (nexc = jj = 0; jj < Rnf; jj++) // check all included files (SO FAR) + { + if (! Rrec[jj].file) continue; + + wstat = MatchWild(fspec,Rrec[jj].file); + if (wstat != 0) continue; + zfree(Rrec[jj].file); // remove matching files + Rrec[jj].file = 0; + nexc++; + } + + wprintf(mLog," %d files removed \n",nexc); + } + } + + ii = jj = 0; // repack after deletions + while (ii < Rnf) + { + if (Rrec[ii].file == 0) ii++; + else + { + if (ii > jj) + { + if (Rrec[jj].file) zfree(Rrec[jj].file); + Rrec[jj].file = Rrec[ii].file; + Rrec[ii].file = 0; + } + ii++; + jj++; + } + } + + Rnf = jj; + wprintf(mLog," total file count: %d \n",Rnf); + + cc = strlen(RJfrom); // copy from: /dirk/.../ + + for (ii = 0; ii < Rnf; ii++) // get selected backup files to restore + { + if (! strnEqu(Rrec[ii].file,RJfrom,cc)) { + wprintf(mLog," *** not within copy-from; %s \n",Rrec[ii].file); + RJvalid = 0; // mark restore job invalid + continue; + } + } + + SortFileList((char *) Rrec,sizeof(rfrec),Rnf,'A'); // sort Rrec[Rnf] by Rrec[].file + return 0; +} + + +// helper function for backups and reports +// +// compare disk and backup files, set disp in Drec[] and Brec[] arrays: +// n new on disk, not on backup +// d deleted on backup, not on disk +// m modified on both, but not equal +// u unchanged on both, and equal +// v versions on backup, only prev. versions are present + +int setFileDisps() +{ + int dii, bii, comp; + char disp; + double diff; + + dii = bii = 0; + nnew = nmod = nunc = ndel = comp = 0; + Mbytes = 0.0; // total bytes, new and modified files + + while ((dii < Dnf) || (bii < Bnf)) // scan disk and backup files parallel + { + if ((dii < Dnf) && (bii == Bnf)) comp = -1; + else if ((dii == Dnf) && (bii < Bnf)) comp = +1; + else comp = strcmp(Drec[dii].file, Brec[bii].file); + + if (comp < 0) { // unmatched disk file + Drec[dii].disp = 'n'; // new + nnew++; // count new files + Mbytes += Drec[dii].size; // accumulate Mbytes + Drec[dii].bindx = -1; // no matching backup file + dii++; + } + + else if (comp > 0) { // unmatched backup file + if (Brec[bii].err == 0) { // if current version is present, + Brec[bii].disp = 'd'; // file was deleted from disk + ndel++; // count deleted files + } + else Brec[bii].disp = 'v'; // only old versions on backup + bii++; + } + + else if (comp == 0) { // file present on disk and backup + Drec[dii].bindx = bii; // link disk to backup record v.4.0 + if (Brec[bii].err == 0) { + diff = Drec[dii].mtime - Brec[bii].mtime; // check if equal mod times + if (fabs(diff) > MODTIMETOLR) disp = 'm'; + else disp = 'u'; // yes, assume unchanged + Drec[dii].disp = Brec[bii].disp = disp; + if (disp == 'u') nunc++; // count unchanged files + if (disp == 'm') nmod++; // count modified files + if (disp == 'm') Mbytes += Drec[dii].size; // and accumulate Mbytes + } + else { + Brec[bii].disp = 'v'; // only old versions on backup + Drec[dii].disp = 'n'; // disk file is logically new + nnew++; // count new files + Mbytes += Drec[dii].size; // accumulate Mbytes + } + dii++; + bii++; + } + } + + Mfiles = nnew + nmod + ndel; + return 0; +} + + +// Sort file list in memory (disk files, backup files, restore files). +// Sort ascii sequence, or sort subdirectories in a directory before files. + +int SortFileList(char *recs, int RL, int NR, char sort) +{ + HeapSortUcomp fcompA, fcompD; // filespec compare funcs + if (sort == 'A') HeapSort(recs,RL,NR,fcompA); // ascii compare + if (sort == 'D') HeapSort(recs,RL,NR,fcompD); // special compare (directories first) + return 0; +} + +int fcompA(cchar *rec1, cchar *rec2) // ascii comparison +{ // current file (no version) sorts first + dfrec *r1 = (dfrec *) rec1; + dfrec *r2 = (dfrec *) rec2; + return strcmp(r1->file,r2->file); +} + +int fcompD(cchar *rec1, cchar *rec2) // special compare filenames +{ // subdirectories in a directory compare + dfrec *r1 = (dfrec *) rec1; // less than files in the directory + dfrec *r2 = (dfrec *) rec2; + return filecomp(r1->file,r2->file); +} + +int filecomp(cchar *file1, cchar *file2) // special compare filenames +{ // subdirectories compare before files + cchar *pp1, *pp10, *pp2, *pp20; + cchar slash = '/'; + int cc1, cc2, comp; + + pp1 = file1; // first directory level or file + pp2 = file2; + + while (true) + { + pp10 = strchr(pp1,slash); // find next slash + pp20 = strchr(pp2,slash); + + if (pp10 && pp20) { // both are directories + cc1 = pp10 - pp1; + cc2 = pp20 - pp2; + if (cc1 < cc2) comp = strncmp(pp1,pp2,cc1); // compare the directories + else comp = strncmp(pp1,pp2,cc2); + if (comp) return comp; + else if (cc1 != cc2) return (cc1 - cc2); + pp1 = pp10 + 1; // equal, check next level + pp2 = pp20 + 1; + continue; + } + + if (pp10 && ! pp20) return -1; // only one is a directory, + if (pp20 && ! pp10) return 1; // the directory is first + + comp = strcmp(pp1,pp2); // both are files, compare + return comp; + } +} + + +// reset all backup job data and free allocated memory + +int BJreset() +{ + for (int ii = 0; ii < BJnnx; ii++) + if (BJfspec[ii]) zfree(BJfspec[ii]); + + BJnnx = BJvalid = 0; + BJvmode = 0; + dFilesReset(); // reset dependent disk file data + return 0; +} + + +// reset all restore job data and free allocated memory + +int RJreset() +{ + for (int ii = 0; ii < RJnnx; ii++) + if (RJfspec[ii]) zfree(RJfspec[ii]); + + RJvalid = RJnnx = 0; + rFilesReset(); // reset dependent disk file data + return 0; +} + + +// reset all file data and free allocated memory + +int dFilesReset() +{ // disk files data + for (int ii = 0; ii < Dnf; ii++) + { + zfree(Drec[ii].file); + Drec[ii].file = 0; + } + + Dnf = 0; + Dbytes = Mbytes = 0.0; + return 0; +} + +int bFilesReset() +{ // backup files data + for (int ii = 0; ii < Bnf; ii++) + { + zfree(Brec[ii].file); + Brec[ii].file = 0; + } + + Bbytes = Bnf = 0; + Cbytes = Cfiles = 0; + Mbytes = Mfiles = 0; + Vbytes = Vfiles = 0; + Pbytes = Pfiles = 0; + return 0; +} + +int rFilesReset() +{ // restore files data + for (int ii = 0; ii < Rnf; ii++) + { + zfree(Rrec[ii].file); + Rrec[ii].file = 0; + } + + Rnf = 0; + return 0; +} + + +// Helper function to copy a file between disk and backup location. +// Owner and permissions are transferred for copied files and directories, +// but this will do nothing in case target is VFAT (Microsoft) file system. + +cchar * copyFile(cchar *sfile, cchar *dfile, int mpf) +{ + char file1[maxfcc], file2[maxfcc]; + int fid1, fid2, err, rcc, dlevs; + char *pp1, *pp2, buff[BIOCC]; + const char *errmess = 0; + struct stat64 fstat1, fstat2; + struct timeval ftimes[2]; + + *file1 = *file2 = 0; + if (mpf == 1) strcpy(file1,BJdirk); // prepend mount point if req. + strcat(file1,sfile); + if (mpf == 2) strcpy(file2,BJdirk); + strcat(file2,dfile); + + pp2 = file2; + dlevs = 0; + + while (true) { // v.25 + pp2 = strchr(pp2+1,'/'); // create missing directory levels + if (! pp2) break; // (check and create from top down) + *pp2 = 0; + err = stat64(file2,&fstat2); + if (err) { + err = mkdir(file2,0731); + if (err) return strerror(errno); + dlevs++; + } + *pp2 = '/'; + } + + while (dlevs) { // v.25 + pp1 = (char *) strrchr(file1,'/'); // for created output directories, + if (! pp1) break; // copy owner and permissions from + pp2 = (char *) strrchr(file2,'/'); // corresponding input directory + if (! pp2) break; // (measured from bottom up) + *pp1 = *pp2 = 0; // (possibly top levels not set) + err = stat64(file1,&fstat1); + if (err) return strerror(errno); + chmod(file2,fstat1.st_mode); + err = chown(file2,fstat1.st_uid,fstat1.st_gid); + if (err) wprintf(mLog,"error: %s \n",wstrerror(err)); + dlevs--; + } + + *file1 = *file2 = 0; + if (mpf == 1) strcpy(file1,BJdirk); // refresh filespecs + strcat(file1,sfile); + if (mpf == 2) strcpy(file2,BJdirk); + strcat(file2,dfile); + + err = lstat64(file1,&fstat1); // get input file attributes v.3.0 + if (err) return strerror(errno); + + if (S_ISLNK(fstat1.st_mode)) { // input file is symlink + rcc = readlink(file1,buff,maxfcc); + if (rcc < 0 || rcc > maxfcc-2) return strerror(errno); + buff[rcc] = 0; + err = symlink(buff,file2); // create output symlink + if (err) return strerror(errno); + ftimes[0].tv_sec = fstat1.st_atime; // get input file access time v.3.0 + ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000; // in microsecs. + ftimes[1].tv_sec = fstat1.st_mtime; + ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000; + lutimes(file2,ftimes); // set output file access time + return 0; + } + + fid1 = open(file1,O_RDONLY+O_NOATIME+O_LARGEFILE); // open input file + if (fid1 == -1) return strerror(errno); + + fid2 = open(file2,O_WRONLY+O_CREAT+O_TRUNC+O_LARGEFILE,0700); // open output file + if (fid2 == -1) { + errmess = strerror(errno); + close(fid1); + return errmess; + } + + while (true) + { + rcc = read(fid1,buff,BIOCC); // read huge blocks + if (rcc == 0) break; + if (rcc == -1) { + errmess = strerror(errno); + close(fid1); + close(fid2); + return errmess; + } + + rcc = write(fid2,buff,rcc); // write blocks + if (rcc == -1) { + errmess = strerror(errno); + close(fid1); + close(fid2); + return errmess; + } + + if (checkKillPause()) break; + } + + close(fid1); // close input file + err = fsync(fid2); // flush output file v.4.4 + if (err) return strerror(errno); + err = close(fid2); // close output file + if (err) return strerror(errno); + + err = lstat64(file1,&fstat1); // get input file attributes + if (err) return strerror(errno); + + chmod(file2,fstat1.st_mode); // copy owner and permissions + err = chown(file2,fstat1.st_uid,fstat1.st_gid); // from input to output file + if (err) wprintf(mLog,"error: %s \n",wstrerror(err)); + + ftimes[0].tv_sec = fstat1.st_atime; // get input file access time + ftimes[0].tv_usec = fstat1.st_atim.tv_nsec / 1000; // in microsecs. + ftimes[1].tv_sec = fstat1.st_mtime; + ftimes[1].tv_usec = fstat1.st_mtim.tv_nsec / 1000; + utimes(file2,ftimes); // set output file access time + + return 0; +} + + +// Verify helper function +// Verify that file on backup medium is readable, return its length. +// Optionally compare backup file to disk file, byte for byte. +// returns error message or null if OK. + +cchar * checkFile(cchar *dfile, int compf, double &tcc) +{ + int vfid = 0, dfid = 0; + int err, vcc, dcc, cmperr = 0; + char vfile[maxfcc], *vbuff = 0, *dbuff = 0; + const char *errmess = 0; + double dtime, vtime; + int open_flagsV = O_RDONLY+O_NOATIME+O_LARGEFILE+O_DIRECT; // bypass cache v.3.5.2 + int open_flagsD = O_RDONLY+O_NOATIME+O_LARGEFILE; // use cache normally v.3.5.3 + struct stat64 filestat; + + tcc = 0.0; + + strcpy(vfile,BJdirk); // prepend mount point + strcat(vfile,dfile); + + lstat64(vfile,&filestat); // if symlink, check readable v.3.1 + if (S_ISLNK(filestat.st_mode)) { + vbuff = (char *) malloc(maxfcc); + vcc = readlink(vfile,vbuff,maxfcc); + if (vcc == -1) errmess = strerror(errno); + goto cleanup; + } + + if (compf) goto comparefiles; + + vfid = open(vfile,open_flagsV); // open for read, large blocks, direct I/O + if (vfid == -1) goto checkerr; + + err = posix_memalign((void**) &vbuff,512,BIOCC); // use aligned buffer + if (err) zappcrash("memory allocation failure"); + + while (1) + { + vcc = read(vfid,vbuff,BIOCC); + if (vcc == 0) break; + if (vcc == -1) { errmess = strerror(errno); break; } + tcc += vcc; // accumulate length + if (checkKillPause()) break; + } + goto cleanup; + +comparefiles: + + vfid = open(vfile,open_flagsV); // open for read, large blocks, direct I/O + if (vfid == -1) goto checkerr; + + dfid = open(dfile,open_flagsD); // disk files, use cached I/O + if (dfid == -1) goto checkerr; + + err = posix_memalign((void**) &vbuff,512,BIOCC); // use aligned buffers + if (err) zappcrash("memory allocation failure"); + err = posix_memalign((void**) &dbuff,512,BIOCC); + if (err) zappcrash("memory allocation failure"); + + while (1) + { + vcc = read(vfid,vbuff,BIOCC); // read two files + if (vcc == -1) { errmess = strerror(errno); goto cleanup; } + + dcc = read(dfid,dbuff,BIOCC); + if (dcc == -1) { errmess = strerror(errno); goto cleanup; } + + if (vcc != dcc) cmperr++; // compare buffers + if (memcmp(vbuff,dbuff,vcc)) cmperr++; + + tcc += vcc; // accumulate length + if (vcc == 0) break; + if (dcc == 0) break; + + if (checkKillPause()) break; + } + + if (vcc != dcc) cmperr++; + + if (cmperr) { // compare error + lstat64(dfile,&filestat); // v.3.0 + dtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; // file modified since snapshot? + lstat64(vfile,&filestat); // v.3.0 + vtime = filestat.st_mtime + filestat.st_mtim.tv_nsec * nano; + if (fabs(dtime-vtime) < MODTIMETOLR) errmess = "compare error"; // no, a real compare error + } + +cleanup: + if (vfid) close(vfid); + if (dfid) close(dfid); + if (vbuff) free(vbuff); + if (dbuff) free(dbuff); + return errmess; + +checkerr: // v.3.7 + errmess = strerror(errno); + if (errno == EINVAL || errno == ENOTSUP) { + if (Fgui) zmessageACK(mWin,0,"large block direct I/O not allowed \n %s",errmess); + else wprintf(mLog,"large block direct I/O not allowed \n %s",errmess); + } + if (errno == EPERM) { + if (Fgui) zmessageACK(mWin,0,"permission denied \n %s",errmess); + else wprintf(mLog,"permission denied \n %s",errmess); + } + goto cleanup; +} + + +// modify filespec to have a specified version +// 0 = no version = current version, +N = previous version +// returns cc of resulting filespec +// warning: filespec must have space for version numbers + +int setFileVersion(char *filespec, int vers) +{ + int fcc, overs, err; + char *psep1; + const char *psep2; + + fcc = strlen(filespec); + psep1 = strstr(filespec+fcc-10,VSEP1); // look for file version v.3.2 + if (psep1) { + overs = 0; + err = convSI(psep1+2,overs,&psep2); // if format not valid, take + if (err < 2) overs = 1; // as non-versioned file + if (strNeq(psep2,VSEP2)) overs = 0; + if (*(psep2+1)) overs = 0; // VSEP2 must be at end + if (overs) *psep1 = 0; + fcc = psep1 - filespec; + } + + if (vers == 0) return fcc; + + if (! psep1) psep1 = filespec + fcc; + strcpy(psep1,VSEP1); + sprintf(psep1+2,"%d",vers); + strcat(psep1+2,VSEP2); + + return fcc + strlen(psep1); +} + + +// rename a backup file to assign the next version number +// update the passed backup file data record, bakrec +// returns error message or null if all OK + +const char * setnextVersion(bfrec &bakrec) +{ + char fspec1[maxfcc], fspec2[maxfcc]; + int vers, err; + + strcpy(fspec1,BJdirk); + strcat(fspec1,bakrec.file); + strcpy(fspec2,fspec1); + + vers = bakrec.hiver + 1; + setFileVersion(fspec2,vers); + + err = rename(fspec1,fspec2); + if (err) return strerror(errno); + + bakrec.hiver = vers; + if (! bakrec.lover) bakrec.lover = vers; // v.4.0 + return null; +} + + +// purge expired file versions in backup location +// bakrec: backup file data record to use and update +// returns error message or null if all OK +// fkeep: if true, keep last version unless disk file was deleted // v.4.0 + +const char * purgeVersions(bfrec &bakrec, int fkeep) +{ + int lover, hiver, loretver, vers; + int err, vfound, vpurged; + int retND, retNV; + double fage; + char fspec[maxfcc]; + const char *mess = null; + struct stat64 filestat; + + strcpy(fspec,BJdirk); // prepend backup location + strcat(fspec,bakrec.file); + + retND = bakrec.retND; + retNV = bakrec.retNV; + lover = bakrec.lover; + hiver = bakrec.hiver; + if (! hiver) return 0; // no versions present + + if (! retND) retND = -1; // zero days retention, defeat test + + if (bakrec.disp == 'd') fkeep = 0; // file no longer in backup job + if (bakrec.disp == 'v') fkeep = 0; // or disk file deleted + + loretver = lover; // lowest retained version + vfound = 0; // actual versions found + vpurged = 0; // versions purged + + for (vers = hiver; vers >= lover; vers--) // loop file versions, high to low + { // v.4.0 + setFileVersion(fspec,vers); + err = lstat64(fspec,&filestat); // check file exists on backup + if (err) continue; + vfound++; // count versions found + + fage = (time(0)-filestat.st_mtime)/24.0/3600.0; // file age in days + + if (fage <= retND || vfound <= retNV) { // retain this version + loretver = vers; // remember lowest retained version + continue; + } + + if ((vers == hiver) && fkeep) { // retain last version v.4.0 + loretver = vers; + continue; + } + + mess = deleteFile(fspec); // purge this version + if (mess) break; + vpurged++; + } + + bakrec.nexpv = fkeep; // set 0 or 1 expired versions v.4.0 + bakrec.lover = loretver; // set new low version v.4.0 + if (vpurged == vfound) + bakrec.lover = bakrec.hiver = 0; // no versions remaining v.4.1 + return mess; +} + + +// helper function to delete a file from backup location. +// delete parent directories if they are now empty. + +cchar * deleteFile(cchar *file) +{ + int err; + char dfile[maxfcc], *pp; + + strcpy(dfile,file); + + err = remove(dfile); + if (err) return strerror(errno); + + while ((pp = (char *) strrchr(dfile,'/'))) // delete empty directory + { + *pp = 0; + err = rmdir(dfile); + if (! err) continue; // and parents ... + if (errno == ENOTEMPTY) return 0; + return strerror(errno); + } + + return 0; +} + + +// do shell command (subprocess) and echo outputs to log window +// returns command status: 0 = OK, +N = error + +int do_shell(cchar *pname, cchar *command) +{ + char buff[500], *crec; + int err, contx = 0; + + snprintf(buff,499,"\n""shell: %s \n",command); + wprintx(mLog,0,buff,boldfont); + + while ((crec = command_output(contx,command))) // bug fix: remove colon v.3.2 + { + wprintf(mLog," %s: %s \n",pname,crec); + zsleep(0.1); // throttle output a little + } + + err = command_status(contx); + if (err == 32) err = 0; // ignore Linux "broken pipe" crap + if (err) wprintf(mLog," %s status: %s \n", pname, strerror(err)); + else wprintf(mLog," OK \n"); + return err; +} + + +// supply unused zdialog callback function + +void KBstate(int key, int state) { return; } + + + + diff -Nru ukopp-4.4/ukopp-4.7.spec ukopp-4.7/ukopp-4.7.spec --- ukopp-4.4/ukopp-4.7.spec 1970-01-01 00:00:00.000000000 +0000 +++ ukopp-4.7/ukopp-4.7.spec 2013-02-27 22:11:30.000000000 +0000 @@ -0,0 +1,42 @@ +# RPM spec file for ukopp + +Name: ukopp +Version: 4.7 +Release: 1 +Summary: backup with retention of file versions +Group: utils +Vendor: kornelix +Packager: kornelix2@gmail.com +License: GPL3 +Source: %{name}-%{version}.tar.gz +URL: http://kornelix.com + +%description +Copy files to backup media. +Copies only new and modified files and is therefore quite fast. +Specify directories or files to include or exclude at any level. +Report disk/backup differences at summary, directory or file level. +Optionally retain prior file versions for a specified time or +version count. Optionally verify backup files. + +%prep +%setup -q + +%build +make + +%install +make install PREFIX=$RPM_BUILD_ROOT/usr + +%clean +rm -rf $RPM_BUILD_ROOT + +%files +%defattr(-,root,root) +/usr/bin/%{name} +/usr/share/%{name} +/usr/share/doc/%{name} +/usr/share/applications/kornelix-%{name}.desktop +/usr/share/man/man1/%{name}.1.gz + + diff -Nru ukopp-4.4/zfuncs.cc ukopp-4.7/zfuncs.cc --- ukopp-4.4/zfuncs.cc 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/zfuncs.cc 2013-02-27 22:11:30.000000000 +0000 @@ -1,8 +1,8 @@ /************************************************************************** zfuncs.cpp collection of Linux and GDK/GTK utility functions - Copyright 2006 2007 2008 2009 2010 2011 2012 Michael Cornelison - source URL: kornelix.squarespace.com + Copyright 2006 2007 2008 2009 2010 2011 2012 2013 Michael Cornelison + source code URL: kornelix.com contact: kornelix2@gmail.com This program is free software: you can redistribute it and/or modify @@ -18,9 +18,157 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . +--------------------------------------------------------------------------- + + These programs originate from the author's web site: http://kornelix.com + Other web sites may offer them for download. Modifications could have been made. + If you have questions, suggestions or a bug to report: kornelix2@gmail.com + + Table of Contents + ================= + + Linux System Functions + ---------------------- + apppause printf message and wait for user ACK + zpopup_message popup message window, thread safe (no GTK) + zappcrash abort with traceback dump to popup window and stdout + catch_signals trap segfault, crash with zappcrash() + beroot restart image as root, if password is OK + timer functions elapsed time, CPU time, process time functions + /proc file functions parse data from various /proc files + zsleep sleep for any amount of time (e.g. 0.1 seconds) + global_lock reserve and release a global resource (all processes/threads) + start_detached_thread simplified method to start a detached thread + synch_threads make threads pause and resume together + zget_locked, etc. safely access parameters from multiple threads + gate_threads gate one thread at a time through a non-thread-safe function + zmalloc etc. malloc with accountabbility and reporting + shell_quiet format and run a shell command, return status + shell_ack "" + popup error message if error + shell_asynch "" + return immediately and query status later + comand_output start shell command and read the output as records + signalProc pause, resume, or kill a child process + runroot run a command or program as root user + checkinstall check if a list of programs are all installed + fgets_trim fgets() with trim of trailing \r \n and optionally blanks + samedirk test if two files/directories have the same directory path + parsefile parse filespec into directory, file, extension + cpu_profile measure CPU time spent per function or code block + + String Functions + ---------------- + comparison macros code legibility improvers + strField get delimited substrings from input string + strParms parse a string in the form "parm1=1.23, parm2=22 ..." + strHash hash string to random number in a range + strHash2 hash input string to random output string of characters a-z + strncpy0 strcpy() with insured null delimiter + strnPad add blank padding to specified length + strTrim remove trailing blanks + strTrim2 remove leading and trailing blanks + strCompress remove imbedded blanks + strncatv catenate multiple strings with length limit + strcmpv compare 1 string to N strings + strToUpper convert string to upper case + strToLower convert string to lower case + repl_1str replace substring within string + repl_Nstrs replace multiple substrings within string + strncpyx convert string to hex format + StripZeros remove trailing zeros (1.23000E+8 >> 1.23E+8) + blank_null test string for null pointer, zero length, and all blanks + strdupz duplicate string in non-volatile memory + clean_escapes replace 2-character escapes ("\n") with the escaped characters + UTF-8 functions deal with UTF-8 multibyte character strings + + Number Conversion and Formatting + -------------------------------- + convSI string to integer with optional limits check + convSD string to double with optional limits check + convSF string to float with optional limits check + convIS integer to string with returned length + convDS double to string with specified digits of precision + formatKBMB format a byte count with specified precision and B/KB/MB/GB units + + Wildcard Functions + ------------------ + MatchWild match string to wildcard string (multiple * and ?) + MatchWildIgnoreCase works like MatchWild() but ignores case + SearchWild wildcard file search (multiple * and ? in path or file name) + SearchWildIgnoreCase works like SearchWild() but ignores case in file name + + Search and Sort Functions + ------------------------- + bsearch binary search of sorted list + HeapSort sort list of integer / float / double / records / pointers to records + MemSort sort records with multiple keys (data types and sequence) + + Misc Functions + -------------- + bitmap_new, etc. functions to create, modify, and read a bitmap + pvlist_create, etc. functions to manage a list of variable strings + rand functions int and double random numbers with improved distributions + spline1/2 cubic spline curve fitting function + + Application Admin Functions + --------------------------- + zinitapp etc. initialize application directory and data files + locale_filespec find a locale-dependent installation file (translate-xx.po etc.) + showz_userguide display user guide (optional help topic) + showz_logfile display application log file + showz_textfile show application text file (README, changelog, etc.) + showz_html show a local or remote HTML file + zmake_menu_launcher create desktop icon / launcher + + GTK Utility Functions + --------------------- + zmainloop do a main loop to process menu events, etc. + zthreadcrash crash if called from a thread other than main() + wprintx etc. printf() to window at specified row or next row + wscroll scroll text window to put line on screen + wclear clear a wprintf window + wscanf read text from edit window, line at a time + wfiledump dump text window to a file + wfilesave wfiledump with save-as dialog + wprintp print text window to default printer + textwidget funcs intercept mouse clicks on text windows, get clicked text + menus / toolbars simplified GTK menus, toolbars and status bars + popup menus create popup menus + gmenuz customizable graphic popup menu + vbox_menu vertical menu/toolbar in vertical packing box + zdialog, etc. simplified GTK dialogs + ZTX(), etc. translate GUI and message text strings for non-English locales + write_popup_text write rows of text to a popup window + popup_command run a shell command with output in a popup window + zmessageACK popup message, printf format, wait for user ACK + zmessLogACK same with parallel output to STDOUT + zmessageYN popup message, printf format, wait for user Yes / No + zmessage_help popup message, printf format, [help] button >> user guide topic + zmessage_post popup message, printf format, show until killed + zdialog_text popup dialog to get 1-2 lines of text input from user + zdialog_choose popup dialog to show a message, select a button, return choice + poptext_window popup small message at given window/mouse position + poptext_mouse popup small message at the current mouse position + zgetfile simplified file chooser dialog + print_image_file dialog to print an image file using GTK functions + drag_drop_connect connect window drag-drop event to user function + get_thumbnail get thumbnail image for given image file + zmakecursor make a cursor from an image file (.png .jpg) + gdk_move_pointer move the mouse pointer within a widget/window + gdk_pixbuf_rotate rotate a pixbuf through any angle + parameters functions to manage a set of numeric parameters + + C++ Classes + ----------- + xstring string manipulation (= / + / insert / overlay) + Vxstring array of xstrings with auto growth + HashTab hash table: add, delete, find, step through + Queue queue of xstrings: push, pop first or last + Tree store / retrieve data by node names or numbers, any depth + + ***************************************************************************/ -// zfuncs.cpp version v.5.2 +// zfuncs.cpp version v.5.5 #include "zfuncs.h" @@ -34,22 +182,22 @@ void apppause() { - printf("*** pause, press return to continue: "); + printf("*** press return to continue: "); getchar(); return; } // Output a message to stdout and wait for user ACK. -// Works like printf. +// Works like printf(). -void apppause(cchar *pMess, ... ) +void apppause(cchar *format, ... ) { va_list arglist; char message[200]; - va_start(arglist,pMess); - vsnprintf(message,200,pMess,arglist); + va_start(arglist,format); + vsnprintf(message,200,format,arglist); va_end(arglist); printf("pause: %s \n",message); @@ -59,27 +207,69 @@ } +// output a message to a popup window +// works like printf() +// thread safe (separate shell process, no GTK) +// returns immediately and message dies after 5 seconds + +void zpopup_message(cchar *format, ... ) // v.5.5 +{ + va_list arglist; + char message[400], tempfile[30], command[100]; + FILE *fid; + int cc, err; + + va_start(arglist,format); // format user message + vsnprintf(message,400,format,arglist); + va_end(arglist); + + printf("popup message: \n %s \n",message); // message to log file stdout + + cc = strlen(message); + if (cc < 30) { + strcat(message," "); // lengthen short message + message[30] = 0; // (no truncate box title line) + } + + sprintf(tempfile,"/tmp/zpopup-%06d",getpid()); // write message to temp file + fid = fopen(tempfile,"w"); + if (! fid) return; + fprintf(fid,"%s",message); + fclose(fid); + + strcpy(command,"xmessage -buttons OK:0 -nearmouse -timeout 5 -file "); // create popup with message file + strcat(command,tempfile); + strcat(command," &"); // return immediately + err = system(command); + if (err) return; + return; +} + + /**************************************************************************/ -// crash with error message and traceback dump in popup window -// works like printf +// write an error message and traceback dump to stdout and to a popup window +// error message works like printf() -void zappcrash(cchar *pMess, ... ) +void zappcrash(cchar *format, ... ) { va_list arglist; FILE *fid1, *fid2; int ii, cc, err, nstack = 50; + static int crash = 0; char message[200]; void *stacklist[50]; char **stackents; char progexe[300], command[300], buff[300], hexaddr[20]; char *pp1, *pp2, *plim, *pfunc; + + if (crash++) return; // stop entry from multiple threads v.5.3 - va_start(arglist,pMess); - vsnprintf(message,200,pMess,arglist); + va_start(arglist,format); + vsnprintf(message,200,format,arglist); va_end(arglist); - printf("zappcrash: \n %s \n",message); // output message to stdout + printf("*** zappcrash: \n %s \n",message); // output message to stdout nstack = backtrace(stacklist,nstack); // get traceback data stackents = backtrace_symbols(stacklist,nstack); @@ -89,13 +279,16 @@ fid1 = fopen("zappcrash","w"); // text file for backtrace - fprintf(fid1,"zappcrash: \n %s \n",message); // output message to text file + fprintf(fid1,"*** zappcrash: \n %s \n",message); // output message to text file + printf("\n\n*** zappcrash: \n %s \n",message); // and to stdout cc = readlink("/proc/self/exe",progexe,300); // get own program path if (cc <= 0) { - printf("cannot get /proc/self/exe \n"); + printf("*** cannot get /proc/self/exe \n"); + fclose(fid1); abort(); } + progexe[cc] = 0; for (ii = 0; ii < nstack; ii++) // output backtrace to text file @@ -116,20 +309,21 @@ } fprintf(fid1," %s %s \n",stackents[ii],pfunc); // write to text file + printf(" %s %s \n",stackents[ii],pfunc); // and to stdout } fclose(fid1); - err = system("cat zappcrash"); // add to log file v.5.2 err = system("xdg-open zappcrash"); // display in editor - if (err) printf("xdg-open failure \n"); + if (err) printf("*** xdg-open failure \n"); abort(); } /**************************************************************************/ -// application initialization function to catch segfaults +// application initialization function to catch some bad news signals +// the signal handler calls zappcrash() to output a traceback dump and exit void catch_signals() { @@ -139,7 +333,10 @@ sigact.sa_handler = sighandler; sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; - sigaction(SIGSEGV,&sigact,0); + sigaction(SIGSEGV,&sigact,0); + sigaction(SIGILL,&sigact,0); // SIGILL/FPE/BUS added v.5.3 + sigaction(SIGFPE,&sigact,0); + sigaction(SIGBUS,&sigact,0); return; } @@ -148,7 +345,12 @@ void sighandler(int signal) { - zappcrash("segment fault"); + const char *signame = "unknown"; + if (signal == SIGSEGV) signame = "segment fault"; + if (signal == SIGILL) signame = "illegal op"; + if (signal == SIGFPE) signame = "float exception"; + if (signal == SIGBUS) signame = "bus error"; + zappcrash("fatal signal %s",signame); return; } @@ -157,6 +359,9 @@ // This function will restart the current program with root privileges, // if the correct (sudo) password is given. It does not return. +// "argc" and "argv" are passed in the command line. Use the original +// argc-1 and argv+1 to avoid passing the command name twice. +// These are optional and may be omitted. void beroot(int argc, char *argv[]) { @@ -174,7 +379,7 @@ strcpy(command,"beesu "); } if (err) { - printf("cannot find gksu or beesu \n"); + printf("*** cannot find gksu or beesu \n"); abort(); } @@ -182,7 +387,7 @@ cc2 = readlink("/proc/self/exe",command+cc1,990); if (cc2 <= 0) { - printf("cannot get /proc/self/exe \n"); + printf("*** cannot get /proc/self/exe \n"); abort(); } command[cc1+cc2] = 0; // gksu or beesu @@ -289,8 +494,7 @@ /**************************************************************************/ -// get elapsed process time for my process, -// including threads and child processes. +// get elapsed process time for my process, including threads and child processes. double jobtime() { @@ -313,6 +517,45 @@ /**************************************************************************/ +// convert a time_t date/time (e.g. st_mtime from stat() call) +// into a compact date/time format "yyyymmddhhmmss" + +void compact_time(const time_t fileDT, char *compactDT) // v.5.5 +{ + struct tm *fdt; + int year, mon, day, hour, min, sec; + + fdt = localtime(&fileDT); + + year = fdt->tm_year + 1900; + mon = fdt->tm_mon + 1; + day = fdt->tm_mday; + hour = fdt->tm_hour; + min = fdt->tm_min; + sec = fdt->tm_sec; + + compactDT[0] = year / 1000 + '0'; + compactDT[1] = (year % 1000) / 100 + '0'; + compactDT[2] = (year % 100) / 10 + '0'; + compactDT[3] = year % 10 + '0'; + compactDT[4] = mon / 10 + '0'; + compactDT[5] = mon % 10 + '0'; + compactDT[6] = day / 10 + '0'; + compactDT[7] = day % 10 + '0'; + compactDT[8] = hour / 10 + '0'; + compactDT[9] = hour % 10 + '0'; + compactDT[10] = min / 10 + '0'; + compactDT[11] = min % 10 + '0'; + compactDT[12] = sec / 10 + '0'; + compactDT[13] = sec % 10 + '0'; + compactDT[14] = 0; + + return; +} + + +/**************************************************************************/ + // Read and parse /proc file with records formatted "parmname xxxxxxx" // Find all requested parameters and return their numeric values @@ -436,7 +679,6 @@ // Only one process/thread may posess a given lock. // A reboot or process exit or crash releases the lock. // fd = global_lock(lockfile) returns fd > 0 if success, -1 otherwise. -// To unlock, call global_unlock(fd, lockfile). int global_lock(cchar *lockfile) // v.5.2 { @@ -444,7 +686,7 @@ fd = open(lockfile,O_RDWR|O_CREAT,0666); // open or create the lock file if (fd < 0) { - printf("global_lock(), %s \n",strerror(errno)); + printf("*** global_lock(), %s \n",strerror(errno)); return -1; } @@ -469,7 +711,9 @@ /**************************************************************************/ -// start a detached thread using a simplified protocol +// Start a detached thread using a simplified protocol. +// Will not make a zombie out of the calling thread if it exits +// without checking the status of the created thread. void start_detached_thread(void * threadfunc(void *), void * arg) { @@ -491,6 +735,10 @@ // Simultaneously resume NT calling threads. // from main(): synch_threads(NT) /* setup to synch NT threads */ // from each thread: synch_threads() /* suspend, resume simultaneously */ +// +// Each calling thread will suspend execution until all threads have suspended, +// then they will all resume execution at the same time. If NT is greater than +// the number of calling threads, the threads will never resume. void synch_threads(int NT) { @@ -511,8 +759,9 @@ /**************************************************************************/ -// safely access parameters from multiple threads -// limitation: one lock for any number of parameters +// Safely access and update parameters from multiple threads. +// A mutex lock is used to insure one thread at a time has access to the parameter. +// Many parameters can be used but there is only one mutex lock. mutex zget_lock = PTHREAD_MUTEX_INITIALIZER; @@ -561,16 +810,24 @@ } -/**************************************************************************/ +/************************************************************************** + + These are wrappers for malloc() and free() with extra diagnostics. + zmalloc() crashes with a message to standard output if the allocation fails, + hence the caller need not check. zmalloc() allocates extra memory for sentinels + placed before and after the returned memory space, and zfree() checks the + sentinels for validity and crashes with a message if they don't match. + The optional tag argument is also copied into the extra allocated space for use + by zmalloc_report(). zmalloc_report() reports total allocated memory by tag to + standard output. Allocation counts and bytes for zmalloc() calls not yet matched + by zfree() calls are listed. + + Benchmark: zmalloc/zfree 5000 * random(1KB-1MB): 0.64 secs. 3.3 GHz core i5 -// malloc() and free() wrappers with auto crash on failure and log option. -// overflow sentinels are placed around the allocated memory. -// zmalloc() and zfree() calls are logged if log count > 0. -// benchmark zmalloc() + zfree(): average 0.22 microseconds on 2.67 GHz CPU. +***/ #define zmalloc_extra 32 uint zmalloc_tot = 0; -int zmalloc_logcount = 0; // set to +N to log next N calls void zmalloc_tabulate(cchar *tag, int bytes); // private function @@ -597,20 +854,15 @@ } *pbytes = bytes; - strncpy(psen1,"sen1",4); + strncpy(psen1,"sen1",4); // set sentinel if (! tag) tag = "notag"; strncpy0(ptag,tag,20); memset(puser,0,bytes); // clear user space to zeros strncpy(psen2,"sen2",4); + zmalloc_tot += bytes; - zmalloc_tabulate(ptag,bytes); // track usage by tag - if (zmalloc_logcount) { - printf("zmalloc %-20s loc: %p bytes: %zu total: %u \n",ptag,puser,bytes,zmalloc_tot); - zmalloc_logcount--; - } - return puser; } @@ -635,24 +887,9 @@ *psen1 = *psen2 = 0; // destroy sentinels zmalloc_tot -= bytes; - zmalloc_tabulate(ptag,-bytes); // track usage by tag - if (zmalloc_logcount) { - printf("zfree %-20s loc: %p bytes: %zu total: %u \n",ptag,puser,bytes,zmalloc_tot); - zmalloc_logcount--; - } - - free(maddr); - return; -} - - -// turn logging flag on for specified log count, or off if count = 0; - -void zmalloc_log(int count) -{ - zmalloc_logcount = count; + free(maddr); // free memory (must be last) return; } @@ -712,22 +949,168 @@ /**************************************************************************/ -// Run a shell command and get its outputs one record at a time. -// Start a new command with contx = 0. Do not change contx. -// NULL return means no more output. -// Get command exit status: err = command_status(contx) -// Caller owns returned strings which are candidates for zfree() +// Format and run a shell command. +// Print a status message to stdout if there is an error. +// returns 0 if OK, +N if error + +int shell_quiet(cchar *command, ...) // v.5.5 +{ + char *cbuff; + va_list arglist; + int cc, err; + + cc = strlen(command) + 1000; + cbuff = zmalloc(cc+1); + + va_start(arglist,command); // format command + vsnprintf(cbuff,cc,command,arglist); + va_end(arglist); + + err = system(cbuff); + + if (! err) { zfree(cbuff); return 0; } + if (strnEqu(command,"diff",4)) { zfree(cbuff); return err; } // no diagnostic for these + if (strnEqu(command,"cmp",3)) { zfree(cbuff); return err; } + + if (cc > 200) cbuff[200] = 0; + printf("%s \n *** %s \n",cbuff,wstrerror(err)); // log error message + + zfree(cbuff); + return err; +} + + +// Format and run a shell command. +// Print a status message to stdout if there is an error. +// Also pop-up an error message window if error. +// Thread safe: GTK is not used. +// returns 0 if OK, +N if error + +int shell_ack(cchar *command, ...) // v.5.5 +{ + char *cbuff; + va_list arglist; + int cc, err; + + cc = strlen(command) + 1000; + cbuff = zmalloc(cc+1); + + va_start(arglist,command); // format command + vsnprintf(cbuff,cc,command,arglist); + va_end(arglist); + + err = system(cbuff); + + if (! err) { zfree(cbuff); return 0; } + if (strnEqu(command,"diff",4)) { zfree(cbuff); return err; } // no diagnostic for these + if (strnEqu(command,"cmp",3)) { zfree(cbuff); return err; } + + if (cc > 200) cbuff[200] = 0; + printf("%s \n *** %s \n",cbuff,wstrerror(err)); // log error message + zmessageACK(null,"command error","%s \n %s",cbuff,wstrerror(err)); // popup error message + + zfree(cbuff); + return err; +} + + +/**************************************************************************/ + +// Start a shell command from a new thread and return immediately. +// The thread waits for the shell command and gets its status. +// If there is an error, it is logged to STDOUT. +// The calling process can get the status asynchronously. +// The command format works like printf(). // +// handle = shell_asynch(command, ...) +// Start the command and return a reference handle. +// +// err = shell_asynch_status(handle) +// Return the command status for the given handle: +// -1 = busy, 0 = complete OK, +N = error (use wstrerror(err)). +// +// The status MUST be queried for allocated space to be freed. + +namespace shell_asynch_names { + char *command[10]; + int status[10]; + mutex mlock = PTHREAD_MUTEX_INITIALIZER; +} + +int shell_asynch(cchar *Fcommand, ...) // v.5.5 +{ + using namespace shell_asynch_names; + + void * shell_asynch_thread(void *); + + va_list arglist; + static int ii; + + mutex_lock(&mlock); // block other callers + + for (ii = 0; ii < 10; ii++) + if (command[ii] == 0) break; + if (ii == 10) zappcrash("shell_asynch > 10 calls"); + + command[ii] = zmalloc(2000,"shell_asynch"); // allocate memory + va_start(arglist,Fcommand); // format command + vsnprintf(command[ii],2000,Fcommand,arglist); + va_end(arglist); + + start_detached_thread(shell_asynch_thread,&ii); // pass command to thread + status[ii] = -1; // status = busy + return ii; // return handle +} + +void * shell_asynch_thread(void *arg) // thread function +{ + using namespace shell_asynch_names; + + int err; + int ii = *((int *) arg); // capture handle + + mutex_unlock(&mlock); // unblock other callers + err = system(command[ii]); // start command, wait until done + if (err) printf("%s \n *** %s \n",command[ii],wstrerror(err)); // log error message + status[ii] = err; // set status for caller + return 0; +} + +int shell_asynch_status(int handle) // get command status +{ + using namespace shell_asynch_names; + + int ii = handle; + if (status[ii] == -1) return -1; // return busy status + zfree(command[ii]); // free memory + command[ii] = 0; + return status[ii]; // return completion status +} + + +/************************************************************************** + + Run a shell command and get its outputs one record at a time. + The outputs are returned one record at a time, until a NULL is returned, + indicating the command has finished and has exited. + The new line character is removed from the returned output records. + Use contx = 0 to start a new command. Do not change the returned value. + Up to 9 commands can run in parallel, with contx values 1-9. + To get the command exit status: status = command_status(contx). + If the command is still busy, -1 is returned. + To kill a command before output is complete: command_kill(contx); + +***/ FILE * CO_contx[10] = { 0,0,0,0,0,0,0,0,0,0 }; int CO_status[10]; char * command_output(int &contx, cchar *command, ...) // simplify, allow parallel usage { - FILE *fid; - va_list arglist; - char buff[10000], *prec; + FILE *fid; + va_list arglist; + char buff[10000], *prec; if (contx == 0) // start new command { @@ -742,6 +1125,7 @@ fid = popen(buff,"r"); // execute command, output to FID if (fid == 0) { CO_status[contx] = errno; // failed to start + printf("command_output: %s\n %s\n",buff,strerror(errno)); // v.5.5 return 0; } CO_contx[contx] = fid + 1000; @@ -763,11 +1147,21 @@ return WEXITSTATUS(err); // special BS for subprocess } +int command_kill(int contx) // kill output before completion v.5.5 +{ + FILE *fid; + fid = CO_contx[contx] - 1000; + CO_status[contx] = pclose(fid); // set status + CO_contx[contx] = 0; // mark context free + return 0; +} + /**************************************************************************/ -// signal a subprocess to pause, resume, or terminate -// return: 0: OK +-N: error +// Signal a running subprocess by name (name of executable or shell command). +// Signal is "pause", "resume" or "kill". If process is paused, kill may not work, +// so issue resume first if process is paused. int signalProc(cchar *pname, cchar *signal) { @@ -871,7 +1265,7 @@ if (Nmiss) { for (int ii = 0; ii < Nmiss; ii++) strncatv(errmessage,199,missprogs[ii]," ",null); - zmessageACK(null,errmessage); + zmessageACK(null,null,errmessage); } return Nmiss; @@ -987,9 +1381,26 @@ utility to measure CPU time spent in various functions or code blocks cpu_profile_init() initialize at start of test + cpu_profile_enter(fnum) at entry to a function + cpu_profile_exit(fnum) at exit from a function cpu_profile_report() report CPU time per function - cpu_profile_enter(fnum) at entry to a function // inline, defined in zfuncs.h - cpu_profile_exit(fnum) at exit from a function // inline, defined in zfuncs.h + + Methodology: cpu_profile_init() starts a thread that suspends and runs every + 1 millisecond and updates a timer. + cpu_profile_enter() and cpu_profile_exit() accumulate the time difference between + entry and exit of code being measured. This may be zero because of the long interval + between timer updates. Accuracy comes from statistical sampling over many seconds, + so that if the time spent in a monitored function is significant, it will be accounted + for. The accuracy is better than 1% as long as the measured function or code block + consumes a second or more of CPU time during the measurement period. + The "fnum" argument (1-99) designates the function or code block being measured. + cpu_profile_report() stops the timer thread and reports time consumed per function, + using the "fnum" tags in the report. + The functions cpu_profile_enter() and cpu_profile_exit() subtract the timer + difference and add to a counter per fnum, so the added overhead is insignificant. + They are inline functions defined as follows: + enter: cpu_profile_timer = cpu_profile_elapsed; + exit: cpu_profile_table[fnum] += cpu_profile_elapsed - cpu_profile_timer; ***************************************************************************/ @@ -1097,6 +1508,9 @@ int ii, nf, fcc = 0; static char blankstring[2], nullstring[1]; + if (! string) return 0; // bad call v.5.3 + if (Nth < 1) return 0; + gate_threads_enter(); // allow one thread at a time if (ftf) // overall first call @@ -1107,8 +1521,6 @@ *nullstring = 0; } - if (Nth < 1) { gate_threads_leave(); return 0; } - pf1 = (char *) string - 1; // start parse nf = 0; @@ -1246,6 +1658,7 @@ // Produce random value from hashed input string. // Output range is 0 to max-1. +// Benchmark: 0.036 usec for 20 char. string 3.3 GHz Core i5 int strHash(cchar *string, int max) { @@ -1269,6 +1682,8 @@ // Hash an input string into a random printable (a-z) output string. // Returns outcc character random printable string in static memory. // Every output character is randomized from the entire input string. +// With an outcc of 12, collision probability is 10**(-17). +// Benchmark: 2.6 usec for 20 char. input, 12 char. output 3.3 GHz Core i5 cchar * strHash2(cchar *instring, int outcc) { @@ -1298,6 +1713,7 @@ // Copy string with specified max. length (including null terminator). // truncate if needed. null terminator is always supplied. +// Returns 0 if no truncation, 1 if input string was truncated to fit. int strncpy0(char *dest, cchar *source, uint cc) { @@ -1310,7 +1726,7 @@ /**************************************************************************/ -// Copy string with blank pad to specified length. No null. +// Copy string with blank pad to specified length. No null is added. void strnPad(char *dest, cchar *source, int cc) { @@ -1501,8 +1917,10 @@ /**************************************************************************/ -// copy string strin to strout, replacing every occurrence -// of the substring ssin with the substring ssout +// Copy string strin to strout, replacing every occurrence +// of the substring ssin with the substring ssout. +// Returns the count of replacements, if any. +// Replacement strings may be longer or shorter or have zero length. int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout) { @@ -1532,8 +1950,11 @@ /**************************************************************************/ -// like repl_1str, but multiple pairs of substrings are processed -// (... ssin1, ssout1, ssin2, ssout2, ... null) +// Copy string strin to strout, replacing multiple substrings with replacement strings. +// Multiple pairs of string arguments follow strout, a substring and a replacement string. +// Last pair of string arguments must be followed by a null argument. +// Returns the count of replacements, if any. +// Replacement strings may be longer or shorter or have zero length. int repl_Nstrs(cchar *strin, char *strout, ...) { @@ -1647,6 +2068,11 @@ /**************************************************************************/ // test for blank/null string +// Returns status depending on input string: +// 0 not a blank or null string +// 1 argument string is NULL +// 2 string has zero length (*string == 0) +// 3 string is all blanks int blank_null(cchar *string) { @@ -1662,6 +2088,7 @@ /**************************************************************************/ // make a copy of a string in heap memory and allocate more space +// tag: optional tag for memory tracking (same as zmalloc) // returned string is subject for zfree(); char * strdupz(cchar *string, int more, cchar *tag) @@ -1672,9 +2099,13 @@ } -// copy into existing 'zmalloc' string if present and long enough -// else free memory and allocate a longer one -// destination string is subject for zfree() +// The source string is copied into the destination string if it is not NULL and +// if it is long enough. Otherwise it frees existing destination memory and +// allocates new space large enough for the source string + 'more' additional bytes. +// The cc copied is returned. The destination string can be freed with zfree(). +// The space available in the destination string is known because zmalloc() adds +// extra bytes in front of the returned address where the length is stored. +// Benchmark: 0.15 usec for string cc 1-30, more 0-9 3.3 GHz Core i5 int strdupz(cchar *source, char *&zdest, int more, cchar *tag) { @@ -1730,9 +2161,9 @@ // Compute the graphic character count for a UTF8 character string. // Depends on UTF8 rules: -// - ascii characters are positive (actually 0x00 to 0x7F) -// - 1st byte of multibyte sequence is negative (actually 0xC0 to 0xFD) -// - subsequent bytes are negative and < 0xC0 (actually 0x80 to 0xBF) +// - ascii characters are positive (0x00 to 0x7F) +// - 1st byte of multibyte sequence is negative (0xC0 to 0xFD) +// - subsequent bytes are negative and < 0xC0 (0x80 to 0xBF) int utf8len(cchar *utf8string) { @@ -1828,6 +2259,7 @@ // utf8in input UTF8 string // Nth graphic character position, zero based // returns starting character (byte) position of Nth graphic character +// returns -1 if Nth is beyond the string length // v.5.5 int utf8_position(cchar *utf8in, int Nth) { @@ -1836,311 +2268,104 @@ for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++) { - if (utf8in[ii] < 0) - while (utf8in[ii+1] < xlimit) ii++; + if (utf8in[ii] < 0) // multi-byte character + while (utf8in[ii+1] && utf8in[ii+1] < xlimit) ii++; // traverse member bytes posx++; } - return ii; + if (utf8in[ii]) return ii; + return -1; // v.5.5 } /************************************************************************** - bitmap functions -***************************************************************************/ -// create a new bitmap with specified bit length. -// initially all bits are false. + Conversion Utilities -bitmap * bitmap_new(int nbits) -{ - int cc, ii; - bitmap *bm; - - bm = (bitmap *) zmalloc(sizeof(bitmap),"bitmap"); - bm->nbits = nbits; - cc = (nbits + 7) / 8; - bm->bits = (uchar *) zmalloc(cc,"bitmap"); - for (ii = 0; ii < cc; ii++) bm->bits[ii] = 0; - return bm; -} + convSI(string, inum, delim) string to int + convSI(string, inum, low, high, delim) string to int with range check + convSD(string, dnum, delim) string to double + convSD(string, dnum, low, high, delim) string to double with range check -// set bit in bitmap to true or false + convSF(string, fnum, delim) string to float + convSF(string, fnum, low, high, delim) string to float with range check -void bitmap_set(bitmap *bm, int bit, bool value) -{ - int ii, jj; - uchar bit1; + convIS(inum, string, cc) int to string with returned cc - if (bit >= bm->nbits) zappcrash("bitmap, bit %d too big",bit); - ii = bit / 8; - jj = bit % 8; - bit1 = 0x80 >> jj; + convDS(fnum, digits, string, cc) double to string with specified + digits of precision and returned cc + + string input (cchar *) or output (char *) + inum input (int) or output (int &) + dnum input (double) or output (double &) + delim optional returned delimiter (null or cchar **) + low, high input range check (int or double) + cc output string length (int &) + digits input digits of precision (int) to be used for output string + + NOTE: decimal point may be comma or period. + 1000's separators must NOT be present. - if (value) bm->bits[ii] = bm->bits[ii] | bit1; - else { - bit1 = bit1 ^ 0xff; - bm->bits[ii] = bm->bits[ii] & bit1; - } + convIS and convDS also return the length cc of the string output. + convDS accepts same formats as atof. Decimal point can be comma or period. + convDS will use whatever format (f/e) gives the shortest result. + Outputs like "e03" or "e+03" will be shortened to "e3". - return; -} + function status returned: + 0 normal conversion, no invalid digits, blank/null termination + 1 successful converstion, but trailing non-numeric found + 2 conversion OK, but outside specified limits + 3 null or blank string, converted to zero + 4 conversion error, invalid data in string + overlapping statuses have following precedence: 4 3 2 1 0 +***************************************************************************/ -// fetch bitmap bit, return true or false +#define max10 (0x7fffffff / 10) -bool bitmap_get(bitmap *bm, int bit) + +// Convert string to integer + +int convSI(cchar *string, int &inum, cchar **delim) { - int ii, jj; - uchar bit1; + char ch; + int sign = 0, digits = 0, tnb = 0; + cchar *pch = string; - ii = bit / 8; - jj = bit % 8; - bit1 = bm->bits[ii] << jj; - if (bit1 < 127) return false; - else return true; -} + inum = 0; + while ((ch = *pch) == ' ') pch++; // skip leading blanks -// delete bitmap + if (ch == '-') sign = -1; // process leading +/- sign + if (ch == '+') sign = 1; // (at most one sign character) + if (sign) pch++; -void bitmap_delete(bitmap *bm) -{ - zfree(bm->bits); - zfree(bm); - return; -} + while ((*pch >= '0') && (*pch <= '9')) // process digits 0 - 9 + { + if (inum > max10) goto conv_err; // value too big + inum = 10 * inum + *pch - '0'; + digits++; + pch++; + } + if (delim) *delim = pch; // terminating delimiter + if (*pch && (*pch != ' ')) tnb++; // not null or blank -/************************************************************************** - variable string list functions - array / list of strings -***************************************************************************/ + if (! digits) // no digits found + { + if (tnb) return 4; // non-numeric (invalid) string + else return 3; // null or blank string + } -// create new variable list with specified capacity + if (sign == -1) inum = -inum; // negate if - sign -pvlist * pvlist_create(int max) -{ - pvlist *pv; + if (! tnb) return 0; // no trailing non-numerics + else return 1; // trailing non-numerics - pv = (pvlist *) zmalloc(sizeof(pvlist),"pvlist"); - pv->max = max; - pv->act = 0; - pv->list = (char **) zmalloc(max * sizeof(char *),"pvlist"); - return pv; -} - -// free memory for variable list - -void pvlist_free(pvlist *pv) -{ - int ii; - - for (ii = 0; ii < pv->act; ii++) - zfree(pv->list[ii]); - zfree(pv->list); - zfree(pv); -} - -// append new entry to end of list (optional if unique) -// if list if full, first entry is removed and rest are packed down -// return: N >= 0: new entry added at position N -// N = -1: not unique, not added - -int pvlist_append(pvlist *pv, cchar *entry, int unique) -{ - int ii; - - if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique - - if (pv->act == pv->max) pvlist_remove(pv,0); // if list full, remove 1st entry - - ii = pv->act; - pv->list[ii] = strdupz(entry,0,"pvlist"); // add to end of list - pv->act++; - return ii; -} - -// prepend new entry to list (optional if unique) -// prior list entries are pushed down to make room -// if list is full, last entry is removed first -// return: N = 0: new entry added at position 0 -// N = -1: not unique, not added - -int pvlist_prepend(pvlist *pv, cchar *entry, int unique) -{ - int ii; - - if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique - - if (pv->act == pv->max) pvlist_remove(pv,pv->act-1); // if list full, remove last entry - - for (ii = pv->act; ii > 0; ii--) // push all list entries down - pv->list[ii] = pv->list[ii-1]; - pv->list[0] = strdupz(entry,0,"pvlist"); // add to start of list - pv->act++; - return 0; -} - -// find list entry by name, return -1 if not found - -int pvlist_find(pvlist *pv, cchar *entry) -{ - int ii; - - for (ii = 0; ii < pv->act; ii++) - if (strEqu(entry,pv->list[ii])) break; - if (ii < pv->act) return ii; - return -1; -} - -// remove an entry by name and repack list - -int pvlist_remove(pvlist *pv, cchar *entry) -{ - int ii; - - ii = pvlist_find(pv,entry); - if (ii < 0) return -1; - pvlist_remove(pv,ii); - return ii; -} - -// remove an entry by number and repack list - -int pvlist_remove(pvlist *pv, int ii) -{ - if (ii < 0 || ii >= pv->act) return -1; - zfree(pv->list[ii]); - for (ii++; ii < pv->act; ii++) { - if (! pv->act) printf("meaningless reference %d",ii); // stop g++ optimization bug /// - pv->list[ii-1] = pv->list[ii]; - } - pv->act--; - return 0; -} - - -// return entry count - -int pvlist_count(pvlist *pv) -{ - return pv->act; -} - - -// replace Nth entry with new one - -int pvlist_replace(pvlist * pv, int ii, cchar *entry) -{ - if (ii < 0 || ii >= pv->act) return -1; - zfree(pv->list[ii]); - pv->list[ii] = strdupz(entry,0,"pvlist"); - return 0; -} - - -// return Nth entry or null - -char * pvlist_get(pvlist *pv, int Nth) -{ - if (Nth >= pv->act) return 0; - return pv->list[Nth]; -} - - -// sort list in ascending order - -int pvlist_sort(pvlist *pv) -{ - HeapSort(pv->list,pv->act); - return 0; -} - - -/************************************************************************** - - Conversion Utilities - - convSI(string, inum, delim) string to int - convSI(string, inum, low, high, delim) string to int with range check - - convSD(string, dnum, delim) string to double - convSD(string, dnum, low, high, delim) string to double with range check - - convSF(string, fnum, delim) string to float - convSF(string, fnum, low, high, delim) string to float with range check - - convIS(inum, string, cc) int to string with returned cc - - convDS(fnum, digits, string, cc) double to string with specified - digits of precision and returned cc - - string input (cchar *) or output (char *) - inum input (int) or output (int &) - dnum input (double) or output (double &) - delim optional returned delimiter (null or cchar **) - low, high input range check (int or double) - cc output string length (int &) - digits input digits of precision (int) to be used for output string - - NOTE: decimal point may be comma or period. - 1000's separators must NOT be present. - - function status returned: - 0 normal conversion, no invalid digits, blank/null termination - 1 successful converstion, but trailing non-numeric found - 2 conversion OK, but outside specified limits - 3 null or blank string, converted to zero - 4 conversion error, invalid data in string - overlapping statuses have following precedence: 4 3 2 1 0 - -***************************************************************************/ - -#define max10 (0x7fffffff / 10) - - -// Convert string to integer - -int convSI(cchar *string, int &inum, cchar **delim) -{ - char ch; - int sign = 0, digits = 0, tnb = 0; - cchar *pch = string; - - inum = 0; - - while ((ch = *pch) == ' ') pch++; // skip leading blanks - - if (ch == '-') sign = -1; // process leading +/- sign - if (ch == '+') sign = 1; // (at most one sign character) - if (sign) pch++; - - while ((*pch >= '0') && (*pch <= '9')) // process digits 0 - 9 - { - if (inum > max10) goto conv_err; // value too big - inum = 10 * inum + *pch - '0'; - digits++; - pch++; - } - - if (delim) *delim = pch; // terminating delimiter - if (*pch && (*pch != ' ')) tnb++; // not null or blank - - if (! digits) // no digits found - { - if (tnb) return 4; // non-numeric (invalid) string - else return 3; // null or blank string - } - - if (sign == -1) inum = -inum; // negate if - sign - - if (! tnb) return 0; // no trailing non-numerics - else return 1; // trailing non-numerics - -conv_err: - inum = 0; - return 4; +conv_err: + inum = 0; + return 4; } @@ -2327,6 +2552,8 @@ // format a number as "123 B" or "12.3 KB" or "1.23 MB" etc. // prec is the desired digits of precision to output. // WARNING: only the last 100 conversions remain available in memory. +// Example formats for 3 digits of precision: +// 12 B, 999 B, 1.23 KB, 98.7 KB, 456 KB, 2.34 MB, 45.6 GB, 12345 GB char * formatKBMB(double fnum, int prec) { @@ -2342,7 +2569,7 @@ if (ftf) { // keep last 100 conversions ftf = 0; for (ii = 0; ii < 100; ii++) - output[ii] = (char *) zmalloc(20); + output[ii] = (char *) zmalloc(20,"formatKBMB"); } gnum = fabs(fnum); @@ -2381,10 +2608,12 @@ Match candidate string to wildcard string containing any number of '*' or '?' wildcard characters. '*' matches any number of characters, including zero characters. '?' matches any one character. - Returns 0 if match, 1 if no match. -***/ + Benchmark: 0.032 usec. wild = *asdf*qwer?yxc + 3.3 GHz Core i5 match = XXXasdfXXXXqwerXyxc + +***************************************************************************/ int MatchWild(cchar *pWild, cchar *pString) { @@ -2430,52 +2659,106 @@ /************************************************************************** - SearchWild - wildcard file search + Wildcard string match - ignoring case + Works like MatchWild() above, but case is ignored. - Find all files with total /pathname/filename matching a pattern, - which may have any number of the wildcard characters '*' and '?' - in either or both the pathname and filename. +***/ - cchar * SearchWild(cchar *wfilespec, int &flag) - - inputs: flag = 1 to start a new search - flag = 2 abort a running search - *** do not modify flag within a search *** +int MatchWildIgnoreCase(cchar *pWild, cchar *pString) // v.5.5 +{ + int ii, star; - wfilespec = filespec to search with optional wildcards - e.g. "/name1/na*me2/nam??e3/name4*.ext?" - - return: a pointer to one matching file is returned per call, - or null when there are no more matching files. - - The search may be aborted before completion, but make a final - call with flag = 2 to clean up temp file. A new search with - flag = 1 will also finish the cleanup. - - NOT THREAD SAFE - do not use in parallel threads - - shell find command is used for the initial search because this - is much faster than recursive use of readdir() (why?). +new_segment: - (#) is used in place of (*) in comments below to prevent - compiler from interpreting (#/) as end of comments + star = 0; + while (pWild[0] == '*') + { + star = 1; + pWild++; + } - GNU find peculiarities: - find /path/# omits "." files - find /path/ includes "." files - find /path/# recurses directories under /path/ - find /path/#.txt does not recurse directories - find /path/#/ finds all files under /path/ - find /path/#/# finds files >= 1 directory level under /path/ - find /path/xxx# never finds anything +test_match: - SearchWild uses simpler and more intuitive matching: - '/' and '.' are matched by '#' - /path/#.txt finds all .txt files under /path/ at any directory level + for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++) + { + if (strncasecmp(pWild+ii,pString+ii,1) != 0) // the only code change + { + if (! pString[ii]) return 1; + if (pWild[ii] == '?') continue; + if (! star) return 1; + pString++; + goto test_match; + } + } + + if (pWild[ii] == '*') + { + pString += ii; + pWild += ii; + goto new_segment; + } + + if (! pString[ii]) return 0; + if (ii && pWild[ii-1] == '*') return 0; + if (! star) return 1; + pString++; + goto test_match; +} + + +/************************************************************************** + + SearchWild - wildcard file search + + Find all files with total /pathname/filename matching a pattern, + which may have any number of the wildcard characters '*' and '?' + in either or both the pathname and filename. + + cchar * SearchWild(cchar *wfilespec, int &flag) + + inputs: flag = 1 to start a new search + flag = 2 abort a running search + *** do not modify flag within a search *** + + wfilespec = filespec to search with optional wildcards + e.g. "/name1/na*me2/nam??e3/name4*.ext?" + + return: a pointer to one matching file is returned per call, + or null when there are no more matching files. + + The search may be aborted before completion, but make a final + call with flag = 2 to clean up temp file. A new search with + flag = 1 will also finish the cleanup. + + NOT THREAD SAFE - do not use in parallel threads + shell find command is used for the initial search because this + is much faster than recursive use of readdir() (why?). + + (#) is used in place of (*) in comments below to prevent + the compiler from interpreting (#/) as end of comments + + GNU find peculiarities: + find /path/# omits "." files + find /path/ includes "." files + find /path/# recurses directories under /path/ + find /path/#.txt does not recurse directories + find /path/#/ finds all files under /path/ + find /path/#/# finds files >= 1 directory level under /path/ + find /path/xxx# never finds anything + + SearchWild uses simpler rules: + '/' and '.' are treated like all other characters and match '#' and '?' + no files are excluded except pure directories + /path/#.txt finds all xxx.txt files under /path/ at all levels + (because #.txt matches aaa.txt, /aaa/bbb.txt, etc.) + + Benchmark: search for /usr/share/#/README, find 457 from 101K files + 1.9 secs. 3.3 GHz Core i5 SSD disk + ***/ -cchar * SearchWild(cchar *wpath, int &uflag) // use popen() instead of scratch file +cchar * SearchWild(cchar *wpath, int &uflag) { static FILE *fid = 0; static char matchfile[maxfcc]; @@ -2536,10 +2819,86 @@ } +/************************************************************************** + + SearchWildIgnoreCase - wildcard file search - ignoring case + Works like SearchWild() above, but case of file name is ignored. + + Actually, the trailing part of the path name is also case-insensitive, + meaning that it is possible to get more matches than technically correct + if directories like this are present: + /AAA/BBB/.../filename + /AAA/bbb/.../filename + +***/ + +cchar * SearchWildIgnoreCase(cchar *wpath, int &uflag) // v.5.5 +{ + static FILE *fid = 0; + static char matchfile[maxfcc]; + char searchpath[maxfcc]; + char command[maxfcc]; + int cc, err; + char *pp; + + if ((uflag == 1) || (uflag == 2)) { // first call or stop flag + if (fid) { + pclose(fid); // if file open, close it + fid = 0; + } + } + + if (uflag == 2) return 0; // kill flag, done + + if (uflag == 1) // first call flag + { + cc = strlen(wpath); + if (cc == 0) return 0; + if (cc > maxfcc-20) zappcrash("SearchWild: wpath > maxfcc"); + + pp = (char *) wpath; + repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null); // init. search path, escape $ and " + + pp = strchr(searchpath,'*'); + if (pp) { // not efficient but foolproof + while ((*pp != '/') && (pp > searchpath)) pp--; // /aaa/bbb/cc*cc... >>> /aaa/bbb/ + if (pp > searchpath) *(pp+1) = 0; + } + + sprintf(command,"find \"%s\" -type f -or -type l",searchpath); // find files (ordinary, symlink) + fid = popen(command,"r"); + if (! fid) zappcrash(strerror(errno)); + uflag = 763568954; // begin search + } + + if (uflag != 763568954) zappcrash("SearchWild, uflag invalid"); + + while (true) + { + pp = fgets(matchfile,maxfcc-2,fid); // next matching file + if (! pp) { + pclose(fid); // no more + fid = 0; + return 0; + } + + cc = strlen(matchfile); // get rid of trailing \n + matchfile[cc-1] = 0; + + err = MatchWildIgnoreCase(wpath,matchfile); // wildcard match? + if (err) continue; // no + + return matchfile; // return file + } +} + + /**************************************************************************/ // perform a binary search on sorted list of integers // return matching element or -1 if not found +// Benchmark: search a list of 10 million sorted integers +// 0.35 usecs. 3.3 GHz Core i5 int bsearch(int seekint, int nn, int list[]) { @@ -2584,8 +2943,10 @@ } -// perform a binary search on sorted set of records in memory -// return matching record number (0 based) or -1 if not found +// Perform a binary search on sorted set of records in memory. +// Return matching record number (0 based) or -1 if not found. +// Benchmark: search 10 million sorted records of 20 chars. +// 0.61 usecs. 3.3 GHz Core i5 int bsearch(char *seekrec, char *allrecs, int recl, int nrecs) { @@ -2682,6 +3043,37 @@ /************************************************************************** heap sort functions + + void HeapSort(int list[], int nn) + void HeapSort(float flist[], int nn) + void HeapSort(double dlist[], int nn) + void HeapSort(char *plist[], int nn) + + Sort list of nn integers, floats, doubles, or pointers to strings. + Numbers are sorted in ascending order. Pointers are sorted in order of the + strings they point to. The strings are not changed. + + Benchmarks: (3.3 GHz Core i5) + 10 million integers: 1.5 secs + 10 million doubles: 2.4 secs + 2 million pointers to 100 character recs: 1.8 secs + + void HeapSort(char *plist[], int nn, compfunc) + ---------------------------------------------- + Sort list of nn pointers to strings. Pointers are sorted in order of the + strings they point to, which is determined by the caller function compfunc. + The strings are not changed. + + int compfunc(cchar *rec1, cchar *rec2) + compare rec1 to rec2, return -1 0 +1 if rec1 < = > rec2 in sort order. + + void HeapSort(char *recs, int RL, int NR, compfunc) + --------------------------------------------------- + Sort an array of records in memory using a caller-supplied compare function. + recs pointer to 1st record in array + RL record length + NR no. of records + ***************************************************************************/ #define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp) @@ -2948,23 +3340,25 @@ /************************************************************************** - int MemSort (char *RECS, int RL, int NR, int KEYS[][3], int NK) + int MemSort (char *RECS, int RL, int NR, int KEYS[][3], int NK) - RECS is an array of records, to be sorted in-place. - (record length = RL, record count = NR) + RECS is an array of records, to be sorted in-place. + (record length = RL, record count = NR) - KEYS[NK,3] is an integer array defined as follows: - [N,0] starting position of Nth key field in RECS - [N,1] length of Nth key field in RECS - [N,2] type of sort for Nth key: - 1 = char ascending - 2 = char descending - 3 = int*4 ascending (int, long) - 4 = int*4 descending - 5 = float*4 ascending (float) - 6 = float*4 descending - 7 = float*8 ascending (double) - 8 = float*8 descending + KEYS[NK,3] is an integer array defined as follows: + [N,0] starting position of Nth key field in RECS + [N,1] length of Nth key field in RECS + [N,2] type of sort for Nth key: + 1 = char ascending + 2 = char descending + 3 = int*4 ascending (int, long) + 4 = int*4 descending + 5 = float*4 ascending (float) + 6 = float*4 descending + 7 = float*8 ascending (double) + 8 = float*8 descending + Benchmark: 2 million recs of 40 bytes with 4 sort keys: + 2.5 secs (3.3 GHz Core i5). ***/ @@ -3073,83 +3467,331 @@ } -/**************************************************************************/ +/************************************************************************** + bitmap functions -// random number generators with explicit context -// and improved randomness over a small series + bitmap *bmap declare a bitmap pointer + bmap = bitmap_new(nbits) create and initialize all bits to 0 + bitmap_set(bmap, bit, value) set a bit value, 0 or 1 + value = bitmap_get(bmap, bit) get a bit value, 0 or 1 + bitmap_delete(bmap) delete a bitmap, release memory + + nbits int capacity of bitmap in bits + bit int bit number to set or get, 0 .. nbits-1 + value int true or false (1 or 0) -int lrandz(int64 *seed) // returns 0 to 0x7fffffff +***************************************************************************/ + +// create a new bitmap with specified bit length. +// initially all bits are false. + +bitmap * bitmap_new(int nbits) { - *seed = *seed ^ (*seed << 17); - *seed = *seed ^ (*seed << 20); - return nrand48((unsigned int16 *) seed); + int cc, ii; + bitmap *bm; + + bm = (bitmap *) zmalloc(sizeof(bitmap),"bitmap"); + bm->nbits = nbits; + cc = (nbits + 7) / 8; + bm->bits = (uchar *) zmalloc(cc,"bitmap"); + for (ii = 0; ii < cc; ii++) bm->bits[ii] = 0; + return bm; } -int lrandz() + +// set bit in bitmap to true or false + +void bitmap_set(bitmap *bm, int bit, bool value) { - static int64 seed = 12345678; - return lrandz(&seed); + int ii, jj; + uchar bit1; + + if (bit >= bm->nbits) zappcrash("bitmap, bit %d too big",bit); + ii = bit / 8; + jj = bit % 8; + bit1 = 0x80 >> jj; + + if (value) bm->bits[ii] = bm->bits[ii] | bit1; + else { + bit1 = bit1 ^ 0xff; + bm->bits[ii] = bm->bits[ii] & bit1; + } + + return; } -double drandz(int64 *seed) // returns 0.0 to 0.99999... + +// fetch bitmap bit, return true or false + +bool bitmap_get(bitmap *bm, int bit) { - *seed = *seed ^ (*seed << 17); - *seed = *seed ^ (*seed << 20); - return erand48((unsigned int16 *) seed); + int ii, jj; + uchar bit1; + + ii = bit / 8; + jj = bit % 8; + bit1 = bm->bits[ii] << jj; + if (bit1 < 127) return false; + else return true; } -double drandz() + +// delete bitmap + +void bitmap_delete(bitmap *bm) { - static int64 seed = 23459876; - return drandz(&seed); + zfree(bm->bits); + zfree(bm); + return; } /************************************************************************** + variable string list functions - array / list of strings - spline1: define a curve using a set of data points (x and y values) - spline2: for a given x-value, return a y-value fitting the curve + pvlist * pvlist_create(int max) + void pvlist_free(pvlist *pv) + int pvlist_append(pvlist *pv, cchar *entry, int unique) + int pvlist_prepend(pvlist *pv, cchar *entry, int unique) + int pvlist_find(pvlist *pv, cchar *entry) + int pvlist_remove(pvlist *pv, cchar *entry) + int pvlist_remove(pvlist *pv, int Nth) + int pvlist_count(pvlist *pv) + int pvlist_replace(pvlist *pv, int Nth, char *entry) + cchar * pvlist_get(pvlist *pv, int Nth) + int pvlist_sort(pvlist *pv) - For spline1, the no. of curve-defining points must be < 100. - For spline2, the given x-value must be within the range defined in spline1. - - v.5.2: all args are float instead of double + These functions manage a variable length list of variable length strings. + Declare such a list as: pvlist *pv; -***/ +***************************************************************************/ -namespace splinedata +// Creates a pvlist with a capacity of max strings and returns a pointer. +// String lengths are unlimited, but the count of strings is limited to max. +// Memory is allocated for max pointers at first. Memory for the strings is +// allocated and freed as the strings are added or removed. + +pvlist * pvlist_create(int max) { - int nn; - float px1[100], py1[100], py2[100]; -} + pvlist *pv; + pv = (pvlist *) zmalloc(sizeof(pvlist),"pvlist"); + pv->max = max; + pv->act = 0; + pv->list = (char **) zmalloc(max * sizeof(char *),"pvlist"); + return pv; +} -void spline1(int dnn, float *dx1, float *dy1) +// free memory for variable list and contained strings + +void pvlist_free(pvlist *pv) { - using namespace splinedata; - - float sig, p, u[100]; int ii; - nn = dnn; - if (nn > 100) zappcrash("spline1(), > 100 data points"); - - for (ii = 0; ii < nn; ii++) - { - px1[ii] = dx1[ii]; - py1[ii] = dy1[ii]; - if (ii && px1[ii] <= px1[ii-1]) - zappcrash("spline1(), x-value not increasing"); - } + for (ii = 0; ii < pv->act; ii++) + zfree(pv->list[ii]); + zfree(pv->list); + zfree(pv); +} - py2[0] = u[0] = 0; +// append new entry to end of list (optional if unique) +// if list if full, first entry is removed and rest are packed down +// return: N >= 0: new entry added at position N +// N = -1: not unique, not added - for (ii = 1; ii < nn-1; ii++) - { - sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]); - p = sig * py2[ii-1] + 2; - py2[ii] = (sig - 1) / p; - u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1]) +int pvlist_append(pvlist *pv, cchar *entry, int unique) +{ + int ii; + + if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique + + if (pv->act == pv->max) pvlist_remove(pv,0); // if list full, remove 1st entry + + ii = pv->act; + pv->list[ii] = strdupz(entry,0,"pvlist"); // add to end of list + pv->act++; + return ii; +} + +// prepend new entry to list (optional if unique) +// prior list entries are pushed down to make room +// if list is full, last entry is removed first +// return: N = 0: new entry added at position 0 +// N = -1: not unique, not added + +int pvlist_prepend(pvlist *pv, cchar *entry, int unique) +{ + int ii; + + if (unique && pvlist_find(pv,entry) >= 0) return -1; // not unique + + if (pv->act == pv->max) pvlist_remove(pv,pv->act-1); // if list full, remove last entry + + for (ii = pv->act; ii > 0; ii--) // push all list entries down + pv->list[ii] = pv->list[ii-1]; + pv->list[0] = strdupz(entry,0,"pvlist"); // add to start of list + pv->act++; + return 0; +} + +// find list entry by name, return entry (0 based) +// return -1 if not found + +int pvlist_find(pvlist *pv, cchar *entry) +{ + int ii; + + for (ii = 0; ii < pv->act; ii++) + if (strEqu(entry,pv->list[ii])) break; + if (ii < pv->act) return ii; + return -1; +} + +// remove an entry by name and repack list +// return (former) entry or -1 if not found + +int pvlist_remove(pvlist *pv, cchar *entry) +{ + int ii; + + ii = pvlist_find(pv,entry); + if (ii < 0) return -1; + pvlist_remove(pv,ii); + return ii; +} + +// remove an entry by number and repack list +// returns -1 if entry is beyond list end + +int pvlist_remove(pvlist *pv, int ii) +{ + if (ii < 0 || ii >= pv->act) return -1; + zfree(pv->list[ii]); + for (ii++; ii < pv->act; ii++) { + if (! pv->act) printf("meaningless reference %d",ii); // stop g++ optimization bug /// + pv->list[ii-1] = pv->list[ii]; + } + pv->act--; + return 0; +} + + +// return entry count + +int pvlist_count(pvlist *pv) +{ + return pv->act; +} + + +// replace Nth entry with new one + +int pvlist_replace(pvlist * pv, int ii, cchar *entry) +{ + if (ii < 0 || ii >= pv->act) return -1; + zfree(pv->list[ii]); + pv->list[ii] = strdupz(entry,0,"pvlist"); + return 0; +} + + +// return Nth entry or null + +char * pvlist_get(pvlist *pv, int Nth) +{ + if (Nth >= pv->act) return 0; + return pv->list[Nth]; +} + + +// sort list in ascending order + +int pvlist_sort(pvlist *pv) +{ + HeapSort(pv->list,pv->act); + return 0; +} + + +/**************************************************************************/ + +// Random number generators with explicit context +// and improved randomness over a small series. +// Benchmark: lrandz 0.012 usec drandz 0.014 usec 3.3 GHz Core i5 + +int lrandz(int64 *seed) // returns 0 to 0x7fffffff +{ + *seed = *seed ^ (*seed << 17); + *seed = *seed ^ (*seed << 20); + return nrand48((unsigned int16 *) seed); +} + +int lrandz() // implicit seed, repeatable sequence +{ + static int64 seed = 12345678; + return lrandz(&seed); +} + +double drandz(int64 *seed) // returns 0.0 to 0.99999... +{ + *seed = *seed ^ (*seed << 17); + *seed = *seed ^ (*seed << 20); + return erand48((unsigned int16 *) seed); +} + +double drandz() // implicit seed, repeatable sequence +{ + static int64 seed = 23459876; + return drandz(&seed); +} + + +/************************************************************************** + + spline1: define a curve using a set of data points (x and y values) + spline2: for a given x-value, return a y-value fitting the curve + + For spline1, the no. of curve-defining points must be < 100. + For spline2, the given x-value must be within the range defined in spline1. + + The algorithm was taken from the book "Numerical Recipes" + (Cambridge University Press) and converted from Fortran to C++. + +***/ + +namespace splinedata +{ + int nn; + float px1[100], py1[100], py2[100]; +} + + +void spline1(int dnn, float *dx1, float *dy1) +{ + using namespace splinedata; + + float sig, p, u[100]; + int ii; + + nn = dnn; + if (nn > 100) zappcrash("spline1(), > 100 data points"); + + for (ii = 0; ii < nn; ii++) + { + px1[ii] = dx1[ii]; + py1[ii] = dy1[ii]; + if (ii && px1[ii] <= px1[ii-1]) + zappcrash("spline1(), x-value not increasing"); + } + + py2[0] = u[0] = 0; + + for (ii = 1; ii < nn-1; ii++) + { + sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]); + p = sig * py2[ii-1] + 2; + py2[ii] = (sig - 1) / p; + u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1]) / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p; } @@ -3191,19 +3833,39 @@ Initialize application files according to following conventions: // new version + binary executable is at: /prefix/bin/appname // = PREFIX/bin/appname + other application directories are derived as follows: - /prefix/share/appname/data/ desktop, parameters, etc. - /prefix/share/doc/appname/ README, CHANGES, user guide - /prefix/share/appname/icons/ icon files .png - /prefix/share/appname/locales/ translations: appname-de.po etc. - /home/user/.appname/ parameters etc. are copied here - /home/user/.appname/ appname.log log file is here - - zprefix install location has /bin and /share subtrees - zdatadir installed data files .desktop, parameters, etc. - zdocdir user documentation README, changelog, user guide - zicondir icons icon files .png - zlocalesdir translation files appname-de.po, etc. - zuserdir /home/user/.appname log file, user parameters + /prefix/share/appname/data/ desktop, parameters ... + /prefix/share/doc/appname/ README, changelog, userguide-xx.html ... + /prefix/share/appname/icons/ icon files: filename.png + /prefix/share/appname/locales/ translate-xx.po ... (original) + /home/user/.appname/ some installation files are copied here + /home/user/.appname/logfile log file with error messages + /home/user/.appname/locales translate-xx.po ... (user modified) + + zprefix install location normally /usr, has subtrees /bin /share /doc + zdatadir installed data files /prefix/share/appname/data/ + zdocdir documentation files /prefix/share/doc/appname/ + zicondir icons /prefix/share/appname/icons/ + zlocalesdir translation files /prefix/share/appname/locales/ + zuserdir local app files /home//.appname + /home//.appname/locales + + If it does not already exist, an application directory for the current user is + created at /home/username/.appname (following common Linux convention). + If this directory was created for the first time, copy specified files + (following the 1st argument) from the install directory into the newly created + user-specific directory. The assumption is that all initial data files for the + application (e.g. parameters) will be in the install data directory, and these are + copied to the user directory where the user or application can modify them. + + If the running program is not connected to a terminal device, stdout and stderr are + redirected to the log file at /home/user/.appname/logfile + + char * get_zprefix() returns install top directory (has /bin and /share under it) + char * get_zuserdir() returns /home/user/.appname (or /root/.appname) + char * get_zdatadir() returns directory where application data files reside + char * get_zdocdir() returns directory for application documentation files + char * get_zicondir() returns directory for application icons + char * get_zlocalesdir() returns directory for translation files ***/ @@ -3213,8 +3875,7 @@ char zprefix[200], zdatadir[200], zdocdir[200]; // app directories char zicondir[200], zlocalesdir[200], zuserdir[200]; char zlang[8] = "en"; // "lc" or "lc_RC" - char JPGquality[4] = "85"; // JPG file save quality - cchar *F1_help_topic = 0; // current F1 help topic + char JPGquality[4] = "90"; // JPG file save quality int open_popup_windows = 0; // open popup window count pthread_t tid_main = 0; // main thread ID } @@ -3234,7 +3895,7 @@ char work[200]; char logfile[200], oldlog[200]; cchar *appfile; - int secs, err; + int cc, secs, err; time_t Tnow; char *chTnow; struct stat statdat; @@ -3256,18 +3917,23 @@ strncatv(zlocalesdir,199,work,"/share/",zappname,"/locales",null); // /prefix/share/appname/locales strncatv(zdocdir,199,work,"/share/doc/",zappname,null); // /prefix/share/doc/appname - snprintf(zuserdir,199,"%s/.%s",getenv("HOME"),zappname); // /home/user/.appname/ + #ifdef DOCDIR + strncpy0(zdocdir,DOCDIR,199); // flexible DOCDIR location + #endif + + snprintf(zuserdir,199,"%s/.%s",getenv("HOME"),zappname); // /home//.appname/ + cc = strlen(zuserdir); // stop humongous username v.5.3 + if (cc > 160) zappcrash("too big: %s",zuserdir); + err = stat(zuserdir,&statdat); // does it exist already? if (err) { err = mkdir(zuserdir,0750); // no, create and initialize if (err) zappcrash("cannot create %s",zuserdir); va_start(arglist,appname); // copy req. application files - while (true) { // from install directory to - appfile = va_arg(arglist, cchar *); // to /home/user/.appname/ + while (true) { // from /prefix/share/appname/data/* + appfile = va_arg(arglist, cchar *); // to /home/user/.appname/* if (! appfile) break; - snprintf(work,199,"cp %s/%s %s",zdatadir,appfile,zuserdir); - err = system(work); - if (err) printf("%s %s \n",work,wstrerror(err)); + err = shell_ack("cp %s/%s %s",zdatadir,appfile,zuserdir); } va_end(arglist); } @@ -3278,218 +3944,190 @@ chTnow[19] = 0; if (! isatty(1)) { // not attached to a terminal - snprintf(logfile,199,"%s/%s.log",zuserdir,zappname); - snprintf(oldlog,199,"%s/%s.log.old",zuserdir,zappname); + snprintf(logfile,199,"%s/logfile",zuserdir); // /home//logfile v.5.5 + snprintf(oldlog,199,"%s/logfile.old",zuserdir); err = stat(logfile,&statdat); if (! err) { secs = Tnow - statdat.st_mtime; // if log file age > 1 hour v.5.0 if (secs > 3600) rename(logfile,oldlog); // rename to *.old } fid = freopen(logfile,"a",stdout); // redirect output to log file - fid = freopen(logfile,"a",stderr); // /home/user/.appname/appname.log - if (! fid) printf("cannot redirect stdout and stderr \n"); + fid = freopen(logfile,"a",stderr); + if (! fid) printf("*** cannot redirect stdout and stderr \n"); } - printf("\n =========== %s %s \n",zappname,chTnow); + printf("\n =========== start %s %s \n",zappname,chTnow); + fflush(0); // v.5.2 return 1; } -/**************************************************************************/ - -// Display help file in a separate process so application is not blocked. -// help file: /zdocdir/userguide-lc_RC.html (or) *-lc.html (or) *-en.html -// context: optional arg. show file starting at internal link = context -// look for user guide file in /usr/share/doc/appname/ -// and in /usr/share/doc/appname/extras/ // distros are different +// Find a locale-dependent installation file or user file. +// file type: doc, data, locale, user, userlocale +// file name: README, changelog, userguide.html, parameters, translate.po ... +// Returns complete file name, e.g. /usr/share/appname/locales/translate-xx.po +// Output filespec should be 200 bytes (limit for all installation files). +// Returns 0 if OK, +N if not found. -void showz_userguide(cchar *context) +int locale_filespec(cchar *filetype, cchar *filename, char *filespec) // v.5.5 { using namespace zfuncs; - int err; - char docfile[200], url[200], lang[4]; - - snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,zlang); // look for userguide-lc_RC.html - err = access(docfile,R_OK); - if (! err) goto add_context; + char *pp, fname[20], fext[8]; + char lc_RC[8]; // -lc or -lc_RC + int cc, err; + struct stat statb; + + filespec[0] = '/'; + strcat(filespec,filetype); // leave /type as default + + if (strEqu(filetype,"doc")) strcpy(filespec,zdocdir); // /usr/share/doc/appname + if (strEqu(filetype,"data")) strcpy(filespec,zdatadir); // /usr/share/appname/data + if (strEqu(filetype,"locale")) strcpy(filespec,zlocalesdir); // /usr/share/appname/locales + if (strEqu(filetype,"user")) strcpy(filespec,zuserdir); // /home//.appname + if (strEqu(filetype,"userlocale")) { + strcpy(filespec,zuserdir); // /home//.appname/locales + strcat(filespec,"/locales"); + } + + strncpy0(fname,filename,20); + pp = strchr(fname,'.'); + if (pp) { + strcpy(fext,pp); // file type .fext + *pp = 0; + } + else *fext = 0; // no type + + lc_RC[0] = '-'; + strncpy0(lc_RC+1,zlang,6); // locale with region code: -lc_RC + +tryextras: + + cc = strlen(filespec); + filespec[cc] = '/'; // /directories.../ + strcpy(filespec+cc+1,fname); // /directories.../fname + cc = strlen(filespec); // | + pp = filespec + cc; // pp + + strcpy(pp,lc_RC); // /directories.../fname-lc_RC.fext + strcat(pp,fext); + err = stat(filespec,&statb); + if (! err) return 0; + + strcpy(pp+3,fext); // /directories.../fname-lc.fext + err = stat(filespec,&statb); + if (! err) return 0; + + strcpy(pp,"-en"); // /directories.../fname-en.fext + strcat(pp,fext); + err = stat(filespec,&statb); + if (! err) return 0; + + strcpy(pp,fext); // /directories.../fname.fext + err = stat(filespec,&statb); + if (! err) return 0; + + if (strEqu(filetype,"doc")) { // these files may be placed in + strcpy(filespec,zdocdir); // /usr/share/doc/appname/extras + strcat(filespec,"/extras"); // due to Linux chaos + filetype = ""; + goto tryextras; // try again using /extras + } - snprintf(docfile,199,"%s/extras/userguide-%s.html",zdocdir,zlang); - err = access(docfile,R_OK); - if (! err) goto add_context; + printf("file not found: %s %s \n",filetype,filename); + return 1; +} - strncpy0(lang,zlang,3); - snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,lang); // look for userguide-lc.html - err = access(docfile,R_OK); - if (! err) goto add_context; - snprintf(docfile,199,"%s/extras/userguide-%s.html",zdocdir,lang); - err = access(docfile,R_OK); - if (! err) goto add_context; +/**************************************************************************/ - snprintf(docfile,199,"%s/userguide-en.html",zdocdir); // look for userguide-en.html - err = access(docfile,R_OK); - if (! err) goto add_context; +// Display help file in a separate process so application is not blocked. +// help file: /zdatadir/userguide-lc_RC.html (or) *-lc.html (or) *-en.html +// context: optional arg. show file starting at internal link = context +// look for user guide file in /usr/share/data/appname/ [ extras/ ] - snprintf(docfile,199,"%s/extras/userguide-en.html",zdocdir); - err = access(docfile,R_OK); - if (! err) goto add_context; +void showz_userguide(cchar *context) // v.5.5 +{ + char filespec[200], url[200]; + int err; + + err = locale_filespec("data","userguide.html",filespec); - zmessageACK(null,ZTX("help file not found: %s"),docfile); // give up - return; + if (err) { + zmessageACK(null,null,ZTX("user guide not found")); + return; + } + + snprintf(url,199,"file://%s",filespec); -add_context: + if (context && *context) // specific topic wanted + strncatv(url,199,"#",context,null); // file://.../userguide-xx.html#context - if (context && *context) - strncatv(docfile,199,"#",context,null); // file://.../userguide-xx.html#context - snprintf(url,199,"file://%s",docfile); showz_html(url); - return; } /**************************************************************************/ -// display various admin text files in a popup window +// display application log file in a popup window +// The log file is /home//.appname/logfile -void showz_readme() +void showz_logfile() // log file v.5.2 { - showz_doctext("README"); - return; -} + using namespace zfuncs; -void showz_changelog() -{ - showz_doctext("changelog"); - return; -} + char buff[200]; -void showz_logfile() // log file v.5.2 -{ - char buff[100]; - fflush(stdout); - fflush(stderr); - snprintf(buff,99,"cat %s/%s.log",get_zuserdir(),zfuncs::zappname); + fflush(0); // v.5.2 + snprintf(buff,199,"cat %s/logfile",zuserdir); popup_command(buff,800,600); return; } -void showz_translations() -{ - showz_doctext("translations"); - return; -} - -// find and show a text in a popup window +// find and show a text file in /usr/share/doc/appname/ +// or /usr/share/appname/data // the text file may also be a compressed .gz file -// look for: /usr/share/doc/appname/file.gz -// /usr/share/doc/appname/file // depends on distro chaos -// /usr/share/doc/appname/extras/file.gz -// /usr/share/doc/appname/extras/file +// type is "doc" or "data" -void showz_doctext(const char *file) +void showz_textfile(const char *type, const char *file) // v.5.5 { - using namespace zfuncs; - - struct stat statb; - char buff1[200], buff2[220]; - cchar *command; + char filex[40], filespec[200], command[200]; int err; - command = "zcat"; - snprintf(buff1,199,"%s/%s.gz",zdocdir,file); // look for .../appname/file.gz - err = stat(buff1,&statb); - if (! err) goto showit; - - command = "cat"; - snprintf(buff1,199,"%s/%s",zdocdir,file); // look for .../appname/file - err = stat(buff1,&statb); - if (! err) goto showit; + strncpy0(filex,file,36); - command = "zcat"; - snprintf(buff1,199,"%s/extras/%s.gz",zdocdir,file); // look for .../appname/extras/file.gz - err = stat(buff1,&statb); - if (! err) goto showit; - - command = "cat"; - snprintf(buff1,199,"%s/extras/%s",zdocdir,file); // look for .../appname/extras/file - err = stat(buff1,&statb); - if (! err) goto showit; + err = locale_filespec(type,filex,filespec); + if (! err) { + snprintf(command,200,"cat %s",filespec); + popup_command(command,600,400); + return; + } - command = "echo file not found:"; - snprintf(buff1,199,"%s/%s",zdocdir,file); + strcat(filex,".gz"); -showit: - snprintf(buff2,219,"%s %s",command,buff1); - popup_command(buff2,600,400); + err = locale_filespec(type,filex,filespec); + if (! err) { + snprintf(command,200,"zcat %s",filespec); + popup_command(command,600,400); + return; + } + + zmessageACK(0,0,"file not found: %s %s",type,file); return; } -/**************************************************************************/ - -// create a desktop icon / launcher with icon -// target system needs to be LSB compliant +// show a local or remote html file using the user's preferred browser +// to show a local file starting at an internal live link location: +// url = "file://directory/.../filename#livelink -void zmake_menu_launcher(cchar *command, cchar *categories, cchar *genericname) +void showz_html(cchar *url) { - using namespace zfuncs; - - char appname[20], dtfile[200], work[200]; - cchar *pp; - FILE *fid; - int err; - - pp = strField(command,' ',1); - if (! pp) pp = "?"; - strncpy0(appname,pp,20); - - snprintf(dtfile,199,"%s/Desktop/kornelix-%s.desktop",getenv("HOME"),appname); - fid = fopen(dtfile,"w"); - if (! fid) { - zmessageACK(null,ZTX("error: %s"),strerror(errno)); - return; - } - - fputs("[Desktop Entry]\n",fid); // [Desktop Entry] - snprintf(work,199,"Name=%s\n",appname); // Name=appname - fputs(work,fid); - snprintf(work,199,"Categories=%s\n",categories); // Categories=Cat1;Cat2; ... - fputs(work,fid); - snprintf(work,199,"GenericName=%s\n",genericname); // GenericName=generic app name - fputs(work,fid); - fputs("Type=Application\n",fid); // Type=Application - fputs("Terminal=false\n",fid); // Terminal=false - snprintf(work,199,"Exec=%s/bin/%s\n",zprefix,command); // Exec=/usr/bin/appname -options - fputs(work,fid); - snprintf(work,199,"Icon=%s/%s.png\n",zicondir,appname); // Icon=/usr/share/appname/icons/appname.png - fputs(work,fid); - fclose(fid); - - snprintf(work,199,"chmod 0750 %s",dtfile); // make executable - err = system(work); - if (err) zmessLogACK(null,"error: %s",wstrerror(err)); - - snprintf(work,199,"xdg-desktop-menu install %s",dtfile); // add menu entry - err = system(work); - if (err) zmessLogACK(null,"error: %s",wstrerror(err)); - - return; -} - - -// show a local or remote html file using the user's preferred browser -// to show a local file starting at an internal live link location: -// url = "file://directory/.../filename#livelink - -void showz_html(cchar *url) -{ - char command[500]; - static char prog[20]; - static int ftf = 1, err; + static char prog[20]; + static int ftf = 1, err; if (ftf) { ftf = 0; @@ -3511,9 +4149,56 @@ return; } - snprintf(command,499,"%s %s &",prog,url); - printf(" %s \n",command); - err = system(command); + shell_ack("%s %s &",prog,url); + return; +} + + +/**************************************************************************/ + +// Creates a desktop icon / launcher and a system menu entry. +// The menu name is taken from the input command, without options. +// A command like "mycom -optA -optB" would generate a menu name of "mycom". +// The categories should be separated by semicolons and conform to LSB categories. +// The generic name is free text to describe the application, e.g. "Image Editor". +// If the target system is not LSB compliant this function will not work. + +void zmake_menu_launcher(cchar *command, cchar *categories, cchar *genericname) +{ + using namespace zfuncs; + + char appname[20], dtfile[200], work[200]; + cchar *pp; + FILE *fid; + + pp = strField(command,' ',1); + if (! pp) pp = "?"; + strncpy0(appname,pp,20); + + snprintf(dtfile,199,"%s/Desktop/kornelix-%s.desktop",getenv("HOME"),appname); + fid = fopen(dtfile,"w"); + if (! fid) { + zmessageACK(null,0,ZTX("error: %s"),strerror(errno)); + return; + } + + fputs("[Desktop Entry]\n",fid); // [Desktop Entry] + snprintf(work,199,"Name=%s\n",appname); // Name=appname + fputs(work,fid); + snprintf(work,199,"Categories=%s\n",categories); // Categories=Cat1;Cat2; ... + fputs(work,fid); + snprintf(work,199,"GenericName=%s\n",genericname); // GenericName=generic app name + fputs(work,fid); + fputs("Type=Application\n",fid); // Type=Application + fputs("Terminal=false\n",fid); // Terminal=false + snprintf(work,199,"Exec=%s/bin/%s\n",zprefix,command); // Exec=/usr/bin/appname -options + fputs(work,fid); + snprintf(work,199,"Icon=%s/%s.png\n",zicondir,appname); // Icon=/usr/share/appname/icons/appname.png + fputs(work,fid); + fclose(fid); + + shell_ack("chmod 0750 %s",dtfile); // make executable + shell_ack("xdg-desktop-menu install %s",dtfile); // add menu entry return; } @@ -3522,7 +4207,14 @@ GTK utility functions **************************************************************************/ -// Iterate main loop every "skip" calls +// Iterate main loop every "skip" calls. +// If called within the main() thread, does a GTK main loop to process menu events, etc. +// You must do this periodically within long-running main() thread tasks if you wish to +// keep menus, buttons, output windows, etc. alive and working. The skip argument will +// cause the function to do nothing for skip calls, then perform the normal function. +// This allows it to be imbedded in loops with little execution time penalty. If skip = 1000, +// then zmainloop() will do nothing for 1000 calls, execute normally, do nothing for 1000 calls, +// execute normally, etc. If called from a thread, zmainloop() does nothing. void zmainloop(int skip) { @@ -3556,7 +4248,7 @@ /**************************************************************************/ -// write message to text view window +// write message to GTK text view window // line: +N existing lines from top (replace) // -N existing lines from bottom (replace) // 0 next line (add new line at bottom) @@ -3575,9 +4267,10 @@ return; } - zthreadcrash(); - + zthreadcrash(); // thread usage not allowed + textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); + if (! textBuff) return; // v.5.5 endMark = gtk_text_buffer_get_mark(textBuff,"wpxend"); // get my end mark @@ -3622,26 +4315,26 @@ return; } -void wprintf(GtkWidget *mLog, int line, cchar *mess, ... ) // "printf" version +void wprintf(GtkWidget *mLog, int line, cchar *format, ... ) // "printf" version { va_list arglist; char message[1000]; - va_start(arglist,mess); - vsnprintf(message,999,mess,arglist); + va_start(arglist,format); + vsnprintf(message,999,format,arglist); va_end(arglist); wprintx(mLog,line,message); return; } -void wprintf(GtkWidget *mLog, cchar *mess, ... ) // "printf", scrolling output +void wprintf(GtkWidget *mLog, cchar *format, ... ) // "printf", scrolling output { va_list arglist; char message[1000]; - va_start(arglist,mess); - vsnprintf(message,999,mess,arglist); // stop overflow, remove warning + va_start(arglist,format); + vsnprintf(message,999,format,arglist); // stop overflow, remove warning va_end(arglist); wprintx(mLog,0,message); @@ -3661,6 +4354,8 @@ GtkTextMark *mark; if (! mLog) return; + + zthreadcrash(); // thread usage not allowed textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); if (line <= 0) line = gtk_text_buffer_get_line_count(textbuff); @@ -3682,6 +4377,8 @@ if (! mLog) return; + zthreadcrash(); // thread usage not allowed + buff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); gtk_text_buffer_set_text(buff,"",-1); return; @@ -3697,6 +4394,8 @@ if (! mLog) return; + zthreadcrash(); // thread usage not allowed + textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog)); gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1); // iter at line start gtk_text_buffer_get_end_iter(textBuff,&iter2); @@ -3707,8 +4406,11 @@ /**************************************************************************/ -// get text records from a text view window, one per call -// removes trailing new line characters ( \n ) +// Read from a text widget, one line at a time. +// Set ftf = 1 for first call (will be returned = 0). +// The next line of text is returned, or null if no more text. +// A final \n character is removed if present. +// Only one line is remembered, so it must be copied before the next call. char * wscanf(GtkWidget *mLog, int & ftf) { @@ -3719,6 +4421,8 @@ if (! mLog) return 0; + zthreadcrash(); // thread usage not allowed + if (ftf) { // get all window text ftf = 0; @@ -3763,7 +4467,7 @@ fid = fopen(filespec,"w"); // open file if (! fid) { - zmessageACK(null,ZTX("cannot open file %s"),filespec); + zmessageACK(null,null,ZTX("cannot open file %s"),filespec); return 1; } @@ -3780,7 +4484,10 @@ } err = fclose(fid); // close file - if (err) { zmessageACK(null,"file close error"); return 2; } + if (err) { + zmessageACK(null,null,"file close error"); + return 2; + } else return 0; } @@ -3799,7 +4506,7 @@ file = zgetfile1(ZTX("save screen to file"),"save","screen-save.txt"); if (! file) return; err = wfiledump(mLog,file); - if (err) zmessageACK(null,"save screen failed (%d)",err); + if (err) zmessageACK(null,null,"save screen failed (%d)",err); zfree(file); return; } @@ -3813,7 +4520,7 @@ void wprintp(GtkWidget *mLog) { int pid, err; - char tempfile[50], command[200]; + char tempfile[50]; if (! mLog) return; @@ -3823,92 +4530,217 @@ if (err) return; if (wfiledump_maxcc < 97) - snprintf(command,199,"lp -o %s -o %s -o %s -o %s -o %s -o %s %s", - "cpi=14","lpi=8","page-left=50","page-top=50", - "page-right=40","page-bottom=40",tempfile); - + err = shell_ack("lp -o %s -o %s -o %s -o %s -o %s -o %s %s", + "cpi=14","lpi=8","page-left=50","page-top=50", + "page-right=40","page-bottom=40",tempfile); else - snprintf(command,199,"lp -o %s -o %s -o %s -o %s -o %s -o %s -o %s %s", - "landscape","cpi=14","lpi=8","page-left=50","page-top=50", - "page-right=40","page-bottom=40",tempfile); - - err = system(command); - if (err) zmessLogACK(null,"print error %s",wstrerror(err)); + err = shell_ack("lp -o %s -o %s -o %s -o %s -o %s -o %s -o %s %s", + "landscape","cpi=14","lpi=8","page-left=50","page-top=50", + "page-right=40","page-bottom=40",tempfile); return; } -/************************************************************************** - simplified GTK menu bar, tool bar, status bar functions -***************************************************************************/ +/**************************************************************************/ -// create menu bar and add to vertical packing box +// Set a function to be called when a GTK text view widget is mouse-clicked. +// Function returns the clicked line number and position, both zero based. +// A wrapped line is still one logical line. +// Note: cannot be used on text windows that are edited. +// The called function looks like this: +// void clickfunc(GtkWidget *widget, int &line, int &pos) -GtkWidget * create_menubar(GtkWidget *vbox) // icon size removed +void textwidget_set_clickfunc(GtkWidget *widget, clickfunc_t clickfunc) { - GtkWidget *wmbar; + void textwidget_mousefunc(GtkWidget *widget, GdkEventButton *event, clickfunc_t clickfunc); - wmbar = gtk_menu_bar_new(); - gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0); - return wmbar; + gtk_widget_add_events(widget,GDK_ALL_EVENTS_MASK); // connect events v.5.2 + G_SIGNAL(widget,"event-after",textwidget_mousefunc,clickfunc); + return; } +void textwidget_mousefunc(GtkWidget *widget, GdkEventButton *event, clickfunc_t clickfunc) +{ + static GdkCursor *arrowcursor = 0; + GdkWindow *gdkwin; + GtkTextIter iter; + int mpx, mpy, tbx, tby, line, pos; -// add menu item to menu bar + if (event->type == GDK_BUTTON_PRESS) + { + mpx = int(event->x); // mouse click position + mpy = int(event->y); + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(widget), + GTK_TEXT_WINDOW_TEXT,mpx,mpy,&tbx,&tby); + gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget),&iter,tbx,tby); + line = gtk_text_iter_get_line(&iter); + pos = gtk_text_iter_get_line_offset(&iter); + clickfunc(widget,line,pos); // call user function + } -GtkWidget * add_menubar_item(GtkWidget *wmbar, cchar *mname, mtFunc func) -{ - GtkWidget *wmitem; + if (! arrowcursor) // why must this be repeated ?? + arrowcursor = gdk_cursor_new(GDK_TOP_LEFT_ARROW); + gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),GTK_TEXT_WINDOW_TEXT); + if (gdkwin) // v.5.5 + gdk_window_set_cursor(gdkwin,arrowcursor); - wmitem = gtk_menu_item_new_with_label(mname); - gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem); - if (func) G_SIGNAL(wmitem,"activate",func,mname); - return wmitem; + return; } -// create a popup menu +// get a given line of text from a GTK text view widget -GtkWidget * create_popmenu() +char * textwidget_get_line(GtkWidget *widget, int line) { - GtkWidget *popmenu; + GtkTextBuffer *textbuffer; + GtkTextIter iter1, iter2; + char *text, *ztext; + int cc; - popmenu = gtk_menu_new(); - return popmenu; + textbuffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); + gtk_text_buffer_get_iter_at_line(textbuffer,&iter1,line); + iter2 = iter1; + gtk_text_iter_forward_line(&iter2); + text = gtk_text_buffer_get_text(textbuffer,&iter1,&iter2,0); + if (! text) return 0; + ztext = strdupz(text,0,"textwidget"); + free(text); + cc = strlen(ztext); + while (cc && ztext[cc-1] < ' ') cc--; + ztext[cc] = 0; + if (cc == 0) { + zfree(ztext); + return 0; + } + return ztext; } -// add a menu item to a popup menu +// get the clicked word within the line +// words are defined by line starts and ends, and the given delimiter -GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, mtFunc func) +char * textwidget_get_word(char *line, int pos, cchar *dlims, char &end) // get delimited word at clicked position { - GtkWidget *wmitem; + char *pp1, *pp2, *ztext; + int cc; - wmitem = gtk_menu_item_new_with_label(mname); - gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),wmitem); - if (func) G_SIGNAL(wmitem,"activate",func,mname); - return wmitem; + if (! line) return 0; + pos = utf8_position(line,pos); // graphic position to byte position + if (pos < 0) return 0; // v.5.5 + + pp1 = line + pos; + if (! *pp1 || strchr(dlims,*pp1)) return 0; // reject edge position or delimiter + while (pp1 > line && ! strchr(dlims,pp1[-1])) pp1--; // find start of word + pp2 = pp1; + while (pp2[1] && ! strchr(dlims,pp2[1])) pp2++; // find following delimiter or EOL + end = pp2[1]; // return delimiter + while (*pp1 == ' ') pp1++; // no leading or trailing blanks + while (*pp2 == ' ') pp2--; + cc = pp2 - pp1 + 1; + if (cc < 1) return 0; // all blanks? + ztext = zmalloc(cc+1,"textwidget"); + strncpy0(ztext,pp1,cc+1); + return ztext; } -// pop-up the menu at current mouse position +/************************************************************************** + simplified GTK menu bar, tool bar, status bar functions + + These functions simplify the creation of GTK menus and toolbars. + The functionality is limited but adequate for most purposes. -void popup_menu(GtkWidget *popmenu) + mbar = create_menubar(vbox) create menubar + mitem = add_menubar_item(mbar, label, func) add menu item to menubar + msub = add_submenu_item(mitem, label, icon, func, tip) add submenu item to menu or submenu + + tbar = create_toolbar(vbox, iconsize) create toolbar + add_toolbar_button(tbar, label, tip, icon, func) add button to toolbar + + stbar = create_stbar(vbox) create status bar + stbar_message(stbar, message) display message in status bar + + These functions to the following: + * create a menu bar and add to existing window verticle packing box + * add menu item to menu bar + * add submenu item to menu bar item or submenu item + * create a toolbar and add to existing window + * add button to toolbar, using stock icon or custom icon + * create a status bar and add to existing window + * display a message in the status bar + + argument definitions: + vbox GtkWidget * a verticle packing box (in a window) + mbar GtkWidget * reference for menu bar + popup GtkWidget * reference for popup menu + mitem GtkWidget * reference for menu item (in a menu bar) + msub GtkWidget * reference for submenu item (in a menu) + label cchar * menu or toolbar name or label + tbar GtkWidget * reference for toolbar + tip cchar * tool button tool tip (popup text via mouse-over) + icon cchar * stock icon name or custom icon file name (see below) + func see below menu or tool button response function + arg cchar * argument to response function + stbar int reference for status bar + message cchar * message to display in status bar + + The icon argument for the function add_toolbar_button() has two forms. + For a GTK stock item referenced with a macro like GTK_STOCK_OPEN, use the + corresponding text name, like "gtk-open". + + For a custom icon, use the icon's file name like "my-icon.png". + The file is expected to be in getz_datadir()/icons. + The icon file may be any size, and is resized to 32x32 for use on the toolbar. + If the file is not found, the stock icon "gtk-missing-image" is used + (".png" and ".jpg" files both work). + + For a button with no icon (text label only), use 0 or null for the icon argument. + For a menu separator, use the menu name "separator". + For a toolbar separator, use the label "separator". + For a title menu (no response function), set the response function to null. + + The response function for both menus and toolbar buttons looks like this: + void func(GtkWidget *, cchar *) + + The following macro is also supplied to simplify the coding of response functions: + G_SIGNAL(window,event,func,arg) which expands to: + g_signal_connect(G_OBJECT(window),event,G_CALLBACK(func),(void *) arg) + +***************************************************************************/ + +// create menu bar and add to vertical packing box + +GtkWidget * create_menubar(GtkWidget *vbox) // icon size removed { - gtk_menu_popup(GTK_MENU(popmenu),0,0,0,0,0,gtk_get_current_event_time()); - gtk_widget_show_all(popmenu); - return; + GtkWidget *wmbar; + + wmbar = gtk_menu_bar_new(); + gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0); + return wmbar; +} + + +// add menu item to menu bar, with optional response function + +GtkWidget * add_menubar_item(GtkWidget *wmbar, cchar *mname, mtFunc func) +{ + GtkWidget *wmitem; + + wmitem = gtk_menu_item_new_with_label(mname); + gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem); + if (func) G_SIGNAL(wmitem,"activate",func,mname); + return wmitem; } -// add submenu item to menu item, optional response function +// add submenu item to menu item, with optional response function -GtkWidget * add_submenu_item(GtkWidget *wmitem, cchar *mlab, mtFunc func) +GtkWidget * add_submenu_item(GtkWidget *wmitem, cchar *mlab, mtFunc func, cchar *mtip) { GtkWidget *wmsub, *wmsubitem; GtkWidget *wicon = 0; - wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem)); + wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem)); // add submenu if not already if (wmsub == null) { wmsub = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub); @@ -3917,19 +4749,25 @@ if (strEqu(mlab,"separator")) wmsubitem = gtk_separator_menu_item_new(); else { - if (wicon) { + if (wicon) { // add menu item with label and icon wmsubitem = gtk_image_menu_item_new_with_label(mlab); gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(wmsubitem),wicon); } - else wmsubitem = gtk_menu_item_new_with_label(mlab); + else wmsubitem = gtk_menu_item_new_with_label(mlab); // add menu item with label only } - gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem); - if (func) G_SIGNAL(wmsubitem,"activate",func,mlab); + gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem); // append submenu item to submenu + + if (func) G_SIGNAL(wmsubitem,"activate",func,mlab); // connect optional response function + + if (mtip) g_object_set(G_OBJECT(wmsubitem),"tooltip-text",mtip,null); // add optional popup menu tip v.5.2 + return wmsubitem; } +/**************************************************************************/ + // create toolbar and add to vertical packing box int tbIconSize = 24; // valid during toolbar construction @@ -3989,69 +4827,1113 @@ } -// create a status bar and add to the start of a packing box +/**************************************************************************/ + +// create a status bar and add to the start of a packing box + +GtkWidget * create_stbar(GtkWidget *pbox) +{ + GtkWidget *stbar; + static PangoFontDescription *fontdesc; + + stbar = gtk_statusbar_new(); + fontdesc = pango_font_description_from_string("Monospace 9"); + gtk_widget_modify_font(stbar,fontdesc); // *** GTK does not work *** + gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0); + gtk_widget_show(stbar); + return stbar; +} + + +// display message in status bar + +int stbar_message(GtkWidget *wstbar, cchar *message) +{ + static int ctx = -1; + + if (ctx == -1) + ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all"); + gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx); + gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message); + return 0; +} + + +/************************************************************************** + + Popup Menu + + popup = create_popmenu() create a popup menu + mitem = add_popmenu_item(popup, label, func, arg, tip) add menu item to popup menu + popup_menu(GtkWidget *win, popup) popup the menu at mouse position + + GtkWidget *popup, *mitem + cchar *label, *arg, *tip + void func(GtkWidget *, cchar *arg) + + Call 'create_popmenu' and then 'add_popmenu_item' for each item in the menu. + 'label' is the menu name, 'func' the response function, 'arg' an argument + for 'func', and 'tip' is a tool-tip. 'arg' and 'tip' may be null. + A call to 'popup_menu' will show all menu entries at the mouse position. + Clicking an entry will call the respective response function. + Hovering on the entry will show the tool-tip. + + The response function looks like this: + void func(GtkWidget *, cchar *) + +***/ + +// create a popup menu + +GtkWidget * create_popmenu() +{ + int popmenu_event(GtkWidget *, GdkEvent *); + + GtkWidget *popmenu; + popmenu = gtk_menu_new(); + + gtk_widget_add_events(popmenu,GDK_BUTTON_PRESS_MASK); // v.5.5 + G_SIGNAL(popmenu,"button-press-event",popmenu_event,0); + return popmenu; +} + + +// handle mouse button event in a popup menu + +int popmenu_event(GtkWidget *popmenu, GdkEvent *event) // v.5.5 +{ + if (((GdkEventButton *) event)->button != 1) // if not left mouse, kill menu + gtk_menu_popdown(GTK_MENU(popmenu)); + return 0; +} + + +// add a menu item to a popup menu + +GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, mtFunc func, cchar *arg, cchar *mtip) +{ + GtkWidget *wmitem; + + wmitem = gtk_menu_item_new_with_label(mname); + if (mtip) g_object_set(G_OBJECT(wmitem),"tooltip-text",mtip,null); // add optional popup menu tip v.5.5 + + gtk_menu_shell_append(GTK_MENU_SHELL(popmenu),wmitem); + if (func) { + if (arg) G_SIGNAL(wmitem,"activate",func,arg); // call func with arg v.5.3 + else G_SIGNAL(wmitem,"activate",func,mname); // call func with menu name + } + return wmitem; +} + + +// pop-up the menu at current mouse position + +void popup_menu(GtkWidget *widget, GtkWidget *popmenu) +{ + gtk_menu_popup(GTK_MENU(popmenu),0,0,0,0,1,GDK_CURRENT_TIME); + gtk_widget_show_all(popmenu); + return; +} + + +/************************************************************************** + + Customizable Graphic Popup Menu + + void gmenuz(GtkWidget *parent, cchar *configfile, callbackfunc) + + Open a popup window with a customizable graphic menu. + parent parent window or null + configfile menu configuration file, will be created if missing + callbackfunc callback function to receive clicked menu entry: + typedef void callbackfunc(cchar *menu) + + This function allows an application to offer a customizable menu which a + user can populate with frequently used (menu) functions, arranged as desired. + A menu entry selected by the user is passed to the application for execution. + + The initial popup window is blank. Right click an empty space on the popup + window to define a new menu entry. Right click an existing entry to modify it. + Use the resulting dialog to define or change the menu entry. + menu text optional text appearing in the popup window + menu func text that is passed to the application callback function + menu icon optional menu icon: /directory.../filename.png + icon size rendered icon size in the popup window, 24x24 to 64x64 pixels + close checkbox: option to close the popup window when menu is used + + Left drag a menu entry to move it somewhere else on the popup window. + The popup window can be resized to fit the contained menu entries. + Left click a menu entry to select the menu. + The callback function will be called to execute the menu function. + If "close" was checked, the popup window will close. + + All menu settings are saved in the supplied configuration file whenever the + popup window is closed, if any changes were made since it was opened. + Icon files are copied into the same directory as the configuration file and + these copies are used. The icon selected when the menu entry is created can + disappear without consequence. + + If the [x] window kill button is pressed, the window is closed and the + calling program is informed by passing "quit" to the callback function. + + layout of menu configuration file: + + popup xpos ypos width height popup window position and size + + posn xpos ypos ww hh menu position in popup window + menu menu text menu text on popup window + func funcname argument for user callback function + icon /.../file.png optional menu icon file + size NN optional icon size (default 24) + kill optional flag, kill window when used + + menu menu text next menu entry + ... + +***************************************************************************/ + +namespace gmenuznames +{ + #define maxME 200 // max. menu entries + #define maxText 1000 // max. text size, all menu fields + #define menuFont "sans 9" // menu font + #define iconSize 24 // menu icon size (default) + + typedef void callbackfunc(cchar *menu); // user callback function + callbackfunc *gmenuzcallback; + + char *menuconfigfile = 0; // configuration file from user + + GtkWidget *mWin, *layout; // popup and drawing windows + GtkWidget *pWin; // parent window + + int winposx=100, winposy=100, winww=400, winhh=300; // initial popup WRT parent window + + struct menuent { // menu entry on popup window + int xpos, ypos, ww, hh; // layout position, extent + char *menu; // text on window on null + char *func; // func name for user callback + char *icon; // icon file or null + GdkPixbuf *pixbuf; // icon pixbuf or null + int size; // icon size or zero + int kill; // kill popup window when menu used + }; + menuent menus[200]; // menu entries + int NME = 0; // entry count + + zdialog *zdedit = 0; // active edit dialog + int mpx, mpy; // mouse click/drag position + int me; // current menu entry + int Fchanged = 0; // flag, menu edited or resized + int Fquit = 0; // popup is being closed + int Fbusy = 0; // popup is active + + void mwpaint(GtkWidget *, cairo_t *); // window repaint - draw event + void resize(); // window resize event + void quit(); // kill window and exit + void update_configfile(); // update menu configuration file + void mouse_event(GtkWidget *, GdkEventButton *, void *); // mouse event function + void KB_event(GtkWidget *, GdkEventKey *, void *); // KB event function + void draw_text(cairo_t *, char *, int px, int py, int &ww, int &hh); // draw text and return pixel extent + void edit_menu(); // dialog to create/edit menu entry + int edit_menu_event(zdialog *zd, cchar *event); // dialog event function +} + + +// user callable function + +void gmenuz(GtkWidget *parent, cchar *ufile, gmenuznames::callbackfunc ufunc) +{ + using namespace gmenuznames; + + FILE *fid; + int nn, xx, yy, ww, hh, size; + int pposx, pposy; + int xpos, ypos; + char *pp, buff[maxText]; + GdkPixbuf *pixbuf; + GError *gerror; + + if (Fbusy) return; // don't allow multiple popups + + pWin = parent; // get parent window + + if (menuconfigfile) zfree(menuconfigfile); + menuconfigfile = strdupz(ufile,0,"gmenuz"); // get menu configuration file + + gmenuzcallback = ufunc; // get user callback function + + NME = 0; + + fid = fopen(menuconfigfile,"r"); // read window geometry + if (fid) + { + nn = fscanf(fid," popup %d %d %d %d",&xx,&yy,&ww,&hh); // get popup window position and size + if (nn == 4 && ww > 50 && ww < 1000 && hh > 50 && hh < 1000) { + winposx = xx; // OK to use + winposy = yy; + winww = ww; + winhh = hh; + } + + while (true) + { + pp = fgets_trim(buff,maxText-1,fid,1); // read next menu entry + if (! pp) break; + + if (strnEqu(pp,"posn ",5)) { // position in popup window + if (NME == maxME) { + zmessageACK(mWin,0,"exceeded %d menu entries",maxME); + break; + } + + me = NME; // new entry + NME++; // entry count + memset(&menus[me],0,sizeof(menuent)); // clear all menu data + + nn = sscanf(pp+5," %d %d ",&xpos,&ypos); // position in popup window + if (nn != 2) xpos = ypos = 100; + if (xpos > 1000) xpos = 1000; + if (ypos > 1000) ypos = 1000; + menus[me].xpos = xpos; + menus[me].ypos = ypos; + } + + if (strnEqu(pp,"menu ",5)) { // menu text + if (strlen(pp+5) > 0) + menus[me].menu = strdupz(pp+5,0,"gmenuz"); // get menu text + else menus[me].menu = 0; + } + + if (strnEqu(pp,"func ",5)) { // function name + if (strlen(pp+5)) menus[me].func = strdupz(pp+5,0,"gmenuz"); + else menus[me].func = 0; + } + + if (strnEqu(pp,"icon ",5)) { // menu icon file + if (strlen(pp+5)) { + menus[me].icon = strdupz(pp+5,0,"gmenuz"); + gerror = 0; + pixbuf = gdk_pixbuf_new_from_file(pp+5,&gerror); + if (! pixbuf && gerror) printf("%s \n",gerror->message); + menus[me].pixbuf = pixbuf; + } + else menus[me].pixbuf = 0; + } + + if (strnEqu(pp,"size ",5)) { + size = atoi(pp+5); + if (size < 24) size = 24; + if (size > 64) size = 64; + menus[me].size = size; + } + + if (strnEqu(pp,"kill",4)) // kill window flag + menus[me].kill = 1; + } + + fclose(fid); + } + + Fchanged = 0; // no changes yet + Fquit = 0; // not being closed + Fbusy = 1; + + mWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // create popup window for menu entries + + if (! pWin) pposx = pposy = 0; // no parent window + else { + gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (NW corner) + gtk_window_set_transient_for(GTK_WINDOW(mWin),GTK_WINDOW(pWin)); // popup window belongs to parent + } + + winposx += pposx; // popup position relative to parent + winposy += pposy; + + gtk_window_set_default_size(GTK_WINDOW(mWin),winww,winhh); // set size and position + gtk_window_move(GTK_WINDOW(mWin),winposx,winposy); + + layout = gtk_layout_new(0,0); // create drawing window + gtk_container_add(GTK_CONTAINER(mWin),layout); // add to popup window + + G_SIGNAL(mWin,"destroy",quit,0); // connect signals to windows + G_SIGNAL(mWin,"delete_event",quit,0); + G_SIGNAL(mWin,"notify",resize,0); + G_SIGNAL(mWin,"key-release-event",KB_event,0); // connect KB key release event + G_SIGNAL(layout,"draw",mwpaint,0); + + gtk_widget_add_events(layout,GDK_BUTTON_PRESS_MASK); // connect mouse events + gtk_widget_add_events(layout,GDK_BUTTON_RELEASE_MASK); + gtk_widget_add_events(layout,GDK_BUTTON_MOTION_MASK); + gtk_widget_add_events(layout,GDK_POINTER_MOTION_MASK); + G_SIGNAL(layout,"button-press-event",mouse_event,0); + G_SIGNAL(layout,"button-release-event",mouse_event,0); + G_SIGNAL(layout,"motion-notify-event",mouse_event,0); + + gtk_widget_show_all(mWin); // show all widgets + return; +} + + +// paint window when created, exposed, resized + +void gmenuznames::mwpaint(GtkWidget *, cairo_t *cr) +{ + using namespace gmenuznames; + + GdkPixbuf *pixbuf; + char *text, *text2; + int xpos, ypos, ww, hh, size, yadd; + + for (int me = 0; me < NME; me++) // loop all menu entries + { + xpos = menus[me].xpos; // window position + ypos = menus[me].ypos; + text = menus[me].menu; // menu text + pixbuf = menus[me].pixbuf; // icon + size = menus[me].size; // size + + if (pixbuf) { // draw icon at window position + gdk_cairo_set_source_pixbuf(cr,pixbuf,xpos,ypos); + cairo_paint(cr); + if (! size) size = iconSize; // use default if not specified + } + else size = 0; + + yadd = 0; + if (pixbuf) yadd = size + 2; // extra space under icon + if (text) { + text2 = zmalloc(strlen(text)+2); // replace "\n" with newline + repl_1str(text,text2,"\\n","\n"); + draw_text(cr,text2,xpos,ypos+yadd,ww,hh); // returns size of text written + zfree(text2); + } + else ww = hh = 0; + + if (ww < size) ww = size; // menu entry enclosing rectangle + hh += yadd; + menus[me].ww = ww; + menus[me].hh = hh; + } + + return; +} + + +// resize event - save current window size + +void gmenuznames::resize() +{ + using namespace gmenuznames; + + int xx, yy, ww, hh; + + if (Fquit) return; // ignore bogus call when killed + + gtk_window_get_position(GTK_WINDOW(mWin),&xx,&yy); + gtk_window_get_size(GTK_WINDOW(mWin),&ww,&hh); + + if (xx == winposx && yy == winposy && + ww == winww && hh == winhh) return; // no change + + winposx = xx; + winposy = yy; + winww = ww; + winhh = hh; + + Fchanged = 1; // mark window changed + return; +} + + +// [x] kill window +// Save current menu status for next session. + +void gmenuznames::quit() +{ + using namespace gmenuznames; + + Fquit = 1; + Fbusy = 0; + if (Fchanged) update_configfile(); + gtk_widget_destroy(mWin); + gmenuzcallback("quit"); // inform host program v.5.4 + return; +} + + +// menu changed, save all menu data to menu file + +void gmenuznames::update_configfile() +{ + using namespace gmenuznames; + + char *pp, *pxbfile; + int pposx, pposy; + FILE *fid; + GError *gerror; + + if (pWin) + gtk_window_get_position(GTK_WINDOW(pWin),&pposx,&pposy); // parent window position (may have moved) + else pposx = pposy = 0; + + winposx -= pposx; // popup position relative to parent + winposy -= pposy; + + fid = fopen(menuconfigfile,"w"); // open for write + if (! fid) { + zmessageACK(mWin,0,"error: %s",strerror(errno)); + return; + } + + fprintf(fid,"popup %d %d %d %d \n",winposx,winposy,winww,winhh); + + for (me = 0; me < NME; me++) // write all menu entries to file + { + if (! menus[me].menu && ! menus[me].pixbuf) { // no text and no icon + printf("gmenuz: skip empty menu entry \n"); + continue; + } + + fprintf(fid,"\n"); // blank line separator + + fprintf(fid,"posn %d %d \n",menus[me].xpos, menus[me].ypos); // menu position in window + + if (menus[me].menu) // menu text + fprintf(fid,"menu %s \n",menus[me].menu); + + if (menus[me].func) // menu function (text) + fprintf(fid,"func %s \n",menus[me].func); + + if (menus[me].pixbuf) { // pixbuf image for menu icon + pxbfile = strdupz(menuconfigfile,20,"gmenuz"); + pp = pxbfile + strlen(pxbfile); // create a local PNG file for pixbuf + snprintf(pp,20,"-pixbuf-%03d.png",me); + gerror = 0; + gdk_pixbuf_save(menus[me].pixbuf,pxbfile,"png",&gerror,null); // write pixbuf to file + if (gerror) printf("%s \n %s \n",menus[me].menu,gerror->message); + else fprintf(fid,"icon %s \n",pxbfile); // pixbuf file name in menu file + zfree(pxbfile); + } + + if (menus[me].size) // icon size + fprintf(fid,"size %d \n",menus[me].size); + + if (menus[me].kill) fprintf(fid,"kill \n"); // kill window flag + } + + fclose(fid); + + Fchanged = 0; + return; +} + + +// mouse event function - capture buttons and drag movements + +void gmenuznames::mouse_event(GtkWidget *, GdkEventButton *event, void *) +{ + using namespace gmenuznames; + + static int bdtime = 0, butime = 0; + static int Lmouse = 0, Rmouse = 0, Fdrag = 0; + static int elapsed, mpx0 = 0, mpy0 = 0; + int Fclick, dx, dy, xpos, ypos; + + mpx = int(event->x); // mouse position in window + mpy = int(event->y); + + if (event->type == GDK_BUTTON_PRESS) + { + Lmouse = Rmouse = Fdrag = 0; + if (event->button == 1) Lmouse++; // left or right mouse button + if (event->button == 3) Rmouse++; + bdtime = event->time; + + for (me = 0; me < NME; me++) // look for clicked menu entry + { + if (mpx < menus[me].xpos) continue; + if (mpy < menus[me].ypos) continue; + if (mpx > menus[me].xpos + menus[me].ww) continue; + if (mpy > menus[me].ypos + menus[me].hh) continue; + break; + } + + if (me < NME) { // menu item clicked (selected) + mpx0 = mpx; // set new drag origin + mpy0 = mpy; + } + else me = -1; // indicate empty space clicked + } + + if (event->type == GDK_BUTTON_RELEASE) + { + Fclick = 0; + butime = event->time; + elapsed = butime - bdtime; // button down time, milliseconds + if (elapsed < 500 && ! Fdrag) Fclick = 1; // mouse clicked + + if (me >= 0 && Fclick && Lmouse) { // menu entry was left-clicked + if (menus[me].func) + gmenuzcallback(menus[me].func); // caller user function(func) + if (menus[me].kill) quit(); // close menu after app launch + } + + else if (Fclick && Rmouse) // menu entry or empty space right-clicked + edit_menu(); // edit menu + + else if (me >= 0 && Fdrag) { // menu entry drag ended + xpos = menus[me].xpos; // align to 16-pixel raster + ypos = menus[me].ypos; + xpos = 16 * ((xpos + 8) / 16) + 4; // min. 4 pixels from edge + ypos = 16 * ((ypos + 8) / 16) + 4; + menus[me].xpos = xpos; + menus[me].ypos = ypos; + gtk_widget_queue_draw(layout); // repaint window + Fdrag = 0; + Fchanged = 1; // mark menu revised + } + + Lmouse = Rmouse = 0; // mouse click action completed + } + + if (event->type == GDK_MOTION_NOTIFY) // mouse movement + { + if (me >= 0 && Lmouse) { // menu drag underway + dx = mpx - mpx0; + dy = mpy - mpy0; + if (Fdrag || (abs(dx) + abs(dy) > 15)) { // ignore small drags v.5.5 + Fdrag++; + mpx0 = mpx; // set new drag origin + mpy0 = mpy; + menus[me].xpos += dx; // add motion to image position + menus[me].ypos += dy; + gtk_widget_queue_draw(layout); // repaint window + } + } + } + + return; +} + + +// KB event function - send KB F1 key to main app + +void gmenuznames::KB_event(GtkWidget *, GdkEventKey *kbevent, void *) +{ + int KBkey = kbevent->keyval; + if (KBkey == GDK_KEY_F1) KBstate(KBkey,0); // v.5.5 + return; +} + + +// draw text into layout and return pixel dimensions of enclosing rectangle + +void gmenuznames::draw_text(cairo_t *cr, char *text, int x, int y, int &w, int &h) +{ + using namespace gmenuznames; + + static PangoFontDescription *pfont = 0; + static PangoLayout *playout = 0; + + if (! pfont) { + pfont = pango_font_description_from_string(menuFont); // first call, get font sizing poop + playout = gtk_widget_create_pango_layout(layout,0); + pango_layout_set_font_description(playout,pfont); + } + + pango_layout_set_text(playout,text,-1); // compute layout + pango_layout_get_pixel_size(playout,&w,&h); // pixel width and height of layout + + cairo_move_to(cr,x,y); // draw layout with text + cairo_set_source_rgb(cr,0,0,0); + pango_cairo_show_layout(cr,playout); + + return; +} + + +// dialog to create a new menu entry from user inputs + +void gmenuznames::edit_menu() +{ + using namespace gmenuznames; + + if (me < 0) { // new menu entry + if (NME == maxME) { + zmessageACK(mWin,0,"capacity limit exceeded"); + return; + } + me = NME; + memset(&menus[me],0,sizeof(menuent)); // clear all data + } + + if (! zdedit) // create dialog if not already + { + zdedit = zdialog_new("edit menu entry",mWin,"apply","delete","cancel",0); + zdialog_add_widget(zdedit,"hbox","hb1","dialog",0,"space=6"); + zdialog_add_widget(zdedit,"vbox","vb1","hb1",0,"homog"); + zdialog_add_widget(zdedit,"vbox","vb2","hb1",0,"homog|expand"); // menu text [_______________] + zdialog_add_widget(zdedit,"label","lab11","vb1","menu text"); // menu func [_______________] + zdialog_add_widget(zdedit,"label","lab12","vb1","menu func"); // menu icon [_______________] [browse] + zdialog_add_widget(zdedit,"label","lab13","vb1","menu icon"); // icon size [___|+-] close window [x] + zdialog_add_widget(zdedit,"label","lab14","vb1","icon size"); // + zdialog_add_widget(zdedit,"entry","text","vb2",0,"scc=40"); // [apply] [delete] [cancel] + zdialog_add_widget(zdedit,"entry","func","vb2",0,"scc=40"); + zdialog_add_widget(zdedit,"hbox","hb2","vb2",0,"expand"); + zdialog_add_widget(zdedit,"entry","icon","hb2",0,"expand"); + zdialog_add_widget(zdedit,"button","browse","hb2","browse","space=5"); + zdialog_add_widget(zdedit,"hbox","hb3","vb2"); + zdialog_add_widget(zdedit,"spin","size","hb3","24|64|1|24"); + zdialog_add_widget(zdedit,"check","kill","hb3","close window","space=30"); + zdialog_run(zdedit,edit_menu_event); + } + + if (menus[me].menu) // stuff menu text into dialog + zdialog_stuff(zdedit,"text",menus[me].menu); + else zdialog_stuff(zdedit,"text",""); + + if (menus[me].func) // stuff menu function + zdialog_stuff(zdedit,"func",menus[me].func); + else zdialog_stuff(zdedit,"func",""); + + if (menus[me].icon) // stuff icon file + zdialog_stuff(zdedit,"icon",menus[me].icon); + else zdialog_stuff(zdedit,"icon",""); + + if (menus[me].size) // stuff icon size + zdialog_stuff(zdedit,"size",menus[me].size); + + if (menus[me].kill) // stuff window kill flag + zdialog_stuff(zdedit,"kill",1); + else zdialog_stuff(zdedit,"kill",0); + + if (me == NME) { // new menu entry + menus[me].xpos = mpx; // initial position from mouse + menus[me].ypos = mpy; + } + + return; +} + + +// menu entry dialog event function + +int gmenuznames::edit_menu_event(zdialog *zd, cchar *event) +{ + using namespace gmenuznames; + + char text[maxText]; + char *iconfile; + int size; + GdkPixbuf *pixbuf; + GError *gerror; + + if (strEqu(event,"browse")) { // browse for icon file + iconfile = zgetfile1("select icon","open",0); + if (iconfile) zdialog_stuff(zd,"icon",iconfile); + } + + if (zd->zstat) // dialog complete + { + if (zd->zstat == 2) { // [delete] - delete menu entry + if (me < 0 || me >= NME) return 0; + for (int me2 = me; me2 < NME-1; me2++) // remove menu entry and close hole + menus[me2] = menus[me2+1]; + NME--; + Fchanged = 1; // mark menu revised + gtk_widget_queue_draw(layout); // repaint window + } + + if (zd->zstat != 1) { // not [apply] - kill dialog + zdialog_free(zdedit); + return 0; + } + // [apply] - update menu from dialog data + zdialog_fetch(zd,"text",text,maxText); + if (*text) menus[me].menu = strdupz(text,0,"gmenuz"); // menu text, optional + else menus[me].menu = 0; + + zdialog_fetch(zd,"func",text,maxText); // menu function name + strTrim2(text); + if (*text) menus[me].func = strdupz(text,0,"gmenuz"); + else menus[me].func = 0; + + zdialog_fetch(zd,"icon",text,maxText); // menu icon file, optional + strTrim2(text); + if (*text) { + zdialog_fetch(zd,"size",size); // icon size + gerror = 0; + pixbuf = gdk_pixbuf_new_from_file_at_size(text,size,size,&gerror); + if (! pixbuf) { + if (gerror) zmessageACK(mWin,0,gerror->message); // bad icon file + zd->zstat = 0; // keep dialog open + return 0; // do nothing + } + menus[me].icon = strdupz(text,0,"gmenuz"); + menus[me].pixbuf = pixbuf; + menus[me].size = size; + } + else { + menus[me].icon = 0; + menus[me].pixbuf = 0; + menus[me].size = 0; + } + + zdialog_fetch(zd,"kill",menus[me].kill); // popup window kill flag + + if (me == NME) NME++; // if new menu entry, incr. count + + Fchanged = 1; // mark menu revised + + zdialog_free(zdedit); // destroy dialog + gtk_widget_queue_draw(layout); // repaint window + } + + return 0; +} + + +/************************************************************************** + Vertical Menu / Toolbar + + Build a custom vertical menu and/or toolbar in a vertical packing box + + vbm = vbox_menu_new(GtkWidget *vbox) create base menu + + vbox_menu_add(vbm, name, icon, desc, func, arg) add menu item or toolbar button + vbox_menu *vbm + cchar *name, *icon, *desc, *arg + void func(GtkWidget *, cchar *name) + + Create a vertical menu / toolbar in a vertical packing box. + Added items can have a menu name, icon, description, response function, + and function argument. 'name' and 'icon' can be null but not both. + 'icon' is a filespec for a .png file containing the icon. + 'desc' is optional and is used as a tool tip if the mouse is hovered + over the displayed 'name/icon'. 'arg' may be null. + When 'name/icon' is clicked, 'func' is called with 'arg'. + + To create a menu entry that is a popup menu with multiple entries, do as follows: + popup = create_popmenu(); + add_popup_menu_item(popup ...); // see create_popmenu() + add_popup_menu_item(popup ...); + ... + vbox_menu_add(vbm,name,icon,desc,create_popmenu,(cchar *) popup); + + i.e. use create_popmenu() as the response function and use the previously + created menu 'popup' as the argument (cast to cchar *). + +***/ + +// build a text and icon based menu in a vertical packing box // v.5.5 + +namespace vbox_menunames +{ + #define menufont "sans 10" // menu font + #define fontheight 18 // text height in layout + #define margin 5 // margins for menu text + #define iconsize 24 // menu icon size + + PangoFontDescription *pfont = 0; + PangoLayout *playout = 0; + + void mwpaint(GtkWidget *, cairo_t *, vbox_menu *); // window repaint - draw event + void draw_text(cairo_t *, cchar *, int px, int py, int &ww, int &hh); // draw text in window, get space used + void mouse_event(GtkWidget *, GdkEventButton *, vbox_menu *); // mouse event function +} + + +// user callable function to add menu entries to a vertical packing box +// vbox parent widget, vertical packing box +// name menu text name +// icon menu icon ("iconfile.png" in /usr/share/appname/icons/) +// desc menu short description for mouse hover popup (or null) +// func user callback function (GtkWidget *, cchar *arg) +// arg argument passed to func(arg) +// +// supply one or both of name and icon, if omitted use null + +vbox_menu *vbox_menu_new(GtkWidget *vbox) +{ + using namespace vbox_menunames; + + int cc = sizeof(vbox_menu); + vbox_menu *vbm = (vbox_menu *) zmalloc(cc,"vbox_menu"); + memset(vbm,0,cc); + vbm->vbox = vbox; + vbm->layout = gtk_layout_new(0,0); + vbm->mcount = 0; + gtk_box_pack_start(GTK_BOX(vbox),vbm->layout,1,1,0); + + if (! pfont) { + pfont = pango_font_description_from_string(menufont); + playout = gtk_widget_create_pango_layout(vbm->layout,0); + pango_layout_set_font_description(playout,pfont); + } + + gtk_widget_add_events(vbm->layout,GDK_BUTTON_PRESS_MASK); + gtk_widget_add_events(vbm->layout,GDK_BUTTON_RELEASE_MASK); + gtk_widget_add_events(vbm->layout,GDK_POINTER_MOTION_MASK); + gtk_widget_add_events(vbm->layout,GDK_LEAVE_NOTIFY_MASK); + G_SIGNAL(vbm->layout,"button-press-event",mouse_event,vbm); + G_SIGNAL(vbm->layout,"button-release-event",mouse_event,vbm); + G_SIGNAL(vbm->layout,"motion-notify-event",mouse_event,vbm); + G_SIGNAL(vbm->layout,"leave-notify-event",mouse_event,vbm); + G_SIGNAL(vbm->layout,"draw",mwpaint,vbm); + return vbm; +} + + +void vbox_menu_add(vbox_menu *vbm, cchar *name, cchar *icon, cchar *desc, mtFunc func, cchar *arg) +{ + using namespace vbox_menunames; + + int me; + uint ii, ww, hh, rs, nc, px, py; + char iconpath[200]; + GdkPixbuf *pixbuf1, *pixbuf2; + GError *gerror = 0; + uint8 *pixels; + + me = vbm->mcount++; // track no. menu entries + + if (name) vbm->menu[me].name = strdupz(name,0,"vbox_menu"); // create new menu entry from caller data + if (icon) vbm->menu[me].icon = strdupz(icon,0,"vbox_menu"); + if (desc) vbm->menu[me].desc = strdupz(desc,0,"vbox_menu"); + vbm->menu[me].pixbuf1 = 0; + vbm->menu[me].pixbuf2 = 0; + vbm->menu[me].func = func; + vbm->menu[me].arg = name; // argument is menu name or arg if avail. + if (arg) vbm->menu[me].arg = arg; + + if (icon) { // if icon is named, get pixbuf + *iconpath = 0; + strncatv(iconpath,199,zfuncs::zicondir,"/",icon,null); + pixbuf1 = gdk_pixbuf_new_from_file_at_scale(iconpath,iconsize,iconsize,1,&gerror); + if (pixbuf1) { + pixbuf2 = gdk_pixbuf_copy(pixbuf1); + ww = gdk_pixbuf_get_width(pixbuf2); // make a false color pixbuf + hh = gdk_pixbuf_get_height(pixbuf2); // for the "clicked" state + rs = gdk_pixbuf_get_rowstride(pixbuf2); + nc = gdk_pixbuf_get_n_channels(pixbuf2); + pixels = gdk_pixbuf_get_pixels(pixbuf2); + for (py = 0; py < hh; py++) + for (px = 0; px < ww; px++) { + ii = py * rs + px * nc; + pixels[ii] += (255 - pixels[ii]) / 2; + pixels[ii+1] += (255 - pixels[ii+1]) / 2; + pixels[ii+2] += (255 - pixels[ii+2]) / 2; + } + vbm->menu[me].pixbuf1 = pixbuf1; + vbm->menu[me].pixbuf2 = pixbuf2; + } + } + + if (me == 0) vbm->menu[me].ypos = margin; // first menu, top position + else vbm->menu[me].ypos = vbm->menu[me-1].ypos + vbm->menu[me-1].yhh; // use next position + + if (name) vbm->menu[me].yhh = fontheight; // space if only menu name is used + if (icon) vbm->menu[me].yhh = iconsize + iconsize / 8; // space if icon is used + + return; +} + + +// paint window when created, exposed, resized + +void vbox_menunames::mwpaint(GtkWidget *widget, cairo_t *cr, vbox_menu *vbm) +{ + using namespace vbox_menunames; + + GdkPixbuf *pixbuf; + cchar *text; + int me, xpos, ypos; + int ww, hh, maxww = 2 * margin + iconsize; + + for (me = 0; me < vbm->mcount; me++) // loop all menu entries + { + ypos = vbm->menu[me].ypos; // menu position + text = vbm->menu[me].name; // menu text + pixbuf = vbm->menu[me].pixbuf1; // icon + + if (pixbuf) { // draw menu icon at menu position + gdk_cairo_set_source_pixbuf(cr,pixbuf,margin,ypos); + cairo_paint(cr); + } + + if (text) { // draw menu text at menu position + if (pixbuf) { + xpos = margin + iconsize + margin; + ypos += (iconsize - fontheight) / 2 + 2; + } + else xpos = margin; + draw_text(cr,text,xpos,ypos,ww,hh); + if (xpos + ww > maxww) maxww = xpos + ww; + } + } + + gtk_widget_set_size_request(vbm->layout,maxww+margin,0); + return; +} + + +// draw text into layout at designated position and return space required + +void vbox_menunames::draw_text(cairo_t *cr, cchar *text, int posx, int posy, int &ww, int &hh) +{ + using namespace vbox_menunames; + + pango_layout_set_text(playout,text,-1); // compute layout + pango_layout_get_pixel_size(playout,&ww,&hh); // pixel width and height of layout + + cairo_move_to(cr,posx,posy); // draw layout with text + cairo_set_source_rgb(cr,0,0,0); + pango_cairo_show_layout(cr,playout); + + return; +} + + +// mouse event function - capture buttons and drag movements + +void vbox_menunames::mouse_event(GtkWidget *widget, GdkEventButton *event, vbox_menu *vbm) +{ + using namespace vbox_menunames; + + GdkDevice *mouse; + GdkPixbuf *pixbuf; + GdkWindow *gdkwin; + int me, mpy, ypos; + static int me0 = -1, metip = 0; + cairo_t *cr; + + gdkwin = gtk_layout_get_bin_window(GTK_LAYOUT(widget)); + + mpy = int(event->y); + mouse = event->device; + + if (event->type == GDK_MOTION_NOTIFY) // show tooltip when entering menu + { + for (me = 0; me < vbm->mcount; me++) { // find menu where mouse is + if (mpy <= vbm->menu[me].ypos) continue; + if (mpy < vbm->menu[me].ypos + vbm->menu[me].yhh) break; + } + + if (me != me0 && metip) { // menu changed, remove tooltip + poptext_mouse(mouse,0,0,0); + metip = 0; + } + + if (me == vbm->mcount) return; // no match + + me0 = me; + if (! metip && vbm->menu[me].desc) { // set tooltip for menu + poptext_mouse(mouse,vbm->menu[me].desc,30,20,5); // v.5.5 + metip = 1; + } + + return; + } + + if (metip) { // reset tooltip + poptext_mouse(mouse,0,0,0); + metip = 0; + me0 = -1; + } + + if (event->type == GDK_BUTTON_PRESS) + { + if (event->button != 1) return; -GtkWidget * create_stbar(GtkWidget *pbox) -{ - GtkWidget *stbar; - static PangoFontDescription *fontdesc; + for (me = 0; me < vbm->mcount; me++) { // look for clicked menu entry + if (mpy <= vbm->menu[me].ypos) continue; + if (mpy < vbm->menu[me].ypos + vbm->menu[me].yhh) break; + } - stbar = gtk_statusbar_new(); - fontdesc = pango_font_description_from_string("Monospace 9"); - gtk_widget_modify_font(stbar,fontdesc); // *** GTK does not work *** - gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0); - gtk_widget_show(stbar); - return stbar; -} + if (me < vbm->mcount) { + pixbuf = vbm->menu[me].pixbuf2; // paint reversed color icon + if (pixbuf) { + ypos = vbm->menu[me].ypos; + cr = gdk_cairo_create(gdkwin); + gdk_cairo_set_source_pixbuf(cr,pixbuf,margin,ypos); + cairo_paint(cr); + cairo_destroy(cr); + } + } + } + if (event->type == GDK_BUTTON_RELEASE) + { + if (event->button != 1) return; // refresh menu entries -// display message in status bar + for (me = 0; me < vbm->mcount; me++) { // look for clicked menu entry + if (mpy <= vbm->menu[me].ypos) continue; + if (mpy < vbm->menu[me].ypos + vbm->menu[me].yhh) break; + } -int stbar_message(GtkWidget *wstbar, cchar *message) -{ - static int ctx = -1; + if (me < vbm->mcount) { + pixbuf = vbm->menu[me].pixbuf2; // repaint original icons + if (pixbuf) gtk_widget_queue_draw(widget); // (0.03 secs. for 11 text + 13 icons) + if (vbm->menu[me].arg) + (vbm->menu[me].func)(widget,vbm->menu[me].arg); // do caller function(widget,arg) + else (vbm->menu[me].func)(widget,vbm->menu[me].name); + } + } - if (ctx == -1) ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all"); - gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx); - gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message); - return 0; + return; } /************************************************************************** simplified GTK dialog functions -***************************************************************************/ + create a dialog with response buttons (OK, cancel ...) + add widgets to dialog (button, text entry, combo box ...) + put data into widgets (text or numeric data) + run the dialog, get user inputs (button press, text entry, checkbox selection ...) + run caller event function when widgets change from user inputs + run caller event function when dialog is completed or canceled + get dialog completion status (OK, cancel, destroyed ...) + get data from dialog widgets (the user inputs) + destroy the dialog and free memory + +***************************************************************************/ // counter, keeps track of open or pending zdialogs -namespace zfuncs { int zdialog_busy; } +namespace zfuncs { + int zdialog_busy; +} // private functions for widget events and dialog completion int zdialog_widget_event(GtkWidget *, zdialog *zd); -int zdialog_response_event(GtkWidget *, int zstat, zdialog *zd); +int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd); // create a new zdialog dialog +// The title and parent arguments may be null. // optional arguments: up to zdmaxbutts button labels followed by null // returned dialog status: +N = button N (1 to zdmaxbutts) // <0 = [x] button or other GTK destroy action - -// build own completion buttons, replacing the fat GTK standard buttons // completion buttons are also events like other widgets -// get rid of separate dialog completion function // all dialogs run parallel, use zdialog_wait() if needed +// The status returned by zdialog_wait() corresponds to the button +// (1-10) used to end the dialog. Status < 0 indicates the [x] button +// was used or the dialog was killed by the program itself. zdialog * zdialog_new(cchar *title, GtkWidget *parent, ...) // parent added { zdialog *zd; - GtkWidget *dialog, *hbox, *vbox, *butt, *hsep; + GtkWidget *topwin, *hbox, *vbox, *butt, *hsep; cchar *bulab[zdmaxbutts]; int cc, ii, nbu; va_list arglist; + zthreadcrash(); // thread usage not allowed + va_start(arglist,parent); for (nbu = 0; nbu < zdmaxbutts; nbu++) // get completion buttons { @@ -4062,23 +5944,22 @@ if (! title) title = " "; // v.5.0 - dialog = gtk_dialog_new(); // attributes optional - gtk_window_set_title(GTK_WINDOW(dialog),title); - vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); - gtk_box_set_spacing(GTK_BOX(vbox),2); // separator removed + topwin = gtk_window_new(GTK_WINDOW_TOPLEVEL); // use window, not dialog v.5.5 + gtk_window_set_title(GTK_WINDOW(topwin),title); + vbox = gtk_box_new(VERTICAL,0); // vertical packing box + gtk_container_add(GTK_CONTAINER(topwin),vbox); // add to main window + gtk_box_set_spacing(GTK_BOX(vbox),2); cc = sizeof(zdialog); // allocate zdialog zd = (zdialog *) zmalloc(cc,"zdialog"); - if (parent) { - zd->parent = parent; - gtk_window_set_transient_for(GTK_WINDOW(dialog),GTK_WINDOW(parent)); - } + if (parent) + gtk_window_set_transient_for(GTK_WINDOW(topwin),GTK_WINDOW(parent)); if (nbu) { // there are some completion buttons - hbox = gtk_hbox_new(0,0); - gtk_box_pack_end(GTK_BOX(vbox),hbox,0,0,0); // add hbox for buttons at bottom - hsep = gtk_hseparator_new(); // add separator line + hbox = gtk_box_new(HORIZONTAL,5); + gtk_box_pack_end(GTK_BOX(vbox),hbox,0,0,5); // add hbox for buttons at bottom + hsep = gtk_separator_new(HORIZONTAL); // add separator line gtk_box_pack_end(GTK_BOX(vbox),hsep,0,0,5); for (ii = nbu-1; ii >= 0; ii--) { // add buttons to hbox @@ -4089,19 +5970,19 @@ } } + zd->parent = parent; // parent window or null zd->sentinel = zdsentinel; // validity sentinel zd->eventCB = 0; // no user event callback function zd->zstat = 0; // no zdialog status zd->disabled = 1; // widget signals disabled zd->saveposn = 0; // position not saved - zd->help_topic = 0; // no help topic zd->widget[0].type = "dialog"; // set up 1st widget = dialog zd->widget[0].name = "dialog"; zd->widget[0].pname = 0; zd->widget[0].data = strdupz(title,0,"zdialog"); zd->widget[0].cblist = 0; - zd->widget[0].widget = dialog; + zd->widget[0].widget = topwin; zd->widget[1].type = 0; // eof - no contained widgets yet return zd; @@ -4109,6 +5990,28 @@ // add widget to existing zdialog +// +// Arguments after parent are optional and default to 0. +// zd zdialog *, created with zdialog_new() +// type string, one of the above widget types +// name string, widget name, used to stuff or fetch widget data +// parent string, parent name: "dialog" or a previously added container widget +// data string, initial data for widget (label name, entry string, spin value, etc.) +// options string, optional args in the form "scc=nn|homog|expand|space=nn|wrap" +// scc width for text entry widget, characters +// homog for hbox or vbox to make even space allocation for contained widgets +// expand widget should expand with dialog box expansion +// space extra space between this widget and neighbors, pixels +// wrap allow text to wrap at right margin +// +// data can be a string ("initial widget data") or a number in string form ("12.345") +// data for togbutt / check / radio: use "0" or "1" for OFF or ON +// data for spin / hscale / vscale: use format "min|max|step|value" (default: 0 | 100 | 1 | 50) +// data for colorbutt: use "rrr|ggg|bbb" with values 0-255 for each RGB color. +// This format is written to initialize the control, and this format is read back +// after the user selects a color. +// +// Multiple radio buttons with same parent are a group: pressing one turns the others OFF. int zdialog_add_widget ( zdialog *zd, cchar *type, cchar *name, cchar *pname, // mandatory args @@ -4125,6 +6028,8 @@ static PangoFontDescription *monofont = 0; + if (! monofont) monofont = pango_font_description_from_string("Monospace"); + if (! zd) zappcrash("zdialog null pointer"); // detect destroyed dialog if (zd->sentinel != zdsentinel) zappcrash("zdialog invalid"); @@ -4147,8 +6052,9 @@ zd->widget[iiw+1].type = 0; // set new EOF marker if (strcmpv(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin", - "label","link","entry","edit","button","togbutt","check","combo", - "comboE","radio","spin","hscale","vscale","colorbutt", null) == 0) + "label","link","entry","edit","text","button","togbutt", + "check","combo","comboE","radio","spin","hscale", + "vscale","colorbutt", null) == 0) zappcrash("zdialog, bad widget type: %s",type); for (iip = iiw-1; iip >= 0; iip--) // find parent (container) widget @@ -4161,30 +6067,36 @@ if (strcmpv(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0) zappcrash("zdialog, bad widget parent type: %s",ptype); - if (! monofont) monofont = pango_font_description_from_string("Monospace"); + if (strEqu(type,"hbox")) widget = gtk_box_new(HORIZONTAL,space); // expandable container boxes v.5.4 + if (strEqu(type,"vbox")) widget = gtk_box_new(VERTICAL,space); + if (strstr("hbox vbox",type)) + gtk_box_set_homogeneous(GTK_BOX(widget),homog); - if (strEqu(type,"hbox")) widget = gtk_hbox_new(homog,space); // expandable container boxes - if (strEqu(type,"vbox")) widget = gtk_vbox_new(homog,space); - - if (strEqu(type,"hsep")) widget = gtk_hseparator_new(); // horiz. & vert. separators - if (strEqu(type,"vsep")) widget = gtk_vseparator_new(); + if (strEqu(type,"hsep")) widget = gtk_separator_new(HORIZONTAL); // horiz. & vert. separators + if (strEqu(type,"vsep")) widget = gtk_separator_new(VERTICAL); if (strEqu(type,"frame")) { // frame around contained widgets widget = gtk_frame_new(data); gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN); + data = 0; // v.5.3 data not further used } if (strEqu(type,"scrwin")) { // scrolled window container widget = gtk_scrolled_window_new(0,0); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget), GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC); + data = 0; // v.5.3 data not further used } - if (strEqu(type,"label")) widget = gtk_label_new(data); // label (static text) + if (strEqu(type,"label")) { // label (static text) + widget = gtk_label_new(data); + data = 0; // v.5.3 data not further used + } if (strEqu(type,"link")) { // label is clickable widget = gtk_link_button_new(data); G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); + data = 0; // v.5.3 data not further used } if (strEqu(type,"entry")) { // 1-line text entry @@ -4205,20 +6117,32 @@ G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd); // buffer signals, not widget } + if (strEqu(type,"text")) { // text output (not editable) + widget = gtk_text_view_new(); // added v.5.5 + editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); + if (data) gtk_text_buffer_set_text(editBuff,data,-1); + gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0); + if (wrap) gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_WORD); + gtk_widget_modify_font(widget,monofont); + } + if (strEqu(type,"button")) { // button widget = gtk_button_new_with_label(data); G_SIGNAL(widget,"clicked",zdialog_widget_event,zd); + data = 0; // v.5.3 data not further used } if (strEqu(type,"togbutt")) { // toggle button widget = gtk_toggle_button_new_with_label(data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); + data = "0"; // v.5.3 default data } if (strEqu(type,"check")) { // checkbox if (data) widget = gtk_check_button_new_with_label(data); else widget = gtk_check_button_new(); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); + data = "0"; // v.5.3 default data } if (strEqu(type,"combo")) { // combo box @@ -4256,6 +6180,7 @@ widget = gtk_radio_button_new_with_label_from_widget // not first, add to group (GTK_RADIO_BUTTON(zd->widget[kk].widget),data); G_SIGNAL(widget,"toggled",zdialog_widget_event,zd); + data = "0"; // v.5.3 default data } if (strcmpv(type,"spin","hscale","vscale",null)) { // spin button or sliding scale @@ -4270,12 +6195,12 @@ gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val); } if (*type == 'h') { - widget = gtk_hscale_new_with_range(min,max,step); + widget = gtk_scale_new_with_range(HORIZONTAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } if (*type == 'v') { - widget = gtk_vscale_new_with_range(min,max,step); + widget = gtk_scale_new_with_range(VERTICAL,min,max,step); gtk_range_set_value(GTK_RANGE(widget),val); gtk_scale_set_draw_value(GTK_SCALE(widget),0); } @@ -4307,7 +6232,7 @@ if (strEqu(ptype,"scrwin")) // add to scroll window gtk_container_add(GTK_CONTAINER(pwidget),widget); if (strEqu(ptype,"dialog")) { // add to dialog box - vbox = gtk_dialog_get_content_area(GTK_DIALOG(pwidget)); + vbox = gtk_bin_get_child(GTK_BIN(pwidget)); // dialog is a gtkwindow v.5.5 gtk_box_pack_start(GTK_BOX(vbox),widget,expand,expand,space); } if (data) @@ -4359,8 +6284,9 @@ // set a common group for a set of radio buttons +// (GTK, this does not work) -int zdialog_set_group(zdialog *zd, cchar *radio1, ...) // (GTK, this does not work) +int zdialog_set_group(zdialog *zd, cchar *radio1, ...) { va_list arglist; cchar *radio2; @@ -4402,6 +6328,7 @@ // put data into a zdialog widget +// private function int zdialog_put_data(zdialog *zd, cchar *name, cchar *data) { @@ -4413,15 +6340,17 @@ char *wdata; double val; + zthreadcrash(); // thread usage not allowed + if (! zd || zd->sentinel != zdsentinel) { // detect destroyed dialog - printf("zdialog_put_data(%s,%s), zdialog invalid \n",name,data); + printf("*** zdialog_put_data(%s,%s), zdialog invalid \n",name,data); return 0; } for (iiw = 1; zd->widget[iiw].type; iiw++) // find widget if (strEqu(zd->widget[iiw].name,name)) break; if (! zd->widget[iiw].type) { - printf("zdialog_put_data(%s), widget invalid \n",name); + printf("*** zdialog_put_data(%s), widget name invalid \n",name); return 0; } @@ -4436,7 +6365,7 @@ wdata = strdupz(data,0,"zdialog"); // set new data for widget zd->widget[iiw].data = wdata; if (utf8_check(wdata)) - printf("zdialog: bad UTF8 encoding %s \n",wdata); + printf("*** zdialog: bad UTF8 encoding %s \n",wdata); } zd->disabled++; // disable for widget stuffing @@ -4458,6 +6387,11 @@ gtk_text_buffer_set_text(textBuff,data,-1); } + if (strEqu(type,"text")) { // text output v.5.5 + textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); + gtk_text_buffer_set_text(textBuff,data,-1); + } + if (strcmpv(type,"togbutt","check","radio",null)) { if (! data) kk = nn = 0; @@ -4517,6 +6451,7 @@ // get data from a dialog widget based on its name +// private function cchar * zdialog_get_data(zdialog *zd, cchar *name) { @@ -4544,7 +6479,7 @@ for (iiw = 1; zd->widget[iiw].type; iiw++) if (strEqu(name,zd->widget[iiw].name)) break; if (! zd->widget[iiw].type) { - printf("zdialog_stuff_limits, %s not found \n",name); + printf("*** zdialog_stuff_limits, %s not found \n",name); return 0; } @@ -4561,18 +6496,28 @@ } -// put help topic into dialog - -void zdialog_help(zdialog *zd, cchar *help_topic) -{ - zd->help_topic = help_topic; - return; -} - - // run the dialog and send events to the event function +// // evfunc: int func(zdialog *zd, cchar *event) -// posn: optional dialog box position (see zdialog_set_position) +// If present, eventFunc is called when a dialog widget is changed or the dialog +// is completed. If a widget was changed, event is the widget name. +// Get the new widget data with zdialog_fetch(). +// If a completion button was pressed, event is "zstat" and zd->zstat will be +// the button number 1-N. +// If the dialog was destroyed, event is "zstat" and zd->zstat is negative. +// +// posn: optional dialog box position: +// "mouse" = position at mouse +// "desktop" = center on desktop +// "parent" = center on parent window +// "nn/nn" = position NW corner at relative x/y position in parent window, +// where nn/nn is a percent 0-100 of the parent window x/y dimensions. +// "save" = save last user-set position and use this whenever the dialog is repeated, +// also across sessions. +// +// KBstate: extern void KBstate(int key, int state) +// This function must be supplied by the caller of zdialog. +// It is called when the F1 key is pressed or released. int zdialog_run(zdialog *zd, zdialog_event evfunc, cchar *posn) { @@ -4588,7 +6533,7 @@ dialog = zd->widget[0].widget; - if (posn) zdialog_set_position(zd,posn); + if (posn) zdialog_set_position(zd,posn); // put dialog at remembered position for (ii = 1; zd->widget[ii].type; ii++) // *** stop auto-selection { // (GTK "feature") @@ -4615,10 +6560,10 @@ if (! zd->parent) // if no parent, force to top gtk_window_set_keep_above(GTK_WINDOW(dialog),1); - G_SIGNAL(dialog,"focus-in-event",zdialog_focus_event_signal,zd); // connect focus event function v.5.0 + G_SIGNAL(dialog,"focus-in-event",zdialog_focus_event_signal,zd); // connect focus event function v.5.0 G_SIGNAL(dialog,"key-press-event",zdialog_KBpress,zd); // connect key press event function G_SIGNAL(dialog,"key-release-event",zdialog_KBrelease,zd); // connect key release event function - G_SIGNAL(dialog,"response",zdialog_response_event,zd); // connect dialog response function + G_SIGNAL(dialog,"delete-event",zdialog_delete_event,zd); // connect delete event function v.5.5 zd->disabled = 0; // enable widget events @@ -4630,6 +6575,7 @@ // zdialog event handler - private function called for dialog events. // Updates data in zdialog, calls user callback function (if present). +// private function int zdialog_widget_event(GtkWidget *widget, zdialog *zd) { @@ -4672,7 +6618,7 @@ } } - printf("zdialog event %s, ignored \n",zd->widget[ii].name); // not found, ignore event + printf("*** zdialog event %s, ignored \n",zd->widget[ii].name); // not found, ignore event return 1; found_widget: @@ -4688,7 +6634,7 @@ if (strEqu(type,"entry")) wdata = gtk_entry_get_text(GTK_ENTRY(widget)); - + if (strEqu(type,"edit")) { gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2); wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0); @@ -4765,61 +6711,99 @@ // zdialog response handler for "focus-in-event" signal // v.5.0 +// private function int zdialog_focus_event_signal(GtkWidget *, GdkEvent *event, zdialog *zd) { if (zd->zstat) return 0; // already complete - if (zd->help_topic) - zfuncs::F1_help_topic = zd->help_topic; // set help topic zdialog_send_event(zd,"focus"); // notify dialog event function return 0; // must be 0 } // zdialog response handler for keyboard events +// key symbols can be found at /usr/include/gtk-3.0/gdk/gdkkeysyms.h +// main app must provide: extern void KBstate(int key, int state) +// private function -int zdialog_KBpress(GtkWidget *, GdkEventKey *kbevent, zdialog *zd) // prevent KB key press from being -{ // sent to toolbar buttons +int KB_Ctrl = 0; // track state of KB Ctrl +int KB_Shift = 0; // and Shift and Alt keys +int KB_Alt = 0; + +int zdialog_KBpress(GtkWidget *, GdkEventKey *kbevent, zdialog *zd) +{ int KBkey = kbevent->keyval; - if (KBkey == GDK_KEY_Control_L || KBkey == GDK_KEY_Control_R) { // Ctrl key is pressed - if (zd->parent) { - gtk_window_present(GTK_WINDOW(zd->parent)); // set KB focus to parent window v.5.2 - gtk_window_activate_focus(GTK_WINDOW(zd->parent)); - } + + if (KBkey == GDK_KEY_Control_L || KBkey == GDK_KEY_Control_R) { // send these keys to main app + KB_Ctrl = 1; + KBstate(KBkey,1); + return 1; } - return 0; // must be 0 + + if (KBkey == GDK_KEY_Shift_L || KBkey == GDK_KEY_Shift_R) { + KB_Shift = 1; + KBstate(KBkey,1); + return 1; + } + + if (KBkey == GDK_KEY_Alt_L || KBkey == GDK_KEY_Alt_R) { + KB_Alt = 1; + KBstate(KBkey,1); + return 1; + } + + if (KB_Ctrl || KB_Alt) { // if Ctrl or Alt pressed, v.5.5 + KBstate(KBkey,1); // send key to main app + return 1; + } + + return 0; // send to dialog widget } + int zdialog_KBrelease(GtkWidget *, GdkEventKey *kbevent, zdialog *zd) { - zdialog_event *evfunc = 0; // dialog event callback function - - int KBkey = kbevent->keyval; - char event[8]; + int KBkey = kbevent->keyval; - if (KBkey == GDK_KEY_F1) { - showz_userguide(zfuncs::F1_help_topic); // context help + if (KBkey == GDK_KEY_Control_L || KBkey == GDK_KEY_Control_R) { // send these keys to main app + KB_Ctrl = 0; + KBstate(KBkey,0); return 1; } - if (zd->stopKB) return 1; // ignore KB events v.5.1 + if (KBkey == GDK_KEY_Shift_L || KBkey == GDK_KEY_Shift_R) { + KB_Shift = 0; + KBstate(KBkey,0); + return 1; + } - if (zd->eventCB) { - evfunc = (zdialog_event *) zd->eventCB; // do user callback function - strcpy(event,"KB x"); // with event = "KB x" - event[3] = KBkey; // (x = key now released) - evfunc(zd,event); + if (KBkey == GDK_KEY_Alt_L || KBkey == GDK_KEY_Alt_R) { + KB_Alt = 0; + KBstate(KBkey,0); + return 1; + } + + if (KB_Ctrl || KB_Alt) { // if Ctrl or Alt pressed, + KBstate(KBkey,0); // send key to main app v.5.5 + return 1; + } + + if (KBkey == GDK_KEY_F1) { // send F1 (help) to main app v.5.5 + KBstate(KBkey,0); return 1; } - return 0; + if (zd->stopKB) return 1; // ignore KB events v.5.1 + + return 0; // send to dialog widget } -// zdialog response handler - private function called when dialog is completed. +// private function called when zdialog is completed. // called when dialog is canceled via [x] button or destroyed by GTK (zstat < 0). +// private function -int zdialog_response_event(GtkWidget *, int zstat, zdialog *zd) +int zdialog_delete_event(GtkWidget *, GdkEvent *, zdialog *zd) { zdialog_event *evfunc = 0; // dialog event callback function @@ -4827,19 +6811,19 @@ if (zd->sentinel != zdsentinel) return 1; if (zd->zstat) return 1; // already complete - if (zstat > -1) zstat = -1; // insure cancel status v.5.0 - zd->zstat = zstat; // set zdialog status + zd->zstat = -1; // set zdialog cancel status v.5.5 if (zd->eventCB) { evfunc = (zdialog_event *) zd->eventCB; // do callback function evfunc(zd,"zstat"); // (should do zfree()) } - return 1; // v.5.0 + return 0; // v.5.5 } -// send an event to an active dialog +// Send the event "name" to an active zdialog. +// The response function eventFunc() will be called with this event. int zdialog_send_event(zdialog *zd, cchar *event) { @@ -4854,8 +6838,10 @@ } -// Complete a dialog and give it a status, without user action. -// Dialog event function will be called, zdialog_wait() will return. +// Complete an active dialog and assign a status. +// Equivalent to the user pressing a dialog completion button. +// The dialog completion function is called if defined, and +// zdialog_wait() is unblocked. // returns: 0 = no active dialog, 1 = OK int zdialog_send_response(zdialog *zd, int zstat) @@ -4877,15 +6863,27 @@ // show or hide a zdialog window +// returns 1 if successful, 0 if zd does not exist. -int zdialog_show(zdialog *zd, int flag) +int zdialog_show(zdialog *zd, int show) { - GtkWidget *widget; + static GtkWidget *widget, *pwidget = 0; + static int posx, posy; widget = zdialog_widget(zd,"dialog"); if (! widget) return 0; - if (flag) gtk_widget_show_all(widget); - else gtk_widget_hide(widget); + if (show) { // show window + if (widget == pwidget) { // restore prior position v.5.4 + gtk_window_move(GTK_WINDOW(widget),posx,posy); + pwidget = 0; + } + gtk_widget_show_all(widget); + } + else { // hide window + pwidget = widget; + gtk_window_get_position(GTK_WINDOW(widget),&posx,&posy); // save position v.5.4 + gtk_widget_hide(widget); + } return 1; } @@ -4897,18 +6895,19 @@ int zdialog_destroy(zdialog *zd) { using namespace zfuncs; - + if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; + if (zd->saveposn) // save position for next use v.5.5 + zdialog_save_position(zd); + if (zd->zstat < 0) { // destroyed by [x] button or GTK zd->widget[0].widget = 0; // assume GTK dialog is gone zdialog_busy--; } if (zd->widget[0].widget) { // multiple destroys OK - if (zd->saveposn) - zdialog_save_position(zd); // save position for next use gtk_widget_destroy(zd->widget[0].widget); // destroy GTK dialog zd->widget[0].widget = 0; zdialog_busy--; @@ -4919,13 +6918,16 @@ } -// free zdialog memory (destroy first, if not already) +// free zdialog memory (will destroy first, if not already) +// zd is set to null int zdialog_free(zdialog *&zd) // reference { if (! zd) return 0; // detect destroyed dialog if (zd->sentinel != zdsentinel) return 0; + zdialog_save_inputs(zd); // save user inputs for next use v.5.3 + zdialog_destroy(zd); // destroy GTK dialog if there zd->sentinel = 0; // mark invalid zfree(zd->widget[0].data); // bugfix memory leak @@ -4946,8 +6948,10 @@ } -// wait for a zdialog to have a completion status or be destroyed -// returns completion status or -1 if destroyed +// Wait for a dialog to complete or be destroyed. This is a zmainloop() loop. +// The returned status is the button 1-N used to complete the dialog, or negative +// if the dialog was destroyed with [x] or otherwise by GTK. If the status was 1-N and +// the dialog will be kept active, set zd->zstat = 0 to restore the active state. int zdialog_wait(zdialog *zd) { @@ -4979,7 +6983,7 @@ } -// set cursor for zdialog (e.g. busy cursor) +// set cursor for zdialog (e.g. a busy cursor) void zdialog_set_cursor(zdialog *zd, GdkCursor *cursor) { @@ -4995,7 +6999,7 @@ } -// convenience functions for stuffing and retrieving widget data +// insert data into a zdialog widget int zdialog_stuff(zdialog *zd, cchar *name, cchar *data) // stuff a string { @@ -5022,6 +7026,9 @@ return 1; } + +// get data from a zdialog widget + int zdialog_fetch(zdialog *zd, cchar *name, char *data, int maxcc) // fetch string data { cchar *zdata; @@ -5084,6 +7091,27 @@ } +/************************************************************************** + + Widget type Combo Box with Entry + -------------------------------- + stat = zdialog_cb_app(zd, name, data) append to combo box list + stat = zdialog_cb_prep(zd, name, data) prepend to combo box list + data = zdialog_cb_get(zd, name, Nth) get combo box list item + data = zdialog_cb_delete(zd, name, data) delete entry matching "data" + data = zdialog_cb_clear(zd, name) clear all entries + zdialog_cb_popup(zd, name) open the combo box entries + + These functions map and track a combo box drop-down list, by maintaining a + parallel list in memory. The function zdialog-stuff, when called for a comboE + widget, automatically prepends the stuffed data to the drop-down list. + The private function zdialog_event(), when processing user input to the edit + box of a comboE widget, adds the updated entry to the drop-down list. + The drop-down list is kept free of redundant and blank entries. + +***************************************************************************/ + + // append new item to combo box list without changing entry box int zdialog_cb_app(zdialog *zd, cchar *name, cchar *data) @@ -5247,6 +7275,7 @@ int zdialog_positions(cchar *action) { + using namespace zfuncs; using namespace zdposn_names; char posfile[200], buff[100], wintitle[64], *pp; @@ -5254,7 +7283,7 @@ int nn, ii; FILE *fid; - snprintf(posfile,199,"%s/zdialog_positions",get_zuserdir()); // /home//.appname/zdialog_positions + snprintf(posfile,199,"%s/zdialog_positions",zuserdir); // /home//.appname/zdialog_positions if (strEqu(action,"load")) // load dialog positions table from file { @@ -5288,7 +7317,7 @@ { fid = fopen(posfile,"w"); if (! fid) { - printf("cannot write zdialog_positions file \n"); + printf("*** cannot write zdialog_positions file \n"); return 0; } @@ -5299,12 +7328,13 @@ return Nzdposn; } - printf("zdialog_positions bad action: %s \n",action); + printf("*** zdialog_positions bad action: %s \n",action); return 0; } -// Set the initial or new zdialog window position from "posn" +// Set the initial or new zdialog window position from "posn". +// Called by zdialog_run(). // null: window manager decides // "mouse" put dialog at mouse position // "desktop" center dialog in desktop window @@ -5382,6 +7412,7 @@ // If the dialog window position is "save" then save // its position WRT parent or desktop for next use. +// called by zdialog_destroy(). void zdialog_save_position(zdialog *zd) { @@ -5431,18 +7462,306 @@ } +/**************************************************************************/ + +// Functions to save and restore zdialog user inputs +// within an app session or across app sessions. + +namespace zdinputs_names +{ + struct zdinputs_t { + char *zdtitle; // zdialog title + int Nw; // no. of widgets + char **wname; // list of widget names + char **wdata; // list of widget data + } zdinputs[200]; // space for 200 dialogs + + int Nzd = 0; // no. zdialogs in use + int Nzdmax = 200; // max. zdialogs + int Nwmax = zdmaxwidgets; // max. widgets in a zdialog + int ccmax1 = 99; // max. widget name length + int ccmax2 = 199; // max. widget data length +} + + +// Load zdialog input fields from its file (app startup) +// or save zdialog input fields to its file (app shutdown). +// Action is "load" or "save". +// Number of zdialogs is returned. + +int zdialog_inputs(cchar *action) // v.5.3 +{ + using namespace zfuncs; + using namespace zdinputs_names; + + char zdinputsfile[200], buff[200]; + char zdtitle[100], wname[100][100], wdata[100][200]; + char *pp, *pp1, *pp2, wdata2[200]; + FILE *fid; + int Nw, ii, jj, cc, cc1, cc2; + + snprintf(zdinputsfile,199,"%s/zdialog_inputs",zuserdir); // /home//.appname/zdialog_inputs + + if (strEqu(action,"load")) // load dialog input fields from its file + { + Nzd = 0; + + fid = fopen(zdinputsfile,"r"); // no file + if (! fid) return 0; + + while (true) + { + pp = fgets_trim(buff,200,fid,1); // read next zdialog title record + if (! pp) break; + if (! strnEqu(pp,"zdialog == ",11)) continue; + + strncpy0(zdtitle,pp+11,ccmax1); // save new zdialog title + + pp = fgets_trim(buff,200,fid,1); // read widget count + if (! pp) break; + Nw = atoi(pp); + if (Nw < 1 || Nw > Nwmax) { + printf("*** zdialog_inputs() bad data: %s \n",zdtitle); + continue; + } + + for (ii = 0; ii < Nw; ii++) // read widget data recs + { + pp = fgets_trim(buff,200,fid,1); + if (! pp) break; + pp1 = pp; + pp2 = strstr(pp1," =="); + if (! pp2) break; // widget has no data + cc1 = pp2 - pp1; + pp1[cc1] = 0; + pp2 += 3; + if (*pp2 == ' ') pp2++; + cc2 = strlen(pp2); + if (cc1 < 1 || cc1 > ccmax1) break; + if (cc2 < 1) pp2 = (char *) ""; + if (cc2 > ccmax2) break; + strcpy(wname[ii],pp1); // save widget name and data + strcpy(wdata2,pp2); + repl_1str(wdata2,wdata[ii],"\\n","\n"); // replace "\n" with newline chars. + } + + if (ii < Nw) { + printf("*** zdialog_inputs() bad data: %s \n",zdtitle); + continue; + } + + if (Nzd == Nzdmax) { + printf("*** zdialog_inputs() too many dialogs \n"); + break; + } + + zdinputs[Nzd].zdtitle = strdupz(zdtitle,0,"zdinputs"); // save acculumated zdialog data + zdinputs[Nzd].Nw = Nw; + cc = Nw * sizeof(char *); + zdinputs[Nzd].wname = (char **) zmalloc(cc,"zdinputs"); + zdinputs[Nzd].wdata = (char **) zmalloc(cc,"zdinputs"); + for (ii = 0; ii < Nw; ii++) { + zdinputs[Nzd].wname[ii] = strdupz(wname[ii],0,"zdinputs"); + zdinputs[Nzd].wdata[ii] = strdupz(wdata[ii],0,"zdinputs"); + } + + Nzd++; + } + + fclose(fid); + return Nzd; + } + + if (strEqu(action,"save")) // save dialog input fields to its file + { + fid = fopen(zdinputsfile,"w"); + if (! fid) { + printf("*** zdialog_inputs() cannot write file \n"); + return 0; + } + + for (ii = 0; ii < Nzd; ii++) + { + fprintf(fid,"zdialog == %s \n",zdinputs[ii].zdtitle); // zdialog == zdialog title + Nw = zdinputs[ii].Nw; + fprintf(fid,"%d \n",Nw); // widget count + for (jj = 0; jj < Nw; jj++) + { + pp1 = zdinputs[ii].wname[jj]; // widget name == widget data + pp2 = zdinputs[ii].wdata[jj]; + repl_1str(pp2,wdata2,"\n","\\n"); // replace newline chars. with "\n" + fprintf(fid,"%s == %s \n",pp1,wdata2); + } + fprintf(fid,"\n"); + } + + fclose(fid); + return Nzd; + } + + printf("*** zdialog_inputs bad action: %s \n",action); + return 0; +} + + +// Save dialog user input fields when a dialog is finished. +// Called automatically by zdialog_free(). + +int zdialog_save_inputs(zdialog *zd) // v.5.3 +{ + using namespace zdinputs_names; + + char zdtitle[100], wname[100], wdata[200], *type; + int ii, jj, Nw, cc; + + if (! zd || zd->sentinel != zdsentinel) { // detect destroyed dialog + printf("*** zdialog_save_inputs, zdialog invalid \n"); + return 0; + } + + if (! zd->saveinputs) return 0; // zdialog does not use this service + + strncpy0(zdtitle,zd->widget[0].data,ccmax1); // zdialog title is widget[0].data + + for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs table + if (strEqu(zdtitle,zdinputs[ii].zdtitle)) break; + + if (ii < Nzd) { // found + zfree(zdinputs[ii].zdtitle); // delete obsolete zdinputs data + for (jj = 0; jj < zdinputs[ii].Nw; jj++) { + zfree(zdinputs[ii].wname[jj]); + zfree(zdinputs[ii].wdata[jj]); + } + zfree(zdinputs[ii].wname); + zfree(zdinputs[ii].wdata); + Nzd--; // decr. zdialog count + for (ii = ii; ii < Nzd; ii++) // pack down the rest + zdinputs[ii] = zdinputs[ii+1]; + } + + if (Nzd == Nzdmax) { + printf("*** zdialog_save_inputs, too many zdialogs \n"); + return 0; + } + + ii = Nzd; // next zdinputs table entry + + for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) // count zdialog widgets + { + type = (char *) zd->widget[jj].type; + if (strstr("dialog hbox vbox hsep vsep frame " // skip non-input widgets + "scrwin label link button",type)) continue; + Nw++; + } + + if (! Nw) return 0; // no input widgets + if (Nw > Nwmax) { + printf("*** zdialog_inputs() bad data: %s \n",zdtitle); + return 0; + } + + zdinputs[ii].zdtitle = strdupz(zdtitle,0,"zdinputs"); // set zdialog title + + cc = Nw * sizeof(char *); // allocate pointers for widgets + zdinputs[ii].wname = (char **) zmalloc(cc,"zdinputs"); + zdinputs[ii].wdata = (char **) zmalloc(cc,"zdinputs"); + + for (Nw = 0, jj = 1; zd->widget[jj].type; jj++) // add widget names and data + { + type = (char *) zd->widget[jj].type; + if (strstr("dialog hbox vbox hsep vsep frame " + "scrwin label link button",type)) continue; + strncpy0(wname,zd->widget[jj].name,ccmax1); + if (zd->widget[jj].data) + strncpy0(wdata,zd->widget[jj].data,ccmax2); + else strcpy(wdata,""); + zdinputs[ii].wname[Nw] = strdupz(wname,0,"zdinputs"); + zdinputs[ii].wdata[Nw] = strdupz(wdata,0,"zdinputs"); + Nw++; + } + + zdinputs[ii].Nw = Nw; // set widget count + Nzd++; // add zdialog to end of zdinputs + + return 1; +} + + +// Restore user input fields from prior use of the same dialog. +// Call this if wanted after zdialog is built and before it is run. +// Override old user inputs with zdialog_stuff() where needed. + +int zdialog_restore_inputs(zdialog *zd) // v.5.3 +{ + using namespace zdinputs_names; + + char *zdtitle, *wname, *wdata; + int ii, jj; + + zd->saveinputs = 1; // flag, save data at zdialog_free() + + zdtitle = (char *) zd->widget[0].data; // zdialog title + + for (ii = 0; ii < Nzd; ii++) // find zdialog in zdinputs + if (strEqu(zdtitle,zdinputs[ii].zdtitle)) break; + if (ii == Nzd) return 0; // not found + + for (jj = 0; jj < zdinputs[ii].Nw; jj++) { // stuff all saved widget data + wname = zdinputs[ii].wname[jj]; + wdata = zdinputs[ii].wdata[jj]; + zdialog_put_data(zd,wname,wdata); + } + + return 1; +} + + /************************************************************************** Translation Functions - + Translation files are standard .po files as used in the Gnu gettext system. However the .po files are used directly, and there is no need to merge and compile them into a binary format (.mo files). - Initialize: + A translation file is one of: /translate-lc.po or *-lc_RC.po + where "lc" is a standard language code and "lc_RC" a language and region code. + The file may also be compressed with the file type .po.gz + + Translation files contain two record types: + msgid "english text" + msgstr "translation text" + + The text strings may continue on multiple lines, each such segment enclosed in quotes. + The strings may contain C-format codes (%s %d etc.) and may contain escaped special + characters (\n \" etc.). + + A text string may have a context part "context::string", where "context" is any short + string, "::" is a separator, and "string" is the string to translate or the translation + of a string. This is to handle the case where a single English string may need multiple + translations, depending on context. The English string may be present multiple times in + a .po file, each one marked with a different context and having a different translation. + The context part is optional in the translation strings and is not displayed in the GUI. + + Initialize translations: int ZTXinit(cchar *lang) lang is "lc" or "lc_RC" or null (current locale will be used) + Initializes the running application for the translation of text message strings + for non-English locales. It reads a translation file which matches the English text + strings in the source program to translated text strings for the user's locale. + + This file is uncompressed if necessary and copied into the user's files at + /locales/translate-lc.po. This is done only if the file at is + newer than the copy at . The local file is read and processed into a + translation table for use by ZTX() and ZTX_translation_start(). + + lang: 2-character language code 'lc' ("de" "fr" "es" etc.) + or 5-character language and region code 'lc_RC' ("de_AT" etc.) + or null to use the current locale + + Status returned: 0 = OK, 1 = unable to process translation files. + Translate a text string: cchar *translation = ZTX(cchar *english) english: text string which may have printf formats (%d %s ...) @@ -5451,31 +7770,23 @@ If the user language is English or if no translation is found, the input string is returned, else the translated string. - A text string may have a context part "context::string", where - "context" is any string < 30 characters and "string" is the - English text or the translation text. The context part "context::" - is removed in the returned string. This is to handle the case where - a single English string may need multiple translations, depending - on context. The English string may be present multiple times in a - .po file, each one marked with a different context and having a - different translation. Context is optional in translation strings. - example: program code: printf(ZTX("answer: %d %s \n next line"), 123, "qwerty"); - A German .po file (appname-de.po) would have the following: + A German .po file (translate-de.po) would have the following: msgid "" "answer: %d %s \n" " next line" + msgstr "" "Antwort: %d %s \n" " nächste Zeile" -***/ +***************************************************************************/ namespace ZTXnames // remove GOFUNC usage becasue of { // GCC optimization errors @@ -5487,6 +7798,7 @@ char **estring, **tstring; // merged, un-quoted, un-escaped int Ntext = 0; // array counts int Ftranslate = 0; // 0/1/2 = translate none/all/missing + int Fnewtranslations = 0; // new translations have been added zdialog *zddump; // dialog for editing translations GtkWidget *trwin; // text window within dialog void ZTXgettext(char *text); @@ -5506,9 +7818,10 @@ using namespace zfuncs; using namespace ZTXnames; - int ii, err; - char *pp, pofile[200]; - char lc[4], lc_RC[8]; // language (de), * + region (de_AT) + int ii, err, Flocal = 0, Finstall = 0; + time_t localdt = 0, installdt = 0; + char localpo[200], installpo[200], ulocalesdir[200]; + char *pp, poname[20], ponamexx[20]; struct stat statb; if (Ntext) { // free prior translation @@ -5543,38 +7856,69 @@ printf("language: %s \n",zlang); if (strnEqu(zlang,"en",2)) return; // English, do nothing - - strncpy0(lc,zlang,3); // language code alone, e.g. "de" - strncpy0(lc_RC,zlang,6); // + opt. region code, "de" or "de_AT" - snprintf(pofile,199,"%s/locales/%s-%s.po",zuserdir,zappname,lc_RC); // look for user custom .po file - err = stat(pofile,&statb); // (a translation project is underway) - if (err) { - snprintf(pofile,199,"%s/locales/%s-%s.po",zuserdir,zappname,lc); - err = stat(pofile,&statb); + err = locale_filespec("userlocale","translate.po",localpo); + if (! err) { + Flocal = 1; // .po file was found in user area + stat(localpo,&statb); + localdt = statb.st_mtime; + } + + err = locale_filespec("locale","translate.po",installpo); + if (err) err = locale_filespec("locale","translate.po.gz",installpo); + if (! err) { // install .po file was found + Finstall = 1; + stat(installpo,&statb); + installdt = statb.st_mtime; } - if (err) { // look for normal installed .po file - snprintf(pofile,199,"%s/%s-%s.po",zlocalesdir,zappname,lc_RC); - err = stat(pofile,&statb); - if (err) { - snprintf(pofile,199,"%s/%s-%s.po",zlocalesdir,zappname,lc); // default to lc only v.5.0 - err = stat(pofile,&statb); - } + if (! Finstall && ! Flocal) { // no local or installed .po found + printf("*** no translate-%s.po file found \n",zlang); + return; } - if (! err) printf("using translation file: %s \n",pofile); - else { - printf("no translation file found for %s \n",lc_RC); - return; + if (Finstall && Flocal) { // both local and install .po present + if (installdt > localdt) { // local .po is stale, replace it + printf("installed .po file is newer than local .po \n"); // v.5.5 + Flocal = 0; + } } + + if (Flocal) printf("using local .po file: %s \n",localpo); + + else + { + printf("using installed .po file: %s \n",installpo); + snprintf(ulocalesdir,200,"%s/locales",zuserdir); // /home//.appname/locales + + pp = strrchr(installpo,'/'); // extract filename from full path + strcpy(poname,pp+1); + pp = strstr(poname,".gz"); // uncompress/copy installed .po file + if (pp) { // to local .po file v.5.5 + *pp = 0; + err = shell_ack("gunzip -c %s > %s/%s",installpo,ulocalesdir,poname); + } + else err = shell_ack("cp %s %s/%s",installpo,ulocalesdir,poname); + if (err) return; + + pp = strstr(poname,"-en.po"); // if translate-en was used, + if (pp) { // rename to the target locale code + strcpy(ponamexx,poname); + strcpy(poname+(pp+1-poname),zlang); // translate-en.po >> translate-xx.po + strcat(poname,".po"); + shell_ack("mv %s/%s %s/%s",ulocalesdir,ponamexx,ulocalesdir,poname); + } - fidr = fopen(pofile,"r"); // open .po file + sprintf(localpo,"%s/%s",ulocalesdir,poname); // final uncompressed local .po file + printf("new local .po file: %s \n",localpo); + } + + fidr = fopen(localpo,"r"); // open .po file if (! fidr) { - printf("cannot open translation file: %s \n",pofile); + printf("*** cannot open .po file: %s \n",localpo); return; } - + porec = 0; // no .po record yet *Etext = *Ttext = 0; // no text yet @@ -5655,6 +7999,8 @@ tstring[ii] = strdupz(pp,0,"ZTX"); } + Fnewtranslations = 1; // notify app of new translations v.5.5 + return; } @@ -5687,7 +8033,7 @@ if (! *ppq2) return; cc = ppq2 - ppq1 + 1; // min. case is "" if (cc + 1 + scc >= ZTXmaxcc) - printf("string is too long %s \n",pstring); + printf("*** string is too long %s \n",pstring); else { strncpy0(pstring+scc,ppq1,cc+1); // accum. substrings, minus quotes scc += cc; @@ -5754,22 +8100,31 @@ Online Translation Utility - If the application menu "Translate" is selected, the function - ZTX_translation_start() is called and the user dialog is used to - set translation mode ON or OFF. If ON, the .po translation file - is copied from the install location to a user location (if not - already there) where it will be modified. This user .po file - will be used for translations going forward. - - Run the application to be translated in the usual manner. Select each - menu or toolbar function needing translation. The related english text - strings and current translations are dumped into a text edit window. - The user can edit the translations while using the application and - therefore more easily understand the complete context. - - Whenever the application calls ZTX() to get a translation, the - function ZTX_translation_dump() is called to add the english text - and the current translation to the text edit window. + This function allows you to update the translations for an application interactively, + while using the application. The advantage is that you can better understand the context + and make a faster and better translation. The updated translations are also immediately + seen in the application dialogs. The process is started from an application's + "Edit Translation" menu. A popup text window is created and displays all english strings + and their translations as they are being used in the application. The format is like a + .po file. The translations may be edited in the window as soon as they appear. When the + [apply] button is pressed, the translation file /locales/translate-lc.po is + updated from the edit window. Each time this function is used, a new popup text window + is created for additional translations, and these are accumulated in the .po file. + When the process is started, a choice can be made to show only missing translations or + all translations. Normally only missing translations are needed, but revisions in existing + translations can also be made. Once a translation is edited and the [apply] button is used, + the translation is no longer missing and will not appear again in the window if 'only missing' + was chosen. The function can be started again (using the application's menu) to change the + mode to show all translations or only missing translations. + + Workflow: If a .po file for your language is provided, use this as a basis for revisions. + If no .po is present, create one using the Gnu utility xgettext with keyword "ZTX" and put + it in the expected location (see zlocalesdir above). If you are starting from zero, editing + the .po file using Gnu poedit (or any text editor) is likely the most efficient method to + get most of the work done. Use the interactive function to check and polish the translations + while using the application. + + The window for editing translations looks like this: ______________________________________________________ | | | msgid "English text with formats %d %d ... " | @@ -5786,7 +8141,6 @@ ***************************************************************************/ - // This function is called from the application menu "Translate". // Set translation mode ON or OFF (zfuncs::Ftranslate). If ON and no // .po translation file is found in /home//.appname/locales/, @@ -5804,12 +8158,10 @@ zdialog *zd; int err, tron, trmissing; - char *pp, command[400]; - char pofile1[200], pofile2[200]; - struct stat statb; + char localpo[200]; if (strnEqu(zlang,"en",2)) { // locale cannot be english - zmessageACK(0,"cannot translate locale %s",zlang); + zmessageACK(0,0,"cannot translate locale %s",zlang); return; } @@ -5852,27 +8204,13 @@ if (Ftranslate) // start or continue translation mode { - snprintf(pofile1,199,"%s/%s-%s.po",zlocalesdir,zappname,zlang); // installed .po file for lc_RC - snprintf(pofile2,199,"%s/locales/%s-%s.po",zuserdir,zappname,zlang); // user .po file for new translations - - err = stat(pofile2,&statb); // look for user .po file - if (err) { - err = stat(pofile1,&statb); // look for installed .po file - if (err) printf("No .po file found %s \n",pofile1); - else { - pp = strrchr(pofile2,'/'); // create /locales if needed - *pp = 0; // bugfix v.5.0 - mkdir(pofile2,0754); - *pp = '/'; - snprintf(command,399,"cp -R %s %s",pofile1,pofile2); // copy installed .po file to user - err = system(command); - if (! err) printf(".po file copied from %s to %s \n",pofile1,pofile2); - else printf("cannot copy .po file from %s to %s \n",pofile1,pofile2); - } + err = locale_filespec("userlocale","translate.po",localpo); + if (err) { // local translate-xx.po required v.5.5 + printf("*** no local translation file %s \n",localpo); + Ftranslate = 0; + return; } - ZTXinit(zlang); // re-initialize translations - zddump = zdialog_new("Translations",parent,"apply","cancel",null); // create dialog for translation edits zdialog_add_widget(zddump,"scrwin","scrwin","dialog",0,"expand"); zdialog_add_widget(zddump,"edit","edit","scrwin"); @@ -5891,12 +8229,15 @@ { using namespace ZTXnames; - if (! zd->zstat) return 1; // wait for [apply] or [cancel] + if (! zd->zstat) return 1; // wait for [apply] or [done] - if (zd->zstat == 1) // [apply] + if (zd->zstat == 1) { // [apply] + zd->zstat = 0; // keep dialog active v.13.03 ZTX_translation_update(); // update user .po file + return 1; + } - Ftranslate = 0; + Ftranslate = 0; // [cancel] zdialog_free(zd); // kill edit window zddump = 0; trwin = 0; @@ -5904,6 +8245,7 @@ } +// private function // This function is called from ZTX() for every translation. // Add the english text and current translation to the text edit window. @@ -5961,6 +8303,7 @@ } +// private function // merge translation edit window with user .po file and clear the window void ZTXnames::ZTX_translation_update() // modified v.5.0 @@ -5968,8 +8311,8 @@ using namespace zfuncs; using namespace ZTXnames; - int ii, err, ftf, Nupd; - char pofile1[200], pofile2[200]; + int ii, ftf, err, Nupd; + char localpo[200], localponew[200]; char Fused[ZTXmaxent]; ftf = 1; @@ -6020,7 +8363,7 @@ else { - printf("unrecognized record: %s \n",wporec); + printf("*** unrecognized record: %s \n",wporec); wporec = 0; continue; } @@ -6044,19 +8387,24 @@ if (! Nupd) return; // nothing new memset(Fused,0,ZTXmaxent); // clear 'english text used' flags + + err = locale_filespec("userlocale","translate.po",localpo); + if (err) { + printf("*** no local translation file %s \n",localpo); + return; + } - snprintf(pofile1,199,"%s/locales/%s-%s.po",zuserdir,zappname,zlang); // user .po file for new translations - fidr = fopen(pofile1,"r"); // open .po file for read + fidr = fopen(localpo,"r"); // open .po file for read if (! fidr) { - printf("cannot read translation file: %s \n",pofile1); + printf("*** cannot read translation file: %s \n",localpo); return; } - strcpy(pofile2,pofile1); // open .po.new file for write - strcat(pofile2,".new"); - fidw = fopen(pofile2,"w"); + strcpy(localponew,localpo); // open .po.new file for write + strcat(localponew,".new"); + fidw = fopen(localponew,"w"); if (! fidw) { - printf("cannot write updated translation file: %s \n",pofile2); + printf("*** cannot write new translation file: %s \n",localponew); fclose(fidr); return; } @@ -6111,13 +8459,12 @@ fclose(fidw); if (Nupd) { // rename *.po.new to *.po - err = rename(pofile2,pofile1); - if (err) printf("cannot rename %s to *.po",pofile2); + err = rename(localponew,localpo); + if (err) printf("*** cannot rename %s to *.po",localponew); } - else remove(pofile2); // no changes, delete *.po.new + else remove(localponew); // no changes, delete *.po.new ZTXinit(zlang); // put translations in use - return; } @@ -6154,7 +8501,7 @@ } cc = ppq2 - ppq1 + 1; // min. case is "" if (cc + 1 + scc >= ZTXmaxcc) - printf("string is too long: %s \n",pstring); + printf("*** string is too long: %s \n",pstring); else { strncpy0(pstring+scc,ppq1,cc+1); // accum. substrings, minus quotes scc += cc; @@ -6202,18 +8549,44 @@ } +// A caller of ZTX_translation_start() can use this function to detect if translations have +// changed since the last call. This is required if popup menu items have translations and must +// be initialized again, because the names are held internally in GTK. Otherwise ZTX() uses the +// revised translations automatically. + +int ZTXnewtranslations() // v.5.5 +{ + using namespace ZTXnames; + + if (Fnewtranslations) { + Fnewtranslations = 0; // reset status flag + return 1; + } + return 0; +} + + /**************************************************************************/ -// Output text to a popup window. action: open, write, top, close -// Window is left on screen until user destroys it with [x] button -// or caller closes it with "close" action. +// Output text to a popup window. +// Window is left on screen until user destroys it with [x] button or +// caller closes it with "close" action. +// action: open: create window with title = text and size = ww, hh +// write: write text to next line in window +// top: scroll back to line 1 after writing last line +// close: close window +// The parent argument is optional and will cause the popup window to center +// on the parent window. The GtkTextView window is returned for possible +// use with textwidget_set_clickfunc() etc. -int write_popup_text(cchar *action, cchar *text, int ww, int hh, GtkWidget *parent) +GtkWidget * write_popup_text(cchar *action, cchar *text, int ww, int hh, GtkWidget *parent) { static GtkWidget *mWin = 0, *mVbox, *mScroll; static GtkWidget *mLog = 0; static PangoFontDescription *monofont = 0; + zthreadcrash(); // thread usage not allowed + if (! monofont) monofont = pango_font_description_from_string("monospace 9"); @@ -6236,7 +8609,7 @@ else gtk_window_set_position(GTK_WINDOW(mWin),GTK_WIN_POS_MOUSE); - mVbox = gtk_vbox_new(0,0); // vertical packing box + 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 @@ -6270,7 +8643,7 @@ } zmainloop(); - return 0; + return mLog; } @@ -6301,25 +8674,33 @@ /**************************************************************************/ // display message box and wait for user acknowledgement +// may be called from a thread (uses xterm message). -void zmessageACK(GtkWidget *parent, cchar *pMess, ... ) +void zmessageACK(GtkWidget *parent, cchar *title, cchar *format, ... ) // with title v.5.5 { va_list arglist; - char message[400]; + char message[1000]; zdialog *zd; GtkWidget *widget; - va_start(arglist,pMess); - vsnprintf(message,400,pMess,arglist); + if (! pthread_equal(pthread_self(),zfuncs::tid_main)) { // from a thread, no GTK allowed + zpopup_message(format); + return; + } + + va_start(arglist,format); + vsnprintf(message,999,format,arglist); va_end(arglist); - zd = zdialog_new("message",parent,"OK",null); // v.5.1 + zd = zdialog_new(title,parent,"OK",null); zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5"); zdialog_add_widget(zd,"label","lab1","hb1",message,"space=5"); zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); - zdialog_run(zd); + gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 + gtk_window_present(GTK_WINDOW(widget)); // should not be needed (GTK bug) v.5.5 + zdialog_run(zd,0,"mouse"); // v.5.5 zdialog_wait(zd); zdialog_free(zd); return; @@ -6330,17 +8711,17 @@ // log error message to STDOUT as well as message box and user OK -void zmessLogACK(GtkWidget *parent, cchar *pMess, ...) +void zmessLogACK(GtkWidget *parent, cchar *format, ...) { va_list arglist; - char message[200]; + char message[1000]; - va_start(arglist,pMess); - vsnprintf(message,200,pMess,arglist); + va_start(arglist,format); + vsnprintf(message,999,format,arglist); va_end(arglist); printf("%s \n",message); - zmessageACK(parent,message); + zmessageACK(parent,0,message); return; } @@ -6348,8 +8729,9 @@ /**************************************************************************/ // display message box and wait for user Yes or No response +// returns 1 or 0 -int zmessageYN(GtkWidget *parent, cchar *pMess, ... ) +int zmessageYN(GtkWidget *parent, cchar *format, ... ) { va_list arglist; char message[400]; @@ -6357,8 +8739,10 @@ int zstat; GtkWidget *widget; - va_start(arglist,pMess); - vsnprintf(message,400,pMess,arglist); + zthreadcrash(); // thread usage not allowed + + va_start(arglist,format); + vsnprintf(message,400,format,arglist); va_end(arglist); zd = zdialog_new("message",parent,ZTX("Yes"),ZTX("No"),null); @@ -6367,7 +8751,8 @@ zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); - zdialog_run(zd); + gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 + zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); zdialog_free(zd); if (zstat == 1) return 1; @@ -6381,7 +8766,7 @@ char *zmessage_help_topic = 0; -void zmessage_help(GtkWidget *parent, cchar *topic, cchar *pmess, ... ) +void zmessage_help(GtkWidget *parent, cchar *topic, cchar *format, ... ) { int zmessage_help_event(zdialog *zd, cchar *event); @@ -6389,8 +8774,10 @@ char message[400]; zdialog *zd; - va_start(arglist,pmess); - vsnprintf(message,400,pmess,arglist); + zthreadcrash(); // thread usage not allowed + + va_start(arglist,format); + vsnprintf(message,400,format,arglist); va_end(arglist); if (zmessage_help_topic) zfree(zmessage_help_topic); @@ -6422,7 +8809,7 @@ // display message indefinitely until timeout or user cancel // or caller kills it with zdialog_free() -zdialog * zmessage_post(GtkWidget *parent, int seconds, cchar *pMess, ... ) +zdialog * zmessage_post(GtkWidget *parent, int seconds, cchar *format, ... ) { int zmessage_post_timeout(zdialog *zd); int zmessage_post_event(zdialog *zd, cchar *event); @@ -6431,8 +8818,10 @@ char message[400]; static zdialog *zd; - va_start(arglist,pMess); - vsnprintf(message,400,pMess,arglist); + zthreadcrash(); // thread usage not allowed + + va_start(arglist,format); + vsnprintf(message,400,format,arglist); va_end(arglist); zd = zdialog_new("message",parent,ZTX("cancel"),null); @@ -6464,6 +8853,7 @@ // get text input from a popup dialog // returned text is subject for zfree() +// null is returned if user presses [cancel] button. char * zdialog_text(GtkWidget *parent, cchar *title, cchar *inittext) { @@ -6472,6 +8862,8 @@ char *text; GtkWidget *widget; + zthreadcrash(); // thread usage not allowed + zd = zdialog_new(title,parent,"OK",ZTX("cancel"),null); zdialog_add_widget(zd,"frame","fred","dialog"); zdialog_add_widget(zd,"edit","edit","fred"); @@ -6480,7 +8872,8 @@ zdialog_resize(zd,200,0); widget = zdialog_widget(zd,"dialog"); // make modal gtk_window_set_modal(GTK_WINDOW(widget),1); - zdialog_run(zd); + gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 + zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); if (zstat == 1) text = (char *) zdialog_get_data(zd,"edit"); @@ -6498,14 +8891,17 @@ // Parent window may be NULL. // List of buttons must be NULL terminated. -int zdialog_choose(cchar *title, GtkWidget *parent, cchar *message, ...) +int zdialog_choose(cchar *title, GtkWidget *parent, cchar *format, ...) { zdialog *zd; va_list arglist; int ii, zstat, Nbutts; cchar *butts[6]; + GtkWidget *widget; + + zthreadcrash(); // thread usage not allowed - va_start(arglist,message); + va_start(arglist,format); for (ii = 0; ii < 5; ii++) { @@ -6520,8 +8916,11 @@ zd = zdialog_new(title,parent,butts[0],butts[1],butts[2],butts[3],butts[4],null); zdialog_add_widget(zd,"hbox","hbmess","dialog","space=5"); - zdialog_add_widget(zd,"label","labmess","hbmess",message,"space=5"); - zdialog_run(zd); + zdialog_add_widget(zd,"label","labmess","hbmess",format,"space=5"); + widget = zdialog_widget(zd,"dialog"); // make modal + gtk_window_set_modal(GTK_WINDOW(widget),1); + gtk_window_set_keep_above(GTK_WINDOW(widget),1); // keep on top v.5.2 + zdialog_run(zd,0,"mouse"); // v.5.5 zstat = zdialog_wait(zd); zdialog_free(zd); @@ -6532,6 +8931,133 @@ /**************************************************************************/ +// Show a small popup text message at the given window position. +// Any prior popup will be killed and replaced. +// If text == null, kill without replacement. +// If secs > 0, kill popup after secs seconds. + +void poptext_window(cchar *text, GtkWindow *pwin, int mx, int my, int secs) +{ + int popup_timeout(GtkWidget **); + + static GtkWidget *popwin[100], *vbox, *label; // v.5.5 + static char *poptext[100]; + static int npop = -1; + int cc, pmx, pmy; + + zthreadcrash(); // thread usage not allowed + + if (npop < 0) { + memset(popwin,0,100*sizeof(GtkWidget *)); // first call initialization + memset(poptext,0,100*sizeof(char *)); + npop = 0; + } + + if (! text) { + if (popwin[npop]) gtk_widget_destroy(popwin[npop]); // kill prior popup + popwin[npop] = 0; + if (poptext[npop]) zfree(poptext[npop]); + poptext[npop] = 0; + return; + } + + if (++npop == 100) npop = 0; // next unique widget pointer + + if (poptext[npop]) zfree(poptext[npop]); + cc = strlen(text); + poptext[npop] = zmalloc(cc+4,"poptext_window"); + poptext[npop][0] = ' '; // add blank before/after text + strcpy(poptext[npop]+1,text); + strcpy(poptext[npop]+cc+1," "); + if (popwin[npop]) gtk_widget_destroy(popwin[npop]); + popwin[npop] = gtk_window_new(GTK_WINDOW_POPUP); + vbox = gtk_box_new(VERTICAL,0); + gtk_container_add(GTK_CONTAINER(popwin[npop]),vbox); + label = gtk_label_new(poptext[npop]); + gtk_box_pack_end(GTK_BOX(vbox),label,1,1,0); + + if (pwin) { // position relative to pwin + gtk_window_get_position(pwin,&pmx,&pmy); + mx += pmx; + my += pmy; + } + + gtk_window_move(GTK_WINDOW(popwin[npop]),mx,my); + gtk_widget_show_all(popwin[npop]); + + if (secs) g_timeout_add_seconds(secs,(GSourceFunc) popup_timeout,&popwin[npop]); // v.5.5 + return; +} + + +// timer function to kill popup windows after timeout +// caller's widget pointer is also cleared + +int popup_timeout(GtkWidget **widget) +{ + if (*widget) gtk_widget_destroy(*widget); + *widget = 0; + return 0; +} + + +// Show a small popup text message at the current mouse position + offsets. +// Any prior popup will be killed and replaced. +// If text == null, kill without replacement. +// If secs > 0, kill popup after secs seconds. +// (use GdkDevice mouse = event->device) + +void poptext_mouse(GdkDevice *mouse, cchar *text, int dx, int dy, int secs) +{ + int popup_timeout(GtkWidget **); + + static GtkWidget *popwin[100], *vbox, *label; // v.5.5 + static char *poptext[100]; + static int npop = -1; + int cc, mx, my; + + zthreadcrash(); // thread usage not allowed + + if (npop < 0) { + memset(popwin,0,100*sizeof(GtkWidget *)); // first call initialization + memset(poptext,0,100*sizeof(char *)); + npop = 0; + } + + if (! text) { + if (popwin[npop]) gtk_widget_destroy(popwin[npop]); // kill prior popup + popwin[npop] = 0; + if (poptext[npop]) zfree(poptext[npop]); + poptext[npop] = 0; + return; + } + + if (++npop == 100) npop = 0; // next unique widget pointer + + if (poptext[npop]) zfree(poptext[npop]); + cc = strlen(text); + poptext[npop] = zmalloc(cc+4,"poptext_mouse"); + poptext[npop][0] = ' '; + strcpy(poptext[npop]+1,text); + strcpy(poptext[npop]+cc+1," "); + if (popwin[npop]) gtk_widget_destroy(popwin[npop]); + popwin[npop] = gtk_window_new(GTK_WINDOW_POPUP); + vbox = gtk_box_new(VERTICAL,0); + gtk_container_add(GTK_CONTAINER(popwin[npop]),vbox); + label = gtk_label_new(poptext[npop]); + gtk_box_pack_end(GTK_BOX(vbox),label,1,1,0); + + gdk_device_get_position(mouse,0,&mx,&my); + gtk_window_move(GTK_WINDOW(popwin[npop]),mx+dx,my+dy); + gtk_widget_show_all(popwin[npop]); + + if (secs) g_timeout_add_seconds(secs,(GSourceFunc) popup_timeout,&popwin[npop]); // v.5.5 + return; +} + + +/**************************************************************************/ + // File chooser dialog for one or more files // // Action: "open" select an existing file @@ -6645,7 +9171,7 @@ if (initfile) { // pre-select filespec err = stat(initfile,&fstat); if (err) { - pdir = strdupz(initfile); // non-existent file + pdir = strdupz(initfile,0,"zgetfile"); // non-existent file pfile = strrchr(pdir,'/'); if (pfile && pfile > pdir) { *pfile++ = 0; // set folder name @@ -6746,21 +9272,23 @@ } -// zgetfile private function - respond to F1 key -// zfuncs::F1_help_topic must be pre-loaded by caller if needed. +// zgetfile private function - send F1 key (help) to main app int zgetfile_KBkey(GtkWidget *dialog, GdkEventKey *event) { int KBkey = event->keyval; - if (KBkey == GDK_KEY_F1) - showz_userguide(zfuncs::F1_help_topic); + if (KBkey != GDK_KEY_F1) return 0; // v.5.5 + KBstate(KBkey,0); return 1; } /************************************************************************** - Programs for printing an image file + print_image_file(GtkWidget *parent, cchar *imagefile) + + Print an image file using the printer, paper, orientation, + margins, and scale set by the user. HPLIP problem: Setting paper size was made less flexible. GtkPrintSettings paper size must agree with the one in the current @@ -6770,37 +9298,18 @@ Print margins can be changed to effect printing a smaller or shifted image on a larger paper size. - - print_image_paper_setup() - - Do a print paper format selection, after which the page width, height - and orientation are available to the caller. Units are CM. - (paper width and height are reversed for landscape orientation) - - - print_image_margins_setup() - - Optionally set the print margins. If not done they are zero - (or printer-dependent minimum). Afterwards the margins are - available to the caller. Units are CM. - - - print_image_file(cchar *imagefile) - - Print the image file on the printer and paper size determined by - a prior call to print_image_paper_setup. - - ***************************************************************************/ namespace print_image { #define MM GTK_UNIT_MM + #define INCH GTK_UNIT_INCH #define PRINTOP GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG #define PORTRAIT GTK_PAGE_ORIENTATION_PORTRAIT #define LANDSCAPE GTK_PAGE_ORIENTATION_LANDSCAPE #define QUALITY GTK_PRINT_QUALITY_HIGH + GtkWidget *parent = 0; GtkPageSetup *priorpagesetup = 0; GtkPageSetup *pagesetup; GtkPrintSettings *printsettings = 0; @@ -6808,20 +9317,104 @@ GtkPageOrientation orientation = PORTRAIT; GdkPixbuf *pixbuf; cchar *printer = 0; - double width = 21.0, height = 29.7; // paper size, CM (default A4 portrait) - double margins[4] = { 0.1, 0.1, 0.1, 0.1 }; // margins, CM (default 0.1) v.5.2 int landscape = 0; // true if landscape + double width = 21.0, height = 29.7; // paper size, CM (default A4 portrait) + double margins[4] = { 0.5, 0.5, 0.5, 0.5 }; // margins, CM (default 0.5) v.5.5 + double imagescale = 100; // image print scale, percent + double pwidth, pheight; // printed image size + + int paper_setup(); + int margins_setup(); + int margins_dialog_event(zdialog *zd, cchar *event); + void get_printed_image_size(); + void print_page(GtkPrintOperation *, GtkPrintContext *, int page); +} + + +// user callable function to set paper, margins, scale, and then print + +void print_image_file(GtkWidget *pwin, cchar *imagefile) // overhauled v.5.2 +{ + using namespace print_image; + + GtkPrintOperationResult printstat; + GError *gerror = 0; + int err; + + parent = pwin; // save parent window + + pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror); // read image file + if (! pixbuf) { + zmessageACK(null,0,gerror->message); + return; + } + + err = paper_setup(); // select paper + if (err) return; // v.5.5 + + err = margins_setup(); // set margins and scale + if (err) return; // v.5.5 + + printop = gtk_print_operation_new(); // print operation + gtk_print_operation_set_default_page_setup(printop,pagesetup); + gtk_print_operation_set_print_settings(printop,printsettings); + gtk_print_operation_set_n_pages(printop,1); + + g_signal_connect(printop,"draw-page",G_CALLBACK(print_page),0); // start print + printstat = gtk_print_operation_run(printop,PRINTOP,0,0); + + if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) { + gtk_print_operation_get_error(printop,&gerror); + zmessageACK(null,0,gerror->message); + } + + g_object_unref(printop); + return; +} + + +// draw the graphics for the print page +// rescale with cairo + +void print_image::print_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page) +{ + using namespace print_image; + + cairo_t *cairocontext; + double iww, ihh, pww, phh, scale; + + pww = gtk_print_context_get_width(printcontext); // print context size, pixels + phh = gtk_print_context_get_height(printcontext); + + iww = gdk_pixbuf_get_width(pixbuf); // original image size + ihh = gdk_pixbuf_get_height(pixbuf); + + scale = pww / iww; // rescale to fit page + if (phh / ihh < scale) scale = phh / ihh; + + cairocontext = gtk_print_context_get_cairo_context(printcontext); // use cairo to rescale + cairo_translate(cairocontext,0,0); + cairo_scale(cairocontext,scale,scale); + gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0); + cairo_paint(cairocontext); + + return; } -void print_image_paper_setup(GtkWidget *parent) +// Do a print paper format selection, after which the page width, height +// and orientation are available to the caller. Units are CM. +// (paper width and height are reversed for landscape orientation) + +int print_image::paper_setup() { + using namespace zfuncs; using namespace print_image; char printsettingsfile[200], pagesetupfile[200]; - snprintf(printsettingsfile,200,"%s/printsettings",get_zuserdir()); - snprintf(pagesetupfile,200,"%s/pagesetup",get_zuserdir()); + snprintf(printsettingsfile,200,"%s/printsettings",zuserdir); + snprintf(pagesetupfile,200,"%s/pagesetup",zuserdir); if (! printsettings) { // start with prior print settings printsettings = gtk_print_settings_new_from_file(printsettingsfile,0); @@ -6835,18 +9428,12 @@ priorpagesetup = gtk_page_setup_new(); } - pagesetup = gtk_print_run_page_setup_dialog // select printer, paper size - (GTK_WINDOW(parent),priorpagesetup,printsettings); // and orientation - if (! pagesetup) return; + pagesetup = gtk_print_run_page_setup_dialog // select printer, paper, orientation + (GTK_WINDOW(parent),priorpagesetup,printsettings); // user cancel cannot be detected v.5.5 g_object_unref(priorpagesetup); // save for next call priorpagesetup = pagesetup; - width = gtk_page_setup_get_paper_width(pagesetup,MM); // save paper width, height - height = gtk_page_setup_get_paper_height(pagesetup,MM); - width = width / 10; // use cm units - height = height / 10; - orientation = gtk_print_settings_get_orientation(printsettings); // save orientation if (orientation == LANDSCAPE) landscape = 1; else landscape = 0; @@ -6857,123 +9444,143 @@ gtk_print_settings_to_file(printsettings,printsettingsfile,0); // save print settings to file gtk_page_setup_to_file(pagesetup,pagesetupfile,0); // save print settings to file - return; + return 0; } -void print_image_margins_setup(GtkWidget *parent) +// Optionally set the print margins and print scale. +// If canceled the margins are zero (or printer-dependent minimum) +// and the scale is 100% (fitting the paper and margins). + +int print_image::margins_setup() { using namespace print_image; - + zdialog *zd; int zstat; + char imagesize[100]; - zd = zdialog_new(ZTX("margins"),parent,ZTX("done"),ZTX("cancel"),0); // build dialog + #define formatsize ZTX("printed image width: %.1f height: %.1f cm") + +/*** + top bottom left right + margins [___] [___] [___] [___] + scale [80|-+] percent + printed image width: xx.x height: xx.x cm +***/ - zdialog_add_widget(zd,"hbox","hbmlab","dialog",0,"homog"); // top bottom left right - zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"space=3"); // margins [___] [___] [___] [___] - zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"space=3"); - zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"space=3"); - zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"space=3"); - zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"space=3"); + zd = zdialog_new(ZTX("margins"),parent,ZTX("done"),ZTX("cancel"),0); + zdialog_add_widget(zd,"hbox","hbmlab","dialog"); + zdialog_add_widget(zd,"vbox","vbmarg","hbmlab",0,"space=5"); + zdialog_add_widget(zd,"vbox","vbtop","hbmlab",0,"space=5"); + zdialog_add_widget(zd,"vbox","vbbottom","hbmlab",0,"space=5"); + zdialog_add_widget(zd,"vbox","vbleft","hbmlab",0,"space=5"); + zdialog_add_widget(zd,"vbox","vbright","hbmlab",0,"space=5"); zdialog_add_widget(zd,"label","space","vbmarg"," "); zdialog_add_widget(zd,"label","labtop","vbtop",ZTX("top")); zdialog_add_widget(zd,"label","labbot","vbbottom",ZTX("bottom")); zdialog_add_widget(zd,"label","lableft","vbleft",ZTX("left")); zdialog_add_widget(zd,"label","labright","vbright",ZTX("right")); - zdialog_add_widget(zd,"label","labmarg","vbmarg",ZTX("margins"),"space=3"); + zdialog_add_widget(zd,"label","labmarg","vbmarg",ZTX("margins"),"space=5"); zdialog_add_widget(zd,"spin","mtop","vbtop","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mbottom","vbbottom","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mleft","vbleft","0|3|0.1|0"); zdialog_add_widget(zd,"spin","mright","vbright","0|3|0.1|0"); - - zdialog_stuff(zd,"mtop",margins[0]); // stuff prior print margins + zdialog_add_widget(zd,"hbox","hbscale","dialog",0,"space=8"); + zdialog_add_widget(zd,"label","labscale","hbscale",ZTX("image scale"),"space=6"); + zdialog_add_widget(zd,"spin","scale","hbscale","5|100|0.1|100"); + zdialog_add_widget(zd,"label","labpct","hbscale",ZTX("percent"),"space=5"); + zdialog_add_widget(zd,"hbox","hbsize","dialog"); + zdialog_add_widget(zd,"label","labsize","hbsize","xxx","space=6"); + + zdialog_stuff(zd,"mtop",margins[0]); // stuff prior page margins zdialog_stuff(zd,"mbottom",margins[1]); zdialog_stuff(zd,"mleft",margins[2]); zdialog_stuff(zd,"mright",margins[3]); + zdialog_stuff(zd,"scale",imagescale); // stuff prior image scale + + gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins + gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) + gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); + gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); + gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % - zdialog_run(zd); // run dialog + get_printed_image_size(); + snprintf(imagesize,100,formatsize,pwidth,pheight); // show print size in dialog + zdialog_stuff(zd,"labsize",imagesize); + + zdialog_run(zd,margins_dialog_event); // run dialog zstat = zdialog_wait(zd); // wait for completion if (zstat != 1) { // user canceled zdialog_free(zd); - return; + return 1; // v.5.5 } - zdialog_fetch(zd,"mtop",margins[0]); // set print margins - zdialog_fetch(zd,"mbottom",margins[1]); - zdialog_fetch(zd,"mleft",margins[2]); - zdialog_fetch(zd,"mright",margins[3]); - zdialog_free(zd); // kill dialog - - gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins, mm units - gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); - gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); - gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); - - return; + return 0; } -void print_image_file(cchar *imagefile) +// dialog event function +// save user margin and scale changes +// recompute print image size + +int print_image::margins_dialog_event(zdialog *zd, cchar *event) { using namespace print_image; - void print_image_page(GtkPrintOperation *, GtkPrintContext *, int page); + char imagesize[100]; - GtkPrintOperationResult printstat; - GError *gerror = 0; + zdialog_fetch(zd,"mtop",margins[0]); // save print margins + zdialog_fetch(zd,"mbottom",margins[1]); + zdialog_fetch(zd,"mleft",margins[2]); + zdialog_fetch(zd,"mright",margins[3]); + zdialog_fetch(zd,"scale",imagescale); // save image print scale - pixbuf = gdk_pixbuf_new_from_file(imagefile,&gerror); // read image file - if (! pixbuf) { - zmessageACK(null,gerror->message); - return; - } - - printop = gtk_print_operation_new(); // print operation - gtk_print_operation_set_default_page_setup(printop,pagesetup); - gtk_print_operation_set_print_settings(printop,printsettings); - gtk_print_operation_set_n_pages(printop,1); - - g_signal_connect(printop,"draw-page",G_CALLBACK(print_image_page),0); // start print - printstat = gtk_print_operation_run(printop,PRINTOP,0,0); + gtk_page_setup_set_top_margin(pagesetup,10*margins[0],MM); // set page margins + gtk_page_setup_set_bottom_margin(pagesetup,10*margins[1],MM); // (cm to mm units) + gtk_page_setup_set_left_margin(pagesetup,10*margins[2],MM); + gtk_page_setup_set_right_margin(pagesetup,10*margins[3],MM); + gtk_print_settings_set_scale(printsettings,imagescale); // set image print scale % - if (printstat == GTK_PRINT_OPERATION_RESULT_ERROR) { - gtk_print_operation_get_error(printop,&gerror); - zmessageACK(null,gerror->message); - } + get_printed_image_size(); + snprintf(imagesize,100,formatsize,pwidth,pheight); // show print size in dialog + zdialog_stuff(zd,"labsize",imagesize); - g_object_unref(printop); - return; + return 1; } -// draw the graphics for the print page -// rescale with cairo: print resolution of 300 dpi is no longer ignored +// compute printed image size based on paper size, +// orientation, margins, and scale (percent) -void print_image_page(GtkPrintOperation *printop, GtkPrintContext *printcontext, int page) -{ +void print_image::get_printed_image_size() +{ using namespace print_image; - cairo_t *cairocontext; - double iww, ihh, pww, phh, scale; - - pww = gtk_print_context_get_width(printcontext); // print context size - phh = gtk_print_context_get_height(printcontext); + double iww, ihh, pww, phh, scale; - iww = gdk_pixbuf_get_width(pixbuf); // image size + pww = 0.1 * gtk_page_setup_get_paper_width(pagesetup,MM); // get paper size + phh = 0.1 * gtk_page_setup_get_paper_height(pagesetup,MM); // (mm to cm units) + + pww = pww - margins[2] - margins[3]; // reduce for margins + phh = phh - margins[1] - margins[2]; + + pww = pww / 2.54 * 300; // convert to dots @ 300 dpi + phh = phh / 2.54 * 300; + + iww = gdk_pixbuf_get_width(pixbuf); // original image size, pixels ihh = gdk_pixbuf_get_height(pixbuf); - scale = pww / iww; // rescale to fit page + scale = pww / iww; // rescale image to fit page if (phh / ihh < scale) scale = phh / ihh; + + scale = scale * 0.01 * imagescale; // adjust for user scale setting - cairocontext = gtk_print_context_get_cairo_context(printcontext); // use cairo to rescale - cairo_translate(cairocontext,0,0); - cairo_scale(cairocontext,scale,scale); - gdk_cairo_set_source_pixbuf(cairocontext,pixbuf,0,0); - cairo_paint(cairocontext); - + pwidth = iww * scale / 300 * 2.54; // dots to cm + pheight = ihh * scale / 300 * 2.54; + return; } @@ -7014,7 +9621,7 @@ if (strstr(text,"file://")) // text is a filespec { - file = strdupz(text+7); // get rid of junk added by GTK + file = strdupz(text+7,0,"drag_drop"); // get rid of junk added by GTK cc = strlen(file); while (file[cc-1] < ' ') cc--; file[cc] = 0; @@ -7096,8 +9703,8 @@ if (err) return 0; if (S_ISDIR(statf.st_mode)) { // if directory, return folder image - bpath = zmalloc(500); - strncatv(bpath,499,zicondir,"/folder256.png",null); + bpath = zmalloc(500,"thumbnail"); + strncatv(bpath,499,zicondir,"/folder.png",null); thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,&gerror); zfree(bpath); return thumbpxb; @@ -7109,6 +9716,7 @@ // make a cursor from a graphic file in application's icon directory +// (see initz_appfiles()). GdkCursor * zmakecursor(cchar *iconfile) { @@ -7128,25 +9736,33 @@ pixbuf = gdk_pixbuf_new_from_file(iconpath,&gerror); if (pixbuf && display) cursor = gdk_cursor_new_from_pixbuf(display,pixbuf,0,0); - else printf("%s \n",gerror->message); + else printf("*** %s \n",gerror->message); return cursor; } -// move the mouse pointer to given position +// move the mouse pointer to given position in given window -int gdk_window_move_pointer(GdkWindow *window, int px, int py) // v.5.0 +int gdk_window_move_pointer(GdkWindow *window, int px, int py) // v.5.4 { - GdkDisplay *display; - GdkScreen *screen; - int rpx, rpy; + GdkDisplay *display; // X workstation (KB, mouse, screen) + GdkScreen *screen; // screen (monitor) + GdkDevice *mouse; // mouse + GdkDeviceManager *manager; // knows screen / mouse associations + int rpx, rpy; + zthreadcrash(); // thread usage not allowed + display = gdk_window_get_display(window); if (! display) return 0; screen = gdk_window_get_screen(window); if (! screen) return 0; + manager = gdk_display_get_device_manager(display); + if (! manager) return 0; + mouse = gdk_device_manager_get_client_pointer(manager); + gdk_window_get_root_coords(window,px,py,&rpx,&rpy); - gdk_display_warp_pointer(display,screen,rpx,rpy); + gdk_device_warp(mouse,screen,rpx,rpy); return 1; } @@ -7175,9 +9791,8 @@ Algorithm: create output pixbuf big enough for rotated input pixbuf compute coefficients for affine transform - loop all output pixels - get next output pixel (px2,py2) - convert to input pixel (px1,py1) using affine transform + loop all output pixels (px2,py2) + get corresp. input pixel (px1,py1) using affine transform if outside of pixmap output pixel = black continue @@ -7186,7 +9801,8 @@ sum RGB values * overlap output aggregate RGB to pixel (px2,py2) - v.5.2: calculations in float instead of double + Benchmark: rotate 7 megapixel image 10 degrees + 0.31 secs. 3.3 GHz Core i5 ***/ @@ -7297,6 +9913,38 @@ /************************************************************************** parameter management functions + + int initParmList(int maxparms) + int loadParms() + int loadParms(char *filename) + int saveParms() + int saveParms(char *filename) + int setParm(cchar *parmname, double parmval) + double getParm(cchar *parmname) + char * getParm(int Nth) + int listParms(GtkWidget *textWin) + int editParms(GtkWidget *textWin, int addp) + + initParms call before any other functions to initialize empty list + loadParms load parameters from a file - file selection dialog + loadParms load parameters from a file - filename argument + saveParms save parameters to a file - file selection dialog + saveParms save parameters to a file - filename argument + setParm sets the value of a parameter (created if not already defined) + getParm returns the value of a parameter by name (NAN if non-existent) + getParm returns the name of the Nth parameter (0 based), or null + listParms list all parameter names and values in text window + editParms edit the parameters with a GUI + + loadParms and saveParms use zgetfile() selection dialog + loadParms returns the count of parameters loaded + saveParms returns the count saved + + editParms() starts a dialog which shows all parameters and allows them to be edited. + The dialog has buttons to allow loading parameters from a file or saving them to a file. + If the argument textWin is non-zero, a button is added which lists all parameters to the window. + If the argument addp is non-zero, a button is added to allow new parameters to be defined. + ***************************************************************************/ struct t_parmlist { // parameter list in memory @@ -7307,16 +9955,20 @@ } parmlist; int parmlistvalid = 0; // flag -char zparmfile[maxfcc]; // last used parm file +char zparmfile[200]; // last used parm file // initialize parameter list - must be called first int initParmlist(int max) { + using namespace zfuncs; + + int err; + if (! parmlistvalid) { // start with default parms file - strcpy(zparmfile,get_zuserdir()); - strcat(zparmfile,"/parameters"); // /home/user/.appname/parameters + err = locale_filespec("user","parameters",zparmfile); + if (err) return 0; } if (parmlistvalid) { // delete old parms @@ -7345,8 +9997,8 @@ int np = loadParms("parameters"); if (! np) { saveParms("parameters"); - zmessageACK(null,ZTX("Initial parameters file created. \n" - "Inspect and revise if necessary.")); + zmessageACK(0,0,ZTX("Initial parameters file created. \n" + "Inspect and revise if necessary.")); } return np; } @@ -7394,7 +10046,7 @@ fid = fopen(pfile,"r"); if (! fid) return 0; // bad file - strncpy0(zparmfile,pfile,maxfcc-1); // set current parm file + strncpy0(zparmfile,pfile,200); // set current parm file while (true) // read file { @@ -7458,11 +10110,11 @@ fid = fopen(pfile,"w"); if (! fid) { - zmessageACK(null,ZTX("cannot open file %s"),pfile); + zmessageACK(null,null,ZTX("cannot open file %s"),pfile); return 0; } - strncpy0(zparmfile,pfile,999); + strncpy0(zparmfile,pfile,200); for (np = 0; np < parmlist.count; np++) fprintf(fid," \"%s\" %.12g \n",parmlist.name[np],parmlist.value[np]); @@ -7602,7 +10254,7 @@ gtk_entry_set_width_chars(GTK_ENTRY(peEdit[ii]),12); sprintf(ptemp,"%.12g",parmlist.value[ii]); gtk_entry_set_text(GTK_ENTRY(peEdit[ii]),ptemp); - peHbox[ii] = gtk_hbox_new(0,0); + peHbox[ii] = gtk_box_new(HORIZONTAL,0); gtk_box_pack_start(GTK_BOX(peHbox[ii]),peLabel[ii],0,0,5); gtk_box_pack_start(GTK_BOX(peHbox[ii]),peEdit[ii],0,0,5); vbox = gtk_dialog_get_content_area(GTK_DIALOG(peDialog)); @@ -7698,6 +10350,23 @@ /************************************************************************** xstring class (dynamic length string) + + xstring(int cc = 0) default constructor + xstring(cchar * ) string constructor + xstring(const xstring &) copy constructor + ~xstring() destructor + operator cchar * () const { return xpp; } conversion operator (cchar *) + xstring operator= (const xstring &) operator = + xstring operator= (cchar *) operator = + friend xstring operator+ (const xstring &, const xstring &) operator + (catenate) + friend xstring operator+ (const xstring &, cchar *) operator + + friend xstring operator+ (cchar *, const xstring &) operator + + void insert(int pos, cchar *string, int cc = 0) insert substring at pos (expand) + void overlay(int pos, cchar *string, int cc = 0) replace substring (possibly expand) + static void getStats(int & tcount2, int & tmem2) get statistics + void validate() const verify integrity + int getcc() const { return xcc; } return string length + ***************************************************************************/ #define wmiv 1648734981 @@ -7928,6 +10597,24 @@ /************************************************************************** Vxstring class (array or vector of xstring) + + Vxstring(int = 0); constructor + ~Vxstring(); destructor + Vxstring(const Vxstring &); copy constructor + Vxstring operator= (const Vxstring &); operator = + xstring & operator[] (int); operator [] + const xstring & operator[] (int) const; operator [] (const) + int search(cchar *string); find element in unsorted Vxstring + int bsearch(cchar *string); find element in sorted Vxstring + int sort(int nkeys, int keys[][3]); sort elements by designated subfields + int sort(int pos = 0, int cc = 0); sort elements by 1 subfield (cc 0 = all) + int getCount() const { return nd; } get current count + + Sort with keys: + keys[N][0] = key position (0 based) of key N + keys[N][1] = key length + keys[N][2] = sort type: 1/2 = ascending/descending, 3/4 = same, ignoring case + ***************************************************************************/ Vxstring::Vxstring(int ii) // constructor @@ -8139,6 +10826,22 @@ /************************************************************************** Hash Table class + + HashTab(int cc, int cap); constructor + ~HashTab(); destructor + int Add(cchar *string); add a new string + int Del(cchar *string); delete a string + int Find(cchar *string); find a string + int GetCount() { return count; } get string count + int GetNext(int & first, char *string); get first/next string + int Dump(); dump hash table to std. output + + constructor: cc = string length of table entries, cap = table capacity + cap should be set 30% higher than needed to reduce collisions and improve speed + + Benchmark: 0.056 usec. to find 19 char string in a table of 100,000 + which is 80% full. 3.3 GHz Core i5 + ***************************************************************************/ // static members (robust for tables up to 60% full) @@ -8297,6 +11000,22 @@ /************************************************************************** class for queue of dynamic strings + + Queue(int cap); create queue with capacity + ~Queue(); destroy queue + int getCount(); get current entry count + int push(const xstring *entry, double secs); add new entry with max. wait time + xstring *pop1(); get 1st entry (oldest) + xstring *popN(); get Nth entry (newest) + + constructor: cap is queue capacity + push: secs is max. time to wait if queue is full. + This makes sense if the queue is being pop'd from another thread. + Use zero otherwise. + + Execution time: 0.48 microsecs per push + pop on queue with 100 slots kept full. + (2.67 GHz Intel Core i7) + ***************************************************************************/ Queue::Queue(int cap) // constructor @@ -8427,8 +11146,58 @@ Store any amount of data at any depth within a tree-structure with named nodes. Data can be found using an ordered list of node names or node numbers. - Node numbers are in the sequence added using put() with names, - or the same as those numbers used in put() with numbers. + Tree(cchar *name); create Tree + ~Tree(); destroy Tree + int put(void *data, int dd, char *nodes[], int nn); put data by node names[] + int put(void *data, int dd, int nodes[], int nn); put data by node numbers[] + int get(void *data, int dd, char *nodes[], int nn); get data by node names[] + int get(void *data, int dd, int nodes[], int nn); get data by node numbers[] + + A Tree can also be thought of as an N-dimensional array with the cells or nodes + having both names and numbers. Data can be stored and retrieved with a list of node + names or numbers. The nodes are created as needed. Nodes are sparse: those with no data + do not exist. Node numbers are created when data is stored by node numbers. + Node numbers are also added when data is stored by node names: the numbers are assigned + sequentially from zero at each level in the tree. + + nodes array of node names or numbers + nn no. of nodes used for a put() or get() call + dd data length to put, or the max. data length to get (i.e. the space available) + put() returns 1 if successful and crashes with a message if not (out of memory) + get() returns the length of the data retrieved (<= dd) or 0 if not found + there is no assumption that the data is character data and no null is appended + data returned has the same length as the data stored (if dd arg is big enough) + + example: + char *snodes[10]; // up to 10 node names (max tree depth) + int knodes[10]; // up to 10 node numbers + char mydata[20]; // data length up to 20 + + Tree *mytree = new Tree("myname"); // create Tree + snodes[0] = "name1"; + snodes[1] = "name2"; + mytree->put("string1",8,snodes,2); // put "string1" at ["name1","name2"] + snodes[1] = "name3"; + mytree->put("string22",9,snodes,2); // put "string22" at ["name1","name3"] + snodes[1] = "name2"; + mytree->get(mydata,20,snodes,2); // get data at ["name1","name2"] ("string1") + knodes[0] = 0; + knodes[1] = 0; + mytree->get(mydata,20,knodes,2); // get data at [0,0] ("string1") + knodes[1] = 1; + mytree->get(mydata,20,knodes,2); // get data at [0,1] ("string22") + + When data was stored at ["name1","name2"] these node names were created along with the + corresponding node numbers [0,0]. When data was stored at ["name1","name3"] a new node + "name3" was created under the existing node "name1", and assigned the numbers [0,1]. + + Benchmark Execution times: + 2.67 GHz Intel Core i7 + Tree with 1 million nodes and average depth of 8 levels (peak 15 levels) + put() all data by node names: 2.1 secs + get() all data by node names: 1.5 secs + put() all data by node numbers: 2.0 secs + get() all data by node numbers: 0.72 secs Internal code conventions: - caller level is node 0, next level is node 1, etc. @@ -8439,7 +11208,6 @@ #define wmid 1374602859 // integrity check key - // constructor Tree::Tree(cchar *name) diff -Nru ukopp-4.4/zfuncs.h ukopp-4.7/zfuncs.h --- ukopp-4.4/zfuncs.h 2012-04-23 07:29:52.000000000 +0000 +++ ukopp-4.7/zfuncs.h 2013-02-27 22:11:30.000000000 +0000 @@ -1,8 +1,8 @@ /************************************************************************** zfuncs.h include file for zfuncs functions - Copyright 2006 2007 2008 2009 2010 2011 2012 Michael Cornelison - source URL: kornelix.squarespace.com + Copyright 2006 2007 2008 2009 2010 2011 2012 2013 Michael Cornelison + source URL: kornelix.com contact: kornelix2@googlemail.com This program is free software: you can redistribute it and/or modify @@ -20,7 +20,7 @@ ***************************************************************************/ -// zfuncs.h version v.5.2 +// zfuncs.h version v.5.5 #include #include @@ -41,6 +41,9 @@ #include #include +#define VERTICAL GTK_ORIENTATION_VERTICAL // GTK shortcuts +#define HORIZONTAL GTK_ORIENTATION_HORIZONTAL + #define int8 char // number types #define int16 short #define int32 long @@ -69,8 +72,9 @@ // system functions ====================================================== void apppause(); // output message and wait for user -void apppause(cchar * pMess, ... ); // same, works like printf -void zappcrash(cchar *pMessage, ...); // crash with popup message in text window +void apppause(cchar * format, ... ); // same, works like printf +void zpopup_message(cchar *format, ...); // popup message, thread safe +void zappcrash(cchar *format, ...); // crash with popup message in text window void catch_signals(); // catch signals and do backtrace dump void beroot(int argc = 0, char *argv[] = 0); // restart image as root if password OK @@ -82,6 +86,7 @@ double CPUtime(); // get elapsed process CPU time for main() double CPUtime2(); // " include all threads double jobtime(); // " include all threads + subprocesses +void compact_time(const time_t fileDT, char *compactDT); // time_t date/time to yyyymmddhhmmss int parseprocfile(cchar *pfile, cchar *pname, double *value, ...); // get data from /proc file int parseprocrec(char *prec, int field, double *value, ...); // get data from /proc file record void zsleep(double dsecs); // sleep specified seconds @@ -100,11 +105,15 @@ char * zmalloc(size_t bytes, cchar *tag = 0); // malloc() with auto crash and log option void zfree(void *buff); // free() with log option -void zmalloc_log(int count); // turn zmalloc() log on/off void zmalloc_report(); // print memory allocation report +int shell_quiet(cchar *command, ...); // format/run shell command, return status +int shell_ack(cchar *command, ...); // "" + popup an error message if error +int shell_asynch(cchar *command, ...); // start shell command, return immediately +int shell_asynch_status(int handle); // get status of asynch shell command char * command_output(int &contx, cchar *command, ...); // get shell command output int command_status(int contx); // get exit status of command +int command_kill(int contx); // kill command before completion int signalProc(cchar * pname, cchar * signal); // pause/resume/kill subprocess int runroot(cchar *sucomm, cchar *command); // run command as root via su or sudo int checkinstall(cchar *prog, ...); // check if programs are installed @@ -165,6 +174,41 @@ int utf8_check(cchar *string); // check utf8 string for encoding errors int utf8_position(cchar *utf8in, int Nth); // get byte position of Nth graphic char. +// number conversion ===================================================== + +int convSI (cchar *inp, int &inum, cchar **delm = 0); // string to int +int convSI (cchar *inp, int &inum, int low, int hi, cchar **delm = 0); // (with low/high limit checking) +int convSD (cchar *inp, double &dnum, cchar **delm = 0); // string to double +int convSD (cchar *inp, double &dnum, double low, double hi, cchar **delm = 0); // (with low/high limit checking) +int convSF (cchar *inp, float &fnum, cchar **delm = 0); // string to double +int convSF (cchar *inp, float &fnum, float low, float hi, cchar **delm = 0); // (with low/high limit checking) +int convIS (int iin, char *outp, int *cc = 0); // int to string, returned cc +int convDS (double din, int prec, char *outp, int *cc = 0); // double to string, precision, ret. cc +char * formatKBMB(double fnum, int prec); // format nnn B, nn.n KB, n.nn MB, etc. + +// wildcard functions ==================================================== + +int MatchWild(cchar * wildstr, cchar * str); // wildcard string match (match = 0) +int MatchWildIgnoreCase(cchar * wildstr, cchar * str); // wildcard string match, ignoring case +cchar * SearchWild(cchar *wpath, int &flag); // wildcard file search +cchar * SearchWildIgnoreCase(cchar *wpath, int &flag); // wildcard file search, ignoring case + +// search and sort functions ============================================= + +int bsearch(int seekint, int nn, int list[]); // binary search sorted list[nn] +int bsearch(char *seekrec, char *allrecs, int recl, int nrecs); // binary search sorted records +int bsearch(char *seekrec, char **allrecs, int N, int nrecs); // binary search of sorted pointers to recs + +typedef int HeapSortUcomp(cchar *rec1, cchar *rec2); // return -1/0/+1 if rec1 rec2 +void HeapSort(int vv[], int nn); // Heap Sort - integer +void HeapSort(float vv[], int nn); // Heap Sort - float +void HeapSort(double vv[], int nn); // Heap Sort - double +void HeapSort(char *vv[], int nn); // Heap Sort - char *, ascending order +void HeapSort(char *vv[], int nn, HeapSortUcomp); // Heap Sort - char *, user-defined order +void HeapSort(char *recs, int RL, int NR, HeapSortUcomp); // Heap Sort - records, user-defined order + +int MemSort(char * RECS, int RL, int NR, int KEYS[][3], int NK); // memory sort, records with multiple keys + // bitmap functions ====================================================== struct bitmap { @@ -197,39 +241,6 @@ char * pvlist_get(pvlist *pv, int Nth); // return Nth entry (0...) int pvlist_sort(pvlist *pv); // sort list, ascending -// number conversion ===================================================== - -int convSI (cchar *inp, int &inum, cchar **delm = 0); // string to int -int convSI (cchar *inp, int &inum, int low, int hi, cchar **delm = 0); // (with low/high limit checking) -int convSD (cchar *inp, double &dnum, cchar **delm = 0); // string to double -int convSD (cchar *inp, double &dnum, double low, double hi, cchar **delm = 0); // (with low/high limit checking) -int convSF (cchar *inp, float &fnum, cchar **delm = 0); // string to double -int convSF (cchar *inp, float &fnum, float low, float hi, cchar **delm = 0); // (with low/high limit checking) -int convIS (int iin, char *outp, int *cc = 0); // int to string, returned cc -int convDS (double din, int prec, char *outp, int *cc = 0); // double to string, precision, ret. cc -char * formatKBMB(double fnum, int prec); // format nnn B, nn.n KB, n.nn MB, etc. - -// wildcard functions ==================================================== - -int MatchWild(cchar * wildstr, cchar * str); // wildcard string match (0) -cchar * SearchWild(cchar *wpath, int &flag); // wildcard file search - -// search and sort functions ============================================= - -int bsearch(int seekint, int nn, int list[]); // binary search sorted list[nn] -int bsearch(char *seekrec, char *allrecs, int recl, int nrecs); // binary search sorted records -int bsearch(char *seekrec, char **allrecs, int N, int nrecs); // binary search of sorted pointers to recs - -typedef int HeapSortUcomp(cchar * rec1, cchar * rec2); // return -1/0/+1 if rec1 rec2 -void HeapSort(int vv[], int nn); // Heap Sort - integer -void HeapSort(float vv[], int nn); // Heap Sort - float -void HeapSort(double vv[], int nn); // Heap Sort - double -void HeapSort(char * vv[], int nn); // Heap Sort - char *, ascending order -void HeapSort(char * vv[], int nn, HeapSortUcomp); // Heap Sort - char *, user-defined order -void HeapSort(char * recs, int RL, int NR, HeapSortUcomp); // Heap Sort - records, user-defined order - -int MemSort(char * RECS, int RL, int NR, int KEYS[][3], int NK); // memory sort, records with multiple keys - // random number functions =============================================== int lrandz(int64 * seed); // returns 0 to 0x7fffffff @@ -254,15 +265,13 @@ cchar * get_zuserdir(); // get /home/user/.appname/ cchar * get_zdatadir(); // get install directory cchar * get_zdocdir(); // get document directory +int locale_filespec(cchar *ftype, cchar *fname, char *filespec); // get a locale dependent file v.5.5 void showz_userguide(cchar *context = 0); // show user guide in new process -void showz_readme(); // show README file in popup window -void showz_changelog(); // show changelog file in popup window void showz_logfile(); // show log file in popup window v.5.2 -void showz_translations(); // show TRANSLATIONS file in popup window -void showz_doctext(cchar *file); // show text file (or .gz) in a popup window -void zmake_menu_launcher(cchar *command, cchar *cats, cchar *generic); // add desktop menu and launcher v.4.1 +void showz_textfile(cchar *type, cchar *file); // show text file [.gz] in popup window void showz_html(cchar *url); // show html via preferred browser +void zmake_menu_launcher(cchar *command, cchar *cats, cchar *generic); // add desktop menu and launcher v.4.1 /************************************************************************** GTK utility functions @@ -272,9 +281,10 @@ void zthreadcrash(); // crash if thread is not main() thread // text window print and read utilities + void wprintx(GtkWidget *Win, int line, cchar *mess, cchar *font = 0); // write text to line, optional font -void wprintf(GtkWidget *Win, int line, cchar *mess, ...); // "printf" version -void wprintf(GtkWidget *Win, cchar *mess, ... ); // "printf" to next line, scroll up +void wprintf(GtkWidget *Win, int line, cchar *format, ...); // "printf" version +void wprintf(GtkWidget *Win, cchar *format, ... ); // "printf" to next line, scroll up void wscroll(GtkWidget *mLog, int line); // scroll window to put line on screen void wclear(GtkWidget *Win); // clear window void wclear(GtkWidget *Win, int line); // clear from line to end @@ -283,6 +293,13 @@ void wfilesave(GtkWidget *Win); // wfiledump() via file-chooser dialog void wprintp(GtkWidget *Win); // print text window to default printer +// intercept text window mouse click functions + +typedef void clickfunc_t(GtkWidget *widget, int &line, int &pos); // function to get clicked text position +void textwidget_set_clickfunc(GtkWidget *widget, clickfunc_t clickfunc); // (wrapped lines are one logical line) +char * textwidget_get_line(GtkWidget *widget, int line); // get entire line at clicked position +char * textwidget_get_word(char *line, int pos, cchar *dlims, char &end); // get delimited word at clicked position + /**************************************************************************/ // functions to simplify building menus, tool bars, status bars @@ -296,18 +313,56 @@ GtkWidget * create_menubar(GtkWidget *vbox); // create menubar in packing box GtkWidget * add_menubar_item(GtkWidget *mbar, cchar *mname, mtFunc func = 0); // add menu item to menubar -GtkWidget * add_submenu_item(GtkWidget *mitem, cchar *label, mtFunc func = 0); // add submenu item to menu item +GtkWidget * add_submenu_item(GtkWidget *mitem, cchar *subname, // add submenu item to menu item + mtFunc func = 0, cchar *mtip = 0); // with opt. function and popup tip + +GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize = 24); // toolbar in packing box (no vert gtk3) +GtkWidget * add_toolbar_button(GtkWidget *tbar, cchar *name, cchar *ttip, // add button with stock (gtk-quit) + cchar *icon, mtFunc func); // or custom icon (iconfile.png) + +GtkWidget * create_stbar(GtkWidget *vbox); // create status bar in packing box +int stbar_message(GtkWidget *stbar, cchar *message); // display message in status bar + +/**************************************************************************/ + +GtkWidget * create_popmenu(); // create an empty popup menu +GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, // add menu item to popup menu + mtFunc func, cchar *arg, cchar *mtip = 0); +void popup_menu(GtkWidget *, GtkWidget *popmenu); // pop-up menu at current mouse posn. + +/**************************************************************************/ -GtkWidget * create_popmenu(); // create an empty popup menu -GtkWidget * add_popmenu_item(GtkWidget *popmenu, cchar *mname, mtFunc func); // add menu item to popup menu -void popup_menu(GtkWidget *popmenu); // pop-up menu at current mouse position - -GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize = 24); // toolbar in packing box (no vert gtk3) -GtkWidget * add_toolbar_button(GtkWidget *tbar, cchar *label, cchar *tip, // add button with stock (gtk-quit) - cchar *icon, mtFunc func); // or custom icon (iconfile.png) +// user editable graphic menu in popup window +// menus can be added and arranged using the mouse -GtkWidget * create_stbar(GtkWidget *vbox); // create status bar in packing box -int stbar_message(GtkWidget *stbar, cchar *message); // display message in status bar +typedef void gmenuz_cbfunc(cchar *menu); // caller-supplied callback function +void gmenuz(GtkWidget *parent, cchar *ufile, gmenuz_cbfunc); // show window, handle mouse drag/click + +/**************************************************************************/ + +// create vertical menu/toolbar in vertical packing box // v.5.5 + +struct vbmenuent { // menu data from caller + cchar *name; // menu name + cchar *icon; // opt. icon file name + cchar *desc; // description (mouse hover popup) + mtFunc *func; // callback func (GtkWidget *, cchar *arg) + cchar *arg; // callback arg + GdkPixbuf *pixbuf1; // icon pixbuf or null + GdkPixbuf *pixbuf2; // darkened pixbuf (clicked) + int ypos, yhh; // layout position, extent +}; + +struct vbox_menu { + GtkWidget *vbox; // parent window + GtkWidget *layout; // drawing window + int mcount; // menu entry count + vbmenuent menu[100]; +}; + +vbox_menu *vbox_menu_new(GtkWidget *vbox); // create new menu in parent vbox +void vbox_menu_add(vbox_menu *vbm, cchar *name, cchar *icon, // add menu item with response function + cchar *desc, mtFunc func, cchar *arg); // function may be popup_menu() /**************************************************************************/ @@ -315,7 +370,7 @@ // widget types: dialog, hbox, vbox, hsep, vsep, frame, scrwin, label, entry, edit, radio, // check, button, togbutt, spin, combo, comboE, hscale, vscale, colorbutt -#define zdmaxwidgets 300 +#define zdmaxwidgets 100 // v.5.3 #define zdmaxbutts 10 #define zdsentinel 2138687954 @@ -340,8 +395,8 @@ int zstat; // dialog status (from completion button) int disabled; // widget signals/events are disabled int saveposn; // save and recall window position each use - int stopKB; // next KB event will be ignored - cchar *help_topic; // optional help topic + int saveinputs; // save and recall user inputs each use + int stopKB; // flag, next KB event will be ignored GtkWidget *parent; // parent window or null GtkWidget *compbutt[zdmaxbutts]; // dialog completion buttons zwidget widget[zdmaxwidgets]; // dialog widgets (EOF = type = 0) @@ -359,15 +414,14 @@ cchar *data, cchar *options); // "scc=nn|homog|expand|space=nn|wrap" GtkWidget * zdialog_widget(zdialog *zd, cchar *name); // GTK widget from zdialog widget name -int zdialog_set_group(zdialog *zd, cchar *radio1, ...); // set group ID for set of radio buttons int zdialog_resize(zdialog *zd, int width, int height); // set size > widget sizes int zdialog_put_data(zdialog *zd, cchar *name, cchar *data); // put data in widget (entry, spin ...) cchar * zdialog_get_data(zdialog *zd, cchar *name); // get widget data int zdialog_set_limits(zdialog *zd, cchar *name, double min, double max); // set new widget limits (spin, scale) -void zdialog_help(zdialog *zd, cchar *help_topic); // set help topic for zdialog typedef int zdialog_event(zdialog *zd, cchar *name); // widget event callback function int zdialog_run(zdialog *zd, zdialog_event = 0, cchar *posn = 0); // run dialog, handle events +void KBstate(int key, int state); // extern: pass KB events to main app int zdialog_send_event(zdialog *zd, cchar *event); // send an event to an active dialog int zdialog_send_response(zdialog *zd, int zstat); // complete a dialog, set status @@ -398,6 +452,10 @@ void zdialog_set_position(zdialog *zd, cchar *posn); // set initial/new zdialog window position void zdialog_save_position(zdialog *zd); // save zdialog window position +int zdialog_inputs(cchar *action); // load or save zdialog input fields +int zdialog_save_inputs(zdialog *zd); // save zdialog input fields when finished +int zdialog_restore_inputs(zdialog *zd); // restore zdialog inputs from prior use + /**************************************************************************/ // translation functions @@ -405,32 +463,33 @@ void ZTXinit(cchar *lang); // setup for message translation cchar *ZTX(cchar *english); // get translation for English message void ZTX_translation_start(GtkWidget *parent); // start online translation +int ZTXnewtranslations(); // new translations were added // write text to popup window, shell command to popup window -int write_popup_text(cchar *action, cchar *text = 0, int ww = 0, int hh = 0, GtkWidget *parent = 0); +GtkWidget * write_popup_text(cchar *action, cchar *text = 0, int ww = 0, int hh = 0, GtkWidget *parent = 0); int popup_command(cchar *command, int ww = 400, int hh = 300, GtkWidget *parent = 0); // popup message dialogs -void zmessageACK(GtkWidget *parent, cchar *pMess, ... ); // display message, wait for OK -void zmessLogACK(GtkWidget *parent, cchar *pMess, ... ); // same, with log to STDOUT -int zmessageYN(GtkWidget *parent, cchar *pMess, ... ); // display message, wait for YES or NO -void zmessage_help(GtkWidget *parent, cchar *topic, cchar *mess, ...); // message box, [help] > user guide topic -zdialog * zmessage_post(GtkWidget *parent, int secs, cchar *mess, ...); // display message until timeout or cancel -char * zdialog_text(GtkWidget *parent, cchar * title, cchar * initext); // get short text input from user -int zdialog_choose(cchar *title, GtkWidget *parent, cchar *mess, ...); // show message and return choice button +void zmessageACK(GtkWidget *parent, cchar *title, cchar *format, ... ); // display message, wait for OK +void zmessLogACK(GtkWidget *parent, cchar *format, ... ); // same, with log to STDOUT +int zmessageYN(GtkWidget *parent, cchar *format, ... ); // display message, wait for YES/NO +void zmessage_help(GtkWidget *parent, cchar *topic, cchar *format, ...); // message box, user guide topic +zdialog * zmessage_post(GtkWidget *parent, int secs, cchar *format, ...); // show message, timeout or cancel +char * zdialog_text(GtkWidget *parent, cchar * title, cchar * initext); // get short text input from user +int zdialog_choose(cchar *title, GtkWidget *parent, cchar *format, ...); // show message, return choice +void poptext_window(cchar *text, GtkWindow *pwin, int mx, int my, int secs = 0); // show popup text at window posn +void poptext_mouse(GdkDevice *mouse, cchar *text, int dx, int dy, int secs = 0); // show popup text at mouse posn // file chooser dialogs for one file or multiple files char * zgetfile1(cchar *title, cchar *action, cchar *file, cchar *butt = 0); char ** zgetfileN(cchar *title, cchar *action, cchar *file, cchar *butt = 0); -// print image file +// print an image file, choosing printer, paper, orientation, margins, and scale -void print_image_paper_setup(GtkWidget *parent); // paper setup for printing an image file -void print_image_margins_setup(GtkWidget *parent); // setup print margins -void print_image_file(cchar *imagefile); // print the image file +void print_image_file(GtkWidget *parent, cchar *imagefile); // drag and drop functions