# HG changeset patch
# User Rudolph Gutzerhagen <rudolph.gutzerhagen@gmail.com>
# Date 1611481781 18000
#      Sun Jan 24 04:49:41 2021 -0500
# Node ID cf925f4379458bd3f4750faf19ce22123b4e865c
# Parent  c0a9082a09259db344e5aa7eff824c37cc61e312
[mq]: stock-config-files-handling      #6079
    o stock-config files handling on program load to preserve any
      user changes to config files (move to archive directory) and
      allow copy in of freshest version of shipped stock config files.
    o enhance stock-config menu to handle access to archive directories.
      - also extended to the 'radio/import from stock config' menu.

Additional note on stock configuration files:
  In order to prevent overwriting user-copy stock config files
  with shipped versions, CHIRP skips the copy of those files
  which already exist in the user-copy directory, leaving any
  new data in shipped versions in the shipped stock config directory.
  This function examines each pair (shipped vs user copy) and if
  different, moves the user-copy stock config to an archive directory,
  thereby permitting CHIRP to copy the newer shipped version to the
  user-copy directory. This new archive retains any user-modified data.

diff --git a/chirp/ui/mainapp.py b/chirp/ui/mainapp.py
--- a/chirp/ui/mainapp.py
+++ b/chirp/ui/mainapp.py
@@ -26,6 +26,7 @@
 import gtk
 import gobject
 import sys
+import hashlib
 
 from chirp.ui import inputdialog, common
 from chirp import platform, directory, util
@@ -585,19 +586,164 @@
         basepath = platform.get_platform().find_resource("stock_configs")
 
         files = glob(os.path.join(basepath, "*.csv"))
+        dir_timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')
         for fn in files:
-            if os.path.exists(os.path.join(stock_dir, os.path.basename(fn))):
-                LOG.info("Skipping existing stock config")
-                continue
+            stock_file = os.path.join(stock_dir, os.path.basename(fn))
+            if os.path.exists(stock_file):
+                # if the stock file exists, check if same content
+                # if different copy existing to timestamp subdirectory
+                if self.compare_same_file_content(fn, stock_file):
+                    LOG.info("Skipping existing stock config %s" % (fn))
+                    continue
+                else:
+                    # copy existing to timestamp subdirectory
+                    self.archive_stock_file(stock_dir, stock_file,
+                                            dir_timestamp)
             try:
                 shutil.copy(fn, stock_dir)
-                LOG.debug("Copying %s -> %s" % (fn, stock_dir))
+                LOG.info("Copying %s -> %s" % (fn, stock_dir))
             except Exception, e:
                 LOG.error("Unable to copy %s to %s: %s" % (fn, stock_dir, e))
                 return False
         return True
 
+    def compare_same_file_content(self, file1, file2):
+        ''' Compare file content for two files, determine if same.
+            (str, str ) -> bool
+            @type: str
+            @type: str
+            @rtype: bool
+            returns True if files match
+            N.B. receives full path filenames
+        '''
+        READ_ONLY_BINARY = 'rb'
+
+        # determine if the stock config file already exists
+        # if not, we are done, indicate we handled it.
+        if not os.path.exists(file1):
+            return False
+        if not os.path.exists(file2):
+            return False
+
+        # calculate and compare checksums
+        checksum1 = ''
+        checksum2 = ''
+
+        with open(file1, READ_ONLY_BINARY) as f1:
+            f1_contents = f1.read()
+            checksum1 = hashlib.md5(f1_contents).hexdigest()
+
+        with open(file2, READ_ONLY_BINARY) as f2:
+            f2_contents = f2.read()
+            checksum2 = hashlib.md5(f2_contents).hexdigest()
+
+        # if the files are the same, we are done
+        # indicate we did not handle it
+        if checksum1 == checksum2:
+            return True
+        else:
+            return False
+
+    def archive_stock_file(self, stock_dir, stock_filename, dir_timestamp):
+        ''' receives full path filename for stock_filename
+            constructs archive directory from stock_dir and dir_timestamp
+        '''
+        # now check to see if the ageing directory exists
+        # if not, then create it
+        archive_directory = os.path.join(stock_dir, dir_timestamp)
+        if not os.path.isdir(archive_directory):
+            try:
+                os.mkdir(archive_directory)
+                LOG.info("Creating archive directory %s" % (archive_directory))
+            except Exception, e:
+                LOG.error("Unable to create archive directory %s"
+                          % (archive_directory))
+
+        # now try to move the stock config file to ageing
+        try:
+            shutil.move(os.path.join(stock_dir, stock_filename),
+                        archive_directory)
+            LOG.info("Move file to archive %s -> %s"
+                     % (stock_filename, archive_directory))
+        except Exception, e:
+            LOG.error("Unable to move file to archive %s -> %s"
+                      % (stock_filename, archive_directory))
+
     def update_stock_configs(self):
