# HG changeset patch # User Rudolph Gutzerhagen # 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)