+        ''' Load shipped stock_configs to user area.
+            Add menu items for those user-area files
+            using previous version inner functions:
+              _do_import_action() // Radio : Import from stock config
+              _do_open_action()   // File : Open stock config
+            which are now rolled into:
+              _do_stock_action()  // add menu items for stock items
+        '''
+        def _do_stock_action(dir_entry, index_ref, isFile, isForImport):
+            ''' (str, int, bool, bool) -> None
+                dir_entry is full-path filename or directory
+            '''
+            if isForImport:
+                path = "/MenuBar/radio/stock"
+            else:
+                path = "/MenuBar/file/openstock"
+
+            widget_label = os.path.splitext(os.path.basename(dir_entry))[0]
+
+            if isFile:
+                widget_icon = ""
+                if isForImport:
+                    widget_action_name = "stock-%i" % index_ref
+                    widget_tip = (_("Import stock configuration") +
+                                  " {name}".format(name=widget_label))
+                else:
+                    widget_action_name = "openstock-%i" % index_ref
+                    widget_tip = (_("Open stock configuration") +
+                                  " {name}".format(name=widget_label))
+            else:
+                widget_icon = "gtk-directory"
+                if isForImport:
+                    widget_action_name = "stock-dir-%i" % index_ref
+                    widget_tip = (_("Import stock directory") +
+                                  " {name}".format(name=widget_label))
+
+                else:
+                    widget_action_name = "openstock-dir-%i" % index_ref
+                    widget_tip = (_("Open stock directory") +
+                                  " {name}".format(name=widget_label))
+
+            action = gtk.Action(widget_action_name,
+                                widget_label,
+                                widget_tip,
+                                widget_icon)
+
+            if isFile:
+                if isForImport:
+                    action.connect("activate", self.import_stock_config,
+                                   dir_entry)
+                else:
+                    action.connect("activate", lambda a, c:
+                                   self.do_open(c), dir_entry)
+            else:
+                if isForImport:
+                    # following probably should be coded (start_dir=c)
+                    action.connect("activate", lambda a, c:
+                                   self.do_import(start_dir=dir_entry),
+                                   dir_entry)
+                else:
+                    # following probably should be coded (start_dir=c)
+                    action.connect("activate", lambda a, c:
+                                   self.do_open(start_dir=dir_entry),
+                                   dir_entry)
+
+            mid = self.menu_uim.new_merge_id()
+            mid = self.menu_uim.add_ui(mid, path,
+                                       widget_label,
+                                       widget_action_name,
+                                       gtk.UI_MANAGER_MENUITEM,
+                                       False)
+            self.menu_ag.add_action(action)
+
+        # Load shipped stock_configs to user area.
         stock_dir = platform.get_platform().config_file("stock_configs")
         if not os.path.isdir(stock_dir):
             try:
@@ -608,42 +754,21 @@
         if not self.copy_shipped_stock_configs(stock_dir):
             return
 
-        def _do_import_action(config):
-            name = os.path.splitext(os.path.basename(config))[0]
-            action_name = "stock-%i" % configs.index(config)
-            path = "/MenuBar/radio/stock"
-            action = gtk.Action(action_name,
-                                name,
-                                _("Import stock "
-                                  "configuration {name}").format(name=name),
-                                "")
-            action.connect("activate", self.import_stock_config, config)
-            mid = self.menu_uim.new_merge_id()
-            mid = self.menu_uim.add_ui(mid, path,
-                                       action_name, action_name,
-                                       gtk.UI_MANAGER_MENUITEM, False)
-            self.menu_ag.add_action(action)
-
-        def _do_open_action(config):
-            name = os.path.splitext(os.path.basename(config))[0]
-            action_name = "openstock-%i" % configs.index(config)
-            path = "/MenuBar/file/openstock"
-            action = gtk.Action(action_name,
-                                name,
-                                _("Open stock "
-                                  "configuration {name}").format(name=name),
-                                "")
-            action.connect("activate", lambda a, c: self.do_open(c), config)
-            mid = self.menu_uim.new_merge_id()
-            mid = self.menu_uim.add_ui(mid, path,
-                                       action_name, action_name,
-                                       gtk.UI_MANAGER_MENUITEM, False)
-            self.menu_ag.add_action(action)
-
+        # process stock config files into menu
         configs = glob(os.path.join(stock_dir, "*.csv"))
         for config in configs:
-            _do_import_action(config)
-            _do_open_action(config)
+            index = configs.index(config)
+            _do_stock_action(config, index, True, True)
+            _do_stock_action(config, index, True, False)
+
+        # process stock config subdirectory entries into menu
+        subdir_index = 0
+        subdirs = sorted(glob(os.path.join(stock_dir, "*")), reverse=True)
+        for subdir in subdirs:
+            if os.path.isdir(subdir):
+                subdir_index += 1
+                _do_stock_action(subdir, subdir_index, False, True)
+                _do_stock_action(subdir, subdir_index, False, False)
 
     def _confirm_experimental(self, rclass):
         sql_key = "warn_experimental_%s" % directory.radio_class_id(rclass)
