From 26c55a48d0a21b647f93442eb17e6ffdd047acc0 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 14:51:57 -0400 Subject: [PATCH 01/25] Fix the JSON loading helper function name. Keeping the next diff cleaner. --- osm.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osm.py b/osm.py index d0a12c4..e564fe2 100755 --- a/osm.py +++ b/osm.py @@ -72,13 +72,21 @@ def init_argparse(): only_one_of.add_argument('--version', '-v', action='store_true', help='show version and exit') return parser -def safe_load_config(config_file): - '''Return the parsed JSON from config_file, or exit with an error message if open/parse fails.''' +def safe_read_contents(from_file): + '''Return the contents of from_file, or exit with an error message if open/read fails.''' try: - with open(config_file) as infile: - return json.load(infile) + return Path(from_file).read_text() except Exception as e: - print('Unable to load Obsidian config file:', config_file) + print('Unable to read file:', from_file) + print(e) + exit(-1) + +def safe_load_json(from_contents, source): + '''Return the parsed JSON from_contents, or exit with an error message if the parse fails.''' + try: + return json.loads(from_contents) + except Exception as e: + print('Unable to parse json from', source) print(e) exit(-1) @@ -100,7 +108,8 @@ def get_vault_paths(root_dir): The list is string version of the absolute paths for the the vaults. ''' root_dir = Path(root_dir) - obsidian = safe_load_config(root_dir / 'obsidian.json') + obsidian_config = root_dir / 'obsidian.json' + obsidian = safe_load_json(safe_read_contents(obsidian_config), f'Obsidian config file: {obsidian_config}') return sorted(user_vault_paths_from(obsidian, root_dir), key=str.lower) # It might be cleaner to have this defined after the functions it calls, From 1bf353ce2df70968b81f303555fd6b3302e0b927 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 17:52:28 -0400 Subject: [PATCH 02/25] Self-Documentation for future readers --- osm.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osm.py b/osm.py index e564fe2..24ce811 100755 --- a/osm.py +++ b/osm.py @@ -9,6 +9,38 @@ # ################################################################ +################################################################ +# +# DESIGN GOALS: +# +# As this is a conceptually simple script for copying Obsidian +# vault meta-data between vaults: +# 1. One file that can be copied to where-ever the user wants. +# 2. Default configuration that works out of the box. +# 3. Independent configurtion that the user can choose to set +# up, regardless of where this script lives or is run from. +# 4. Only use Batteries Included Python libraries so that no +# additional setup is needed to run this script. +# +# This script operates with two different "configuration files": +# 1. Our own (OSM) configuration: +# a) Where to find the Obsidian configuration. +# b) What parts of the vaults should be copied. +# 2. Obsidians configuration. +# a) The location of the Obsidian vaults and their +# individual vault-specifc configuration. +# +# CODE STRUCTURE: +# +# 1) Usual Python header materials +# 2) Global Variables +# 3) Generic utiltiy functions +# 4) Configuration functions +# 5) Action functions +# +################################################################ + + VERSION = 'v0.3.2' APPNAME = 'Obsidian Settings Manager' From b053f4a68e1caf20a87d3214ac729c369293cb66 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 17:57:07 -0400 Subject: [PATCH 03/25] Document code sections and move code around. No code changes, just adding comments and moving code. The diff is bad enough without having to look for a code change too. --- osm.py | 68 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/osm.py b/osm.py index 24ce811..d58d086 100755 --- a/osm.py +++ b/osm.py @@ -32,11 +32,14 @@ # # CODE STRUCTURE: # -# 1) Usual Python header materials -# 2) Global Variables -# 3) Generic utiltiy functions -# 4) Configuration functions -# 5) Action functions +# To keep this as a single runnable file, rather than having +# separate utility modules, the code is organized into sections: +# +# - Usual Python header materials +# - Global Variables +# - Generic utiltiy functions +# - Configuration functions +# - Action functions # ################################################################ @@ -54,6 +57,10 @@ import traceback from pathlib import Path +### +# Globals +### + DEFAULT_OBSIDIAN_ROOT = str(Path.home() / 'Library' / 'Application Support' / 'obsidian') OBSIDIAN_ROOT_DIR = os.getenv('OBSIDIAN_ROOT', DEFAULT_OBSIDIAN_ROOT) @@ -72,7 +79,6 @@ def datestring(): # Keep this in sync with the format returned by datestring() ISO_8601_GLOB = '*-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z' - VERBOSE = False DRY_RUN = False @@ -80,6 +86,10 @@ def datestring(): DIFF_CMD = '' '''When not '', it is set to the absolute path of the diff command to use.''' +### +# Generic Utility Functions +### + def verbose(*args, **kwargs): '''Print parameters if VERBOSE flag is True or DRY_RUN is True.''' if DRY_RUN: @@ -87,23 +97,6 @@ def verbose(*args, **kwargs): elif VERBOSE: print(*args, **kwargs) -def init_argparse(): - '''Return an initialized command line parser.''' - parser = argparse.ArgumentParser(description='Manage Obsidian settings across multiple vaults.') - parser.add_argument('--verbose', action='store_true', help='Print what the file system operations are happening') - parser.add_argument('--dry-run', '-n', action='store_true', help='Do a dry-run. Show what would be done, without doing it.') - parser.add_argument('--root', default=OBSIDIAN_ROOT_DIR, help=f'Use an alternative Obsidian Root Directory (default {OBSIDIAN_ROOT_DIR!r})') - only_one_of = parser.add_mutually_exclusive_group(required=True) - only_one_of.add_argument('--list', '-l', action='store_true', help='list Obsidian vaults') - only_one_of.add_argument('--update', '-u', help='update Obsidian vaults from UPDATE vault') - only_one_of.add_argument('--exact-copy-of', help='delete and recreate Obsidian vaults with an exact copy of the EXACT_COPY_OF vault') - only_one_of.add_argument('--diff-to', '-d', help='Like update but instead of copying, just show a diff against DIFF_TO instead (no changes made).') - only_one_of.add_argument('--execute', '-x', help='run EXECUTE command within each vault (use caution!)') - only_one_of.add_argument('--backup-list', action='store_true', help='list ISO 8601-formatted .obsidian backup files from all vaults') - only_one_of.add_argument('--backup-remove', action='store_true', help='remove ISO 8601-formatted .obsidian backup files from all vaults') - only_one_of.add_argument('--version', '-v', action='store_true', help='show version and exit') - return parser - def safe_read_contents(from_file): '''Return the contents of from_file, or exit with an error message if open/read fails.''' try: @@ -126,6 +119,27 @@ def is_user_path(root_dir, path_to_test): '''Return True if path_to_test is a user's path, not an Obsidian system path (such as Help, etc)''' return Path(path_to_test).parent != root_dir +def init_argparse(): + '''Return an initialized command line parser.''' + parser = argparse.ArgumentParser(description='Manage Obsidian settings across multiple vaults.') + parser.add_argument('--verbose', action='store_true', help='Print what the file system operations are happening') + parser.add_argument('--dry-run', '-n', action='store_true', help='Do a dry-run. Show what would be done, without doing it.') + parser.add_argument('--root', default=OBSIDIAN_ROOT_DIR, help=f'Use an alternative Obsidian Root Directory (default {OBSIDIAN_ROOT_DIR!r})') + only_one_of = parser.add_mutually_exclusive_group(required=True) + only_one_of.add_argument('--list', '-l', action='store_true', help='list Obsidian vaults') + only_one_of.add_argument('--update', '-u', help='update Obsidian vaults from UPDATE vault') + only_one_of.add_argument('--exact-copy-of', help='delete and recreate Obsidian vaults with an exact copy of the EXACT_COPY_OF vault') + only_one_of.add_argument('--diff-to', '-d', help='Like update but instead of copying, just show a diff against DIFF_TO instead (no changes made).') + only_one_of.add_argument('--execute', '-x', help='run EXECUTE command within each vault (use caution!)') + only_one_of.add_argument('--backup-list', action='store_true', help='list ISO 8601-formatted .obsidian backup files from all vaults') + only_one_of.add_argument('--backup-remove', action='store_true', help='remove ISO 8601-formatted .obsidian backup files from all vaults') + only_one_of.add_argument('--version', '-v', action='store_true', help='show version and exit') + return parser + +### +# Configuration Functions +### + def user_vault_paths_from(obsidian, root_dir): '''Return the paths for each vault in obsidian that isn't a system vault.''' # The vaults' dictionary's keys aren't of any use/interest to us, @@ -163,6 +177,10 @@ def call_for_each_vault(vault_paths, operation, *args, **kwargs): for vault_path in vault_paths: operation(vault_path, *args, **kwargs) +### +# Action Functions +### + def backup(item, suffix): '''Rename item to have the given suffix.''' backup = str(item)+suffix @@ -313,6 +331,10 @@ def show_vault_path(vault_path): '''Print the vault path relative to the user's home directory (more readable).''' print(Path(vault_path).relative_to(Path.home())) +### +# MAIN +### + def main(): argparser = init_argparse() args = argparser.parse_args() From 9e2aa6a5e38497901d8542e6c9a23a2df536410d Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 14:53:07 -0400 Subject: [PATCH 04/25] Convert to JSON for configuration, add command line switch. Next commits will implement based on comments/discussion from: https://github.com/peterkaminski/obsidian-settings-manager/issues/11 --- osm.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/osm.py b/osm.py index d58d086..678d980 100755 --- a/osm.py +++ b/osm.py @@ -61,16 +61,38 @@ # Globals ### -DEFAULT_OBSIDIAN_ROOT = str(Path.home() / 'Library' / 'Application Support' / 'obsidian') -OBSIDIAN_ROOT_DIR = os.getenv('OBSIDIAN_ROOT', DEFAULT_OBSIDIAN_ROOT) - -ITEMS_TO_COPY = [ - 'config', - 'starred.json', - 'README.md', # used for vaults distributed to others via git - 'plugins', - 'snippets', -] +OSM_CONFIG_FILE = "osm.config" +OSM_CONFIG_ENV_VAR_OVERRIDE = "OSM_CONFIG_FILE" +OSM_CONFIG = {} # Will be replaced (using .update()) by the config data we end up loading. +OSM_DEFAULT_CONFIG = ''' +{ + "obsidian_config": { + "config_file": "obsidian.json", + "search_path": [ + "/Users//Library/Application Support/obsidian", + "/home//.config/obsidian", + "/home//.var/app/md.obsidian.Obsidian/config/obsidian", + "C:\\Users\\\\AppData\\obsidian" + ], + "config_file_override": null + }, + "files_to_copy": [ + { "copy": "README.md" }, + { "copy": "config" }, + { "copy": "*.json" }, + { "skip": "app.json" }, + { "skip": "core-plugins**" }, + { "skip": "workspace*" }, + { "skip": "command-palette.json" }, + + { "copy": "plugins" }, + { "skip": "plugins/auto-note-mover" }, + + { "copy": "snippets" }, + { "copy": "themes" } + ] +} +''' def datestring(): '''Return the current date and time in UTC string format.''' From 1c41f857322babf4668d355427e7d31556683def Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 15:11:20 -0400 Subject: [PATCH 05/25] Load our own (OSM) config file or use internal default contents --- osm.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 678d980..2d36ece 100755 --- a/osm.py +++ b/osm.py @@ -159,7 +159,32 @@ def init_argparse(): return parser ### -# Configuration Functions +# Configuration Functions - OSM +### + +def find_osm_config_file(): + '''No configuration file was given, so let's go looking and return the first one in our priority list!''' + env_var_value = os.getenv(OSM_CONFIG_ENV_VAR_OVERRIDE) + if env_var_value: + return Path(env_var_value) + local_config = Path(OSM_CONFIG_FILE) + if local_config.is_file(): + return local_config + home_config = Path.home() / OSM_CONFIG_FILE + if home_config.is_file(): + return home_config + return None + +def load_osm_config(config_file=None): + '''Load our OSM configuration from the config_file if given, or from our hierarchy of places to look.''' + config_file = config_file or find_osm_config_file() + if config_file: + OSM_CONFIG.update(safe_load_json(safe_read_contents(config_file), f'Config file: {config_file}')) + else: + OSM_CONFIG.update(safe_load_json(OSM_DEFAULT_CONFIG, 'Built-in configuration data')) + +### +# Configuration Functions - Obsidian ### def user_vault_paths_from(obsidian, root_dir): From 05a8cb14bee006a81293ccebd77dd602186f3e9b Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 15:41:28 -0400 Subject: [PATCH 06/25] Fix bug with quoting strings. The encoded JSON needs to be a raw string to prevent backslash quoting explosion --- osm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 2d36ece..a81eb75 100755 --- a/osm.py +++ b/osm.py @@ -64,7 +64,7 @@ OSM_CONFIG_FILE = "osm.config" OSM_CONFIG_ENV_VAR_OVERRIDE = "OSM_CONFIG_FILE" OSM_CONFIG = {} # Will be replaced (using .update()) by the config data we end up loading. -OSM_DEFAULT_CONFIG = ''' +OSM_DEFAULT_CONFIG = r''' { "obsidian_config": { "config_file": "obsidian.json", From 7bc10510871727fc4ef81d701fd8727001edd083 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 18:23:45 -0400 Subject: [PATCH 07/25] Load the OSM config file. Added primitive tracing to help when things do work as expected. Very preliminary implmentation. ENV VARs are a quick way to set values before the command line is parsed but aren't as user friendly. --- osm.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osm.py b/osm.py index a81eb75..a928737 100755 --- a/osm.py +++ b/osm.py @@ -61,8 +61,8 @@ # Globals ### -OSM_CONFIG_FILE = "osm.config" -OSM_CONFIG_ENV_VAR_OVERRIDE = "OSM_CONFIG_FILE" +OSM_CONFIG_FILE = 'osm.config' +OSM_CONFIG_ENV_VAR_OVERRIDE = 'OSM_CONFIG_FILE' OSM_CONFIG = {} # Will be replaced (using .update()) by the config data we end up loading. OSM_DEFAULT_CONFIG = r''' { @@ -102,9 +102,11 @@ def datestring(): ISO_8601_GLOB = '*-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z' VERBOSE = False - DRY_RUN = False +# Various 'what went wrong' controls a user can use to see why OSM isn't doing what they expected. +CONFIG_TRACE = os.getenv('CONFIG_TRACE', False) + DIFF_CMD = '' '''When not '', it is set to the absolute path of the diff command to use.''' @@ -112,6 +114,11 @@ def datestring(): # Generic Utility Functions ### +def config_trace(*args, **kwargs): + '''Print parameters if CONFIG_TRACE flag is True.''' + if CONFIG_TRACE: + print(*args, **kwargs) + def verbose(*args, **kwargs): '''Print parameters if VERBOSE flag is True or DRY_RUN is True.''' if DRY_RUN: @@ -146,7 +153,7 @@ def init_argparse(): parser = argparse.ArgumentParser(description='Manage Obsidian settings across multiple vaults.') parser.add_argument('--verbose', action='store_true', help='Print what the file system operations are happening') parser.add_argument('--dry-run', '-n', action='store_true', help='Do a dry-run. Show what would be done, without doing it.') - parser.add_argument('--root', default=OBSIDIAN_ROOT_DIR, help=f'Use an alternative Obsidian Root Directory (default {OBSIDIAN_ROOT_DIR!r})') + parser.add_argument('--config', '-c', help="Use CONFIG as the OSM config file instead of the default") only_one_of = parser.add_mutually_exclusive_group(required=True) only_one_of.add_argument('--list', '-l', action='store_true', help='list Obsidian vaults') only_one_of.add_argument('--update', '-u', help='update Obsidian vaults from UPDATE vault') @@ -163,24 +170,33 @@ def init_argparse(): ### def find_osm_config_file(): - '''No configuration file was given, so let's go looking and return the first one in our priority list!''' + '''Return the first OSM config file we find from our priority list, or None.''' env_var_value = os.getenv(OSM_CONFIG_ENV_VAR_OVERRIDE) + config_trace(f'OSM Config checking environment variable: {env_var_value!r}') if env_var_value: return Path(env_var_value) + local_config = Path(OSM_CONFIG_FILE) + config_trace(f'OSM Config checking local directory: {local_config}') if local_config.is_file(): return local_config + home_config = Path.home() / OSM_CONFIG_FILE + config_trace(f'OSM Config checking home directory: {home_config}') if home_config.is_file(): return home_config + + config_trace(f'OSM Config not found') return None def load_osm_config(config_file=None): '''Load our OSM configuration from the config_file if given, or from our hierarchy of places to look.''' config_file = config_file or find_osm_config_file() if config_file: + config_trace(f'Loading OSM configuration from: {config_file}') OSM_CONFIG.update(safe_load_json(safe_read_contents(config_file), f'Config file: {config_file}')) else: + config_trace('Loading OSM configuration from internal default configuration') OSM_CONFIG.update(safe_load_json(OSM_DEFAULT_CONFIG, 'Built-in configuration data')) ### @@ -401,6 +417,8 @@ def main(): print("Error: Cannot locate the 'diff' command, aborting.") exit(-1) + load_osm_config() + try: vault_paths = get_vault_paths(args.root) From 07a8b757f488e4bf602013e3bbd21fb994cc4db3 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 21:02:07 -0400 Subject: [PATCH 08/25] Be persnickety about the structure of the config files. --- osm.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osm.py b/osm.py index a928737..36c7aa9 100755 --- a/osm.py +++ b/osm.py @@ -126,6 +126,14 @@ def verbose(*args, **kwargs): elif VERBOSE: print(*args, **kwargs) +def must_get_key(a_dict, key, aux_msg): + '''Return the value of key from a_dict, or print an descriptive error message and exit.''' + try: + return a_dict[key] + except Exception: + print(f'Error, missing key {key!r} {aux_msg}') + exit(-1) + def safe_read_contents(from_file): '''Return the contents of from_file, or exit with an error message if open/read fails.''' try: From f61377638a79d1497dd37511a271de0f765ad48f Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 21:03:27 -0400 Subject: [PATCH 09/25] Look for the Obsidian config file and load it. --- osm.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 36c7aa9..014cbfb 100755 --- a/osm.py +++ b/osm.py @@ -94,6 +94,8 @@ } ''' +OBSIDIAN_CONFIG = {} # Will be .updated() with the obsidian configuration file data. + def datestring(): '''Return the current date and time in UTC string format.''' return f'-{datetime.datetime.utcnow().isoformat()}Z' @@ -194,7 +196,7 @@ def find_osm_config_file(): if home_config.is_file(): return home_config - config_trace(f'OSM Config not found') + config_trace('OSM Config not found') return None def load_osm_config(config_file=None): @@ -211,6 +213,36 @@ def load_osm_config(config_file=None): # Configuration Functions - Obsidian ### +def find_obsidian_config_file(): + '''Return the Path for the Obsidian config file from our configuration, or print an error message and exit if not found.''' + obsidian_config = must_get_key(OSM_CONFIG, 'obsidian_config', 'from top level OSM configuration file') + config_file_name = obsidian_config.get('config_file_override') + if config_file_name: + config_file = Path(config_file_name) + if config_file.is_file(): + return config_file + print(f'Unable to find Obsidian configuration file: {config_file_name!r}') + print("from the 'config_file_override' configuration value.") + exit(-1) + config_file_base_name = must_get_key(obsidian_config, 'config_file', 'from the obsdian_config part of the OSM configuration file') + config_file_search_path = must_get_key(obsidian_config, 'search_path', 'from the obsdian_config part of the OSM configuration file') + username = os.getlogin() + checked_files = [] + for a_dir in config_file_search_path: + candidate = Path(a_dir.replace("", username)) / config_file_base_name + checked_files.append(candidate) + if candidate.is_file(): + config_trace('Found Obsidian config file:', candidate) + return candidate + print("Error, could not locate Obsidian configuration file after checking all of:") + print("\n".join(map(str, checked_files))) + exit(-1) + +def load_obsidian_config(): + '''Find and load the obsidian config file after having loaded our own config file.''' + config_file = find_obsidian_config_file() + OBSIDIAN_CONFIG.update(safe_load_json(safe_read_contents(config_file), f'Obsidian config file: {config_file}')) + def user_vault_paths_from(obsidian, root_dir): '''Return the paths for each vault in obsidian that isn't a system vault.''' # The vaults' dictionary's keys aren't of any use/interest to us, @@ -426,6 +458,7 @@ def main(): exit(-1) load_osm_config() + load_obsidian_config() try: vault_paths = get_vault_paths(args.root) From 4d6a38ed04bd742e6eba7ee977ffd88490006aac Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 21:39:30 -0400 Subject: [PATCH 10/25] Load the Obsidian Vault paths. This also involved tracking the Obsidian Root dirctory since it is no longer a parameter or env var to the script --- osm.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osm.py b/osm.py index 014cbfb..05fd072 100755 --- a/osm.py +++ b/osm.py @@ -64,6 +64,8 @@ OSM_CONFIG_FILE = 'osm.config' OSM_CONFIG_ENV_VAR_OVERRIDE = 'OSM_CONFIG_FILE' OSM_CONFIG = {} # Will be replaced (using .update()) by the config data we end up loading. +# NOTE: Documented keys in the OSM Config should not start with an underscore (_), +# as the running code will add new keys with that prefix (using helper functions). OSM_DEFAULT_CONFIG = r''' { "obsidian_config": { @@ -179,6 +181,14 @@ def init_argparse(): # Configuration Functions - OSM ### +def remember_obsidian_root_dir(value): + '''Keep track of where the Obsidian root directory was by adding it to our config.''' + OSM_CONFIG['_obsidian_root_dir'] = value + +def get_obsidian_root_dir(): + '''Return the Obsidian root directory saved in our config.''' + return OSM_CONFIG['_obsidian_root_dir'] + def find_osm_config_file(): '''Return the first OSM config file we find from our priority list, or None.''' env_var_value = os.getenv(OSM_CONFIG_ENV_VAR_OVERRIDE) @@ -241,25 +251,23 @@ def find_obsidian_config_file(): def load_obsidian_config(): '''Find and load the obsidian config file after having loaded our own config file.''' config_file = find_obsidian_config_file() + remember_obsidian_root_dir(config_file.parent) OBSIDIAN_CONFIG.update(safe_load_json(safe_read_contents(config_file), f'Obsidian config file: {config_file}')) -def user_vault_paths_from(obsidian, root_dir): +def user_vault_paths(root_dir): '''Return the paths for each vault in obsidian that isn't a system vault.''' # The vaults' dictionary's keys aren't of any use/interest to us, # so we only need to look the path defined in the vault. - return [vault_data['path'] for vault_data in obsidian['vaults'].values() + return [vault_data['path'] for vault_data in OBSIDIAN_CONFIG['vaults'].values() if is_user_path(root_dir, vault_data['path'])] -def get_vault_paths(root_dir): +def get_vault_paths(): ''' Return a list of all the vault paths Obsidian is tracking. The list is string version of the absolute paths for the the vaults. ''' - root_dir = Path(root_dir) - obsidian_config = root_dir / 'obsidian.json' - obsidian = safe_load_json(safe_read_contents(obsidian_config), f'Obsidian config file: {obsidian_config}') - return sorted(user_vault_paths_from(obsidian, root_dir), key=str.lower) + return sorted(user_vault_paths(get_obsidian_root_dir()), key=str.lower) # It might be cleaner to have this defined after the functions it calls, # but keeping it close to get_vault_paths to make it easier to track changes if needed. @@ -461,7 +469,7 @@ def main(): load_obsidian_config() try: - vault_paths = get_vault_paths(args.root) + vault_paths = get_vault_paths() if args.version: print(f'{APPNAME} {VERSION}') From f8665d0bcbc759048ea90ed50cc430e0c7f48e1c Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 21:51:33 -0400 Subject: [PATCH 11/25] Add option to print the default configuration. Also, hoist the code for version and config printing to above where the configs are loaded. They don't depend on it, and this makes it easier to bootstrap a custom configuration. --- osm.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osm.py b/osm.py index 05fd072..a73462c 100755 --- a/osm.py +++ b/osm.py @@ -174,6 +174,7 @@ def init_argparse(): only_one_of.add_argument('--execute', '-x', help='run EXECUTE command within each vault (use caution!)') only_one_of.add_argument('--backup-list', action='store_true', help='list ISO 8601-formatted .obsidian backup files from all vaults') only_one_of.add_argument('--backup-remove', action='store_true', help='remove ISO 8601-formatted .obsidian backup files from all vaults') + only_one_of.add_argument('--print-default-config', action='store_true', help='print the default configuration and extt') only_one_of.add_argument('--version', '-v', action='store_true', help='show version and exit') return parser @@ -292,6 +293,10 @@ def call_for_each_vault(vault_paths, operation, *args, **kwargs): # Action Functions ### +def print_default_config(): + '''Print the default configuration so user can save and customize.''' + print(OSM_DEFAULT_CONFIG) + def backup(item, suffix): '''Rename item to have the given suffix.''' backup = str(item)+suffix @@ -458,6 +463,14 @@ def main(): global DRY_RUN DRY_RUN = True + if args.version: + print(f'{APPNAME} {VERSION}') + return + + if args.print_default_config: + print_default_config() + return + if args.diff_to: global DIFF_CMD DIFF_CMD = shutil.which('diff') @@ -471,9 +484,7 @@ def main(): try: vault_paths = get_vault_paths() - if args.version: - print(f'{APPNAME} {VERSION}') - elif args.list: + if args.list: call_for_each_vault(vault_paths, show_vault_path) elif args.update: ensure_valid_vault(vault_paths, args.update) From d9bea3d42fa48a5100e8cb0ea7e5d113dd4a0560 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 21:54:28 -0400 Subject: [PATCH 12/25] Allow the obsidian config file override to use the `~` short hand. --- osm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osm.py b/osm.py index a73462c..eb9253e 100755 --- a/osm.py +++ b/osm.py @@ -195,7 +195,7 @@ def find_osm_config_file(): env_var_value = os.getenv(OSM_CONFIG_ENV_VAR_OVERRIDE) config_trace(f'OSM Config checking environment variable: {env_var_value!r}') if env_var_value: - return Path(env_var_value) + return Path(env_var_value).expanduser() local_config = Path(OSM_CONFIG_FILE) config_trace(f'OSM Config checking local directory: {local_config}') @@ -229,7 +229,7 @@ def find_obsidian_config_file(): obsidian_config = must_get_key(OSM_CONFIG, 'obsidian_config', 'from top level OSM configuration file') config_file_name = obsidian_config.get('config_file_override') if config_file_name: - config_file = Path(config_file_name) + config_file = Path(config_file_name).expanduser() if config_file.is_file(): return config_file print(f'Unable to find Obsidian configuration file: {config_file_name!r}') From 9e4a633c6483247db3ea205fa5299961678ce7d5 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 8 Jul 2023 21:59:39 -0400 Subject: [PATCH 13/25] Misc fixes: Actually use value of --config parameter, trace where obsidian config is loading from. --- osm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osm.py b/osm.py index eb9253e..141f0a6 100755 --- a/osm.py +++ b/osm.py @@ -252,6 +252,7 @@ def find_obsidian_config_file(): def load_obsidian_config(): '''Find and load the obsidian config file after having loaded our own config file.''' config_file = find_obsidian_config_file() + config_trace("Loading Obsidian configuration from", config_file) remember_obsidian_root_dir(config_file.parent) OBSIDIAN_CONFIG.update(safe_load_json(safe_read_contents(config_file), f'Obsidian config file: {config_file}')) @@ -478,7 +479,7 @@ def main(): print("Error: Cannot locate the 'diff' command, aborting.") exit(-1) - load_osm_config() + load_osm_config(args.config) load_obsidian_config() try: From 9414ebdcd3c895a2e1c11979f8f940bf1c951158 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sun, 9 Jul 2023 08:16:36 -0400 Subject: [PATCH 14/25] New helper for checking types in the config files. Helps us to be more careful about using the config. Usages to follow in the next commits. --- osm.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 141f0a6..6577950 100755 --- a/osm.py +++ b/osm.py @@ -137,7 +137,15 @@ def must_get_key(a_dict, key, aux_msg): except Exception: print(f'Error, missing key {key!r} {aux_msg}') exit(-1) - + +def must_be_type(item, type_, prefix_msg): + '''If item is an instance of type_, return it for convenience, or print a descriptive error message and exit.''' + if isinstance(item, type_): + return item + print(f'Error: {prefix_msg} {type(item).__name__}: {item!r} is not a {type_.__name__}') + exit(-1) + + def safe_read_contents(from_file): '''Return the contents of from_file, or exit with an error message if open/read fails.''' try: From 417559797f1f684236c59b7aad7424666b417085 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sun, 9 Jul 2023 09:15:10 -0400 Subject: [PATCH 15/25] Parse the files_to_copy list into a sequence of actions. Next commits will implement finding the files in a specific vault. --- osm.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/osm.py b/osm.py index 6577950..10239f1 100755 --- a/osm.py +++ b/osm.py @@ -41,6 +41,20 @@ # - Configuration functions # - Action functions # +# DATA STRUCTURES +# +# Configurations: +# - JSON converted to Python. +# Files To Copy: +# - A list of actions and their items (see the README) +# - A set of files to copy(or diff) is constructed from +# sequentially processing the actions in the configuration. +# - Each action is mapped to a set function to be applied +# to the resulting list of files derived from the items. +# - The items indicate where to find the desired files, +# with the actual list of files depending on the contents of +# the source vault at the time of examination. +# ################################################################ @@ -114,6 +128,17 @@ def datestring(): DIFF_CMD = '' '''When not '', it is set to the absolute path of the diff command to use.''' +# Map "files_to_copy" actions to functions for managing the set of files to operate on. +FILE_ACTIONS = { + "copy": set.add, + "skip": set.discard +} + +# Dedup and condense some error message strings. +# Good for humans to read, but clutters up code when they're inline. +BAD_ACTION_MSG = 'OSM Configuration action(key) in "files_to_copy" list:' +BAD_ITEM_MSG = 'OSM Configuration item (value) in "files_to_copy" list:' + ### # Generic Utility Functions ### @@ -228,6 +253,27 @@ def load_osm_config(config_file=None): config_trace('Loading OSM configuration from internal default configuration') OSM_CONFIG.update(safe_load_json(OSM_DEFAULT_CONFIG, 'Built-in configuration data')) +def get_processing_directives(): + ''' + Yield a series of (function, item) pairs from the files to copy from a source vault. + + If there are any errors in the config file, print a descriptive error message and exit. + Each returned function should be called with a set and a filename. + ''' + directives = must_get_key(OSM_CONFIG, 'files_to_copy', 'in OSM configuration') + must_be_type(directives, list, 'OSM Configuration key "files_to_copy"') + + for directive in directives: + must_be_type(directive, dict, 'OSM Configuration value under "files_to_copy"') + # Having more than one action type per step seems confusing, so limit to just 1. + if len(directive) != 1: + print(f'Error: OSM Configuration list elements under "files_to_copy" must have only one key: {directive!r}') + exit(-1) + for action, item in directive.items(): # Easiest way to access the contents even when there is only one key/value pair. + # Actions must be strings to pass JSON parsing, not checking their type here. + operation = must_get_key(FILE_ACTIONS, action, BAD_ACTION_MSG) + yield (operation, must_be_type(item, str, BAD_ITEM_MSG)) + ### # Configuration Functions - Obsidian ### @@ -492,6 +538,7 @@ def main(): try: vault_paths = get_vault_paths() + directives = list(get_processing_directives()) if args.list: call_for_each_vault(vault_paths, show_vault_path) From c9dc3eed0f83b93269ac9f048ee7147d3df6221f Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sun, 9 Jul 2023 09:40:04 -0400 Subject: [PATCH 16/25] Wordsmithing on the bad actions message. Probably needs more work too. --- osm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 10239f1..05e11c2 100755 --- a/osm.py +++ b/osm.py @@ -136,7 +136,7 @@ def datestring(): # Dedup and condense some error message strings. # Good for humans to read, but clutters up code when they're inline. -BAD_ACTION_MSG = 'OSM Configuration action(key) in "files_to_copy" list:' +BAD_ACTION_MSG = 'is an unknown/invalid action in OSM Configuration in the "files_to_copy" list' BAD_ITEM_MSG = 'OSM Configuration item (value) in "files_to_copy" list:' ### From 2f4f4edaf27e912c5aa6028148113dd67c4c89c8 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sun, 9 Jul 2023 10:08:15 -0400 Subject: [PATCH 17/25] Implement the file action matching functions. --- osm.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/osm.py b/osm.py index 05e11c2..9039e2c 100755 --- a/osm.py +++ b/osm.py @@ -112,6 +112,9 @@ OBSIDIAN_CONFIG = {} # Will be .updated() with the obsidian configuration file data. +ACTIONS_TO_TAKE = [] # Will be updated with the file actions are processed from the OSM config. +ITEMS_TO_COPY = [] # Path() file objects; updated when FILE_ACTIONS are applied to a source vault. + def datestring(): '''Return the current date and time in UTC string format.''' return f'-{datetime.datetime.utcnow().isoformat()}Z' @@ -211,6 +214,34 @@ def init_argparse(): only_one_of.add_argument('--version', '-v', action='store_true', help='show version and exit') return parser +def only_relative_files_from(item_list, relative_to): + '''Return a list of just the files from item_list.''' + return [item.relative_to(relative_to) for item in item_list if item.is_file()] + +def get_matching_files(source_dir, item): + ''' + Returns a list of relative Path objects for all the files described by item. + + Files are a list of themselves. + Directories are the list of files in them, recursively. + Globs are whatever files match the glob, if any. + ''' + item_path = source_dir / item + if item_path.is_file(): + return [Path(item)] + if item_path.is_dir(): + return only_relative_files_from(item_path.rglob('*'), source_dir) + return only_relative_files_from(source_dir.glob(item), source_dir) + +def process_action_list_in(source_dir, actions_list): + '''Return a list of Path files to copy by applying the actions list actions to source_dir.''' + files = set() + source_dir = Path(source_dir) + for action, item in actions_list: + for a_file in get_matching_files(source_dir, item): + action(files, a_file) + return files + ### # Configuration Functions - OSM ### @@ -274,6 +305,16 @@ def get_processing_directives(): operation = must_get_key(FILE_ACTIONS, action, BAD_ACTION_MSG) yield (operation, must_be_type(item, str, BAD_ITEM_MSG)) +def get_items_to_copy(src): + ''' + Return the list of Paths to be copied from src. + + The return value is also cached in the ITEMS_TO_COPY list so we only build the list once. + ''' + if not ITEMS_TO_COPY: + ITEMS_TO_COPY.extend(process_action_list_in(src, ACTIONS_TO_TAKE)) + return ITEMS_TO_COPY + ### # Configuration Functions - Obsidian ### @@ -448,7 +489,7 @@ def copy_settings(dest, src, clean_first=False): if clean_first: recreate_dir(dest) - for item in ITEMS_TO_COPY: + for item in get_items_to_copy(src): copy_settings_item(suffix, src, dest, item) def do_diff(old, new): @@ -479,7 +520,7 @@ def diff_settings(dest, src): src = src / '.obsidian' dest = dest / '.obsidian' - for item in ITEMS_TO_COPY: + for item in get_items_to_copy(src): dest_item = dest / item src_item = src / item if src_item.exists() and dest_item.exists(): @@ -538,7 +579,7 @@ def main(): try: vault_paths = get_vault_paths() - directives = list(get_processing_directives()) + ACTIONS_TO_TAKE.extend(get_processing_directives()) if args.list: call_for_each_vault(vault_paths, show_vault_path) From 4104bfc603fe748d4b84af990ab4353b254cb85b Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sun, 9 Jul 2023 10:52:56 -0400 Subject: [PATCH 18/25] Keep files to be copied sorted. It's much easier on the human brain to see them in sorted order esp when looking at diffs. --- osm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 9039e2c..600dd0d 100755 --- a/osm.py +++ b/osm.py @@ -312,7 +312,7 @@ def get_items_to_copy(src): The return value is also cached in the ITEMS_TO_COPY list so we only build the list once. ''' if not ITEMS_TO_COPY: - ITEMS_TO_COPY.extend(process_action_list_in(src, ACTIONS_TO_TAKE)) + ITEMS_TO_COPY.extend(sorted(process_action_list_in(src, ACTIONS_TO_TAKE))) return ITEMS_TO_COPY ### From 93dc99623ce51ada78e2e8157742da32d1198f64 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sun, 9 Jul 2023 10:54:54 -0400 Subject: [PATCH 19/25] Option to show what files would be selected for a particular vault. Sure, you could use `--diff-to` and try to sort it out, but as you are adjusting your files_to_copy list, it would be nice to see how it would pull files from a specific vault without the other output --- osm.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osm.py b/osm.py index 600dd0d..a1e99e6 100755 --- a/osm.py +++ b/osm.py @@ -210,6 +210,7 @@ def init_argparse(): only_one_of.add_argument('--execute', '-x', help='run EXECUTE command within each vault (use caution!)') only_one_of.add_argument('--backup-list', action='store_true', help='list ISO 8601-formatted .obsidian backup files from all vaults') only_one_of.add_argument('--backup-remove', action='store_true', help='remove ISO 8601-formatted .obsidian backup files from all vaults') + only_one_of.add_argument('--show-selected', dest="from_vault", help='print the files that would be copied from FROM_VAULT to other vaults, then exit') only_one_of.add_argument('--print-default-config', action='store_true', help='print the default configuration and extt') only_one_of.add_argument('--version', '-v', action='store_true', help='show version and exit') return parser @@ -393,6 +394,12 @@ def print_default_config(): '''Print the default configuration so user can save and customize.''' print(OSM_DEFAULT_CONFIG) +def show_selected_files_for(vault): + '''Print the files that would be copied from vault.''' + print("These files would be copied from", vault) + for a_file in get_items_to_copy(Path(vault) / '.obsidian'): + print(" ", a_file) + def backup(item, suffix): '''Rename item to have the given suffix.''' backup = str(item)+suffix @@ -583,6 +590,9 @@ def main(): if args.list: call_for_each_vault(vault_paths, show_vault_path) + elif args.from_vault: + ensure_valid_vault(vault_paths, args.from_vault) + show_selected_files_for(Path.home() / args.from_vault) elif args.update: ensure_valid_vault(vault_paths, args.update) call_for_each_vault(vault_paths, copy_settings, Path.home() / args.update, clean_first=False) From b8aededa2381f5288dbc6ab1b1fc290f85f60f5e Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Mon, 10 Jul 2023 22:27:18 -0400 Subject: [PATCH 20/25] Fix baackup action function name. Was left over from when there were two separate backup action functions. --- osm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osm.py b/osm.py index a1e99e6..823b0a8 100755 --- a/osm.py +++ b/osm.py @@ -540,7 +540,7 @@ def diff_settings(dest, src): # If neither exist, nothing to do, nothing to say. Move along. -def backup_list_operation(vault_path, operation): +def backup_operation(vault_path, operation): '''Call operation with each backup item found in the given vault.''' dir_path = Path(vault_path) / '.obsidian' for dest in dir_path.glob(ISO_8601_GLOB): @@ -603,9 +603,9 @@ def main(): ensure_valid_vault(vault_paths, args.diff_to) call_for_each_vault(vault_paths, diff_settings, Path.home() / args.diff_to) elif args.backup_list: - call_for_each_vault(vault_paths, backup_list_operation, print) + call_for_each_vault(vault_paths, backup_operation, print) elif args.backup_remove: - call_for_each_vault(vault_paths, backup_list_operation, remove_item) + call_for_each_vault(vault_paths, backup_operation, remove_item) elif args.execute: call_for_each_vault(vault_paths, execute_command, args.execute) else: From 4f1f26f88737081adc1ed659e07f46717aef87c2 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Mon, 10 Jul 2023 22:28:22 -0400 Subject: [PATCH 21/25] Add configuration info for backups. --- osm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osm.py b/osm.py index 823b0a8..7e7f64a 100755 --- a/osm.py +++ b/osm.py @@ -106,7 +106,11 @@ { "copy": "snippets" }, { "copy": "themes" } - ] + ], + "backup": { + "archive_format_priority_list": ["zip", "gztar", "bztar", "xztar", "tar"], + "location": ".osm-obsidian-settings-backup" + } } ''' From 860420dfdcb35d6f4de9645ebc3bc3bced7a6ea6 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Thu, 13 Jul 2023 08:51:38 -0400 Subject: [PATCH 22/25] Remove old per-file backup functionality. --- osm.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/osm.py b/osm.py index 7e7f64a..20d7fc9 100755 --- a/osm.py +++ b/osm.py @@ -404,14 +404,6 @@ def show_selected_files_for(vault): for a_file in get_items_to_copy(Path(vault) / '.obsidian'): print(" ", a_file) -def backup(item, suffix): - '''Rename item to have the given suffix.''' - backup = str(item)+suffix - verbose('Saving current', item, 'as', backup) - if DRY_RUN: - return - item.rename(backup) - def copy_directory(src_target, dest_target): '''Copy the src_target directry to dest_target.''' verbose('Copying directory', src_target, 'to', dest_target) @@ -453,7 +445,7 @@ def execute_command(vault_path, command): else: subprocess.run(command, cwd=vault_path, shell=True) -def copy_settings_item(suffix, src, dest, itemname): +def copy_settings_item(src, dest, itemname): ''' Copy itemname from src to dest. @@ -467,8 +459,6 @@ def copy_settings_item(suffix, src, dest, itemname): if not src_target.exists(): return verbose() - if dest_target.exists(): - backup(dest_target, suffix) if src_target.is_dir(): copy_directory(src_target, dest_target) else: @@ -494,14 +484,11 @@ def copy_settings(dest, src, clean_first=False): src = src / '.obsidian' dest = dest / '.obsidian' - # Use a timestamp for the suffix for uniqueness - suffix = datestring() - if clean_first: recreate_dir(dest) for item in get_items_to_copy(src): - copy_settings_item(suffix, src, dest, item) + copy_settings_item(src, dest, item) def do_diff(old, new): '''Diff two items, prefix with the diff command used.''' From 270b3c516ca1960bade6184d559257b36197e091 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 22 Jul 2023 14:21:47 -0400 Subject: [PATCH 23/25] Allow iso date format to be more generally used. Remove the leading dash so that it can be used as a prefix. Match the ISO string anywhere, prefix, suffix, or embedded. --- osm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osm.py b/osm.py index 20d7fc9..b6a20e2 100755 --- a/osm.py +++ b/osm.py @@ -121,10 +121,10 @@ def datestring(): '''Return the current date and time in UTC string format.''' - return f'-{datetime.datetime.utcnow().isoformat()}Z' + return datetime.datetime.utcnow().isoformat() + 'Z' # Keep this in sync with the format returned by datestring() -ISO_8601_GLOB = '*-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z' +ISO_8601_GLOB = '*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9][0-9][0-9]Z*' VERBOSE = False DRY_RUN = False From 2c57ed1e87d6f36ed75ddcf3ab3d10618dc7bc35 Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 22 Jul 2023 14:23:38 -0400 Subject: [PATCH 24/25] Add new backup commands and implement backup_vault code. Also backup before doing an update. NOTE: update hasn't been fixed yet, only the backup code is working at this point --- osm.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/osm.py b/osm.py index b6a20e2..dcfe075 100755 --- a/osm.py +++ b/osm.py @@ -212,6 +212,8 @@ def init_argparse(): only_one_of.add_argument('--exact-copy-of', help='delete and recreate Obsidian vaults with an exact copy of the EXACT_COPY_OF vault') only_one_of.add_argument('--diff-to', '-d', help='Like update but instead of copying, just show a diff against DIFF_TO instead (no changes made).') only_one_of.add_argument('--execute', '-x', help='run EXECUTE command within each vault (use caution!)') + only_one_of.add_argument('--backup-all-vaults', action="store_true", help='make a backup of the obsidian settings for all vaults') + only_one_of.add_argument('--backup-vault', help='make a backup of the obsidian settings for BACKUP_VAULT') only_one_of.add_argument('--backup-list', action='store_true', help='list ISO 8601-formatted .obsidian backup files from all vaults') only_one_of.add_argument('--backup-remove', action='store_true', help='remove ISO 8601-formatted .obsidian backup files from all vaults') only_one_of.add_argument('--show-selected', dest="from_vault", help='print the files that would be copied from FROM_VAULT to other vaults, then exit') @@ -481,6 +483,8 @@ def copy_settings(dest, src, clean_first=False): print(f"Copying '{src}' configuration to '{dest}'") + backup_vault(dest) + src = src / '.obsidian' dest = dest / '.obsidian' @@ -531,11 +535,42 @@ def diff_settings(dest, src): # If neither exist, nothing to do, nothing to say. Move along. +def backup_vault(vault_path): + print('#', vault_path) + + vault_path = Path(vault_path) + + backup_info = must_get_key(OSM_CONFIG, 'backup', 'in OSM configuration') + backup_dir_name = Path(must_get_key(backup_info, 'location', 'in "backup" section of OSM configuration')) + backup_formats = must_get_key(backup_info, 'archive_format_priority_list', 'in "backup" section of OSM configuration') + + backup_from_dir = vault_path / '.obsidian' + backup_full_dir = Path(vault_path) / backup_dir_name + backup_full_dir.mkdir(exist_ok=True) + backup_file = Path(vault_path) / backup_dir_name / datestring() + for backup_format in backup_formats: + try: + backup_file = shutil.make_archive(backup_file, backup_format, root_dir=backup_from_dir) + if backup_file: + print("Backup made:", backup_file) + print() + return + except ValueError: + pass # Not a supported format, keep checking. + + print("Unable to make backup of", vault_path, "in any format:", ", ".join(map(repr, backup_formats))) + exit(-1) + def backup_operation(vault_path, operation): '''Call operation with each backup item found in the given vault.''' - dir_path = Path(vault_path) / '.obsidian' + backup_info = must_get_key(OSM_CONFIG, 'backup', 'in OSM configuration') + backup_dir = Path(must_get_key(backup_info, 'location', 'in "backup" section of OSM configuration')) + + print(f'# {vault_path}') + dir_path = Path(vault_path) / backup_dir for dest in dir_path.glob(ISO_8601_GLOB): operation(dest) + print() def show_vault_path(vault_path): '''Print the vault path relative to the user's home directory (more readable).''' @@ -593,6 +628,11 @@ def main(): elif args.diff_to: ensure_valid_vault(vault_paths, args.diff_to) call_for_each_vault(vault_paths, diff_settings, Path.home() / args.diff_to) + elif args.backup_all_vaults: + call_for_each_vault(vault_paths, backup_vault) + elif args.backup_vault: + ensure_valid_vault(vault_paths, args.backup_vault) + backup_vault(Path.home() / args.backup_vault) elif args.backup_list: call_for_each_vault(vault_paths, backup_operation, print) elif args.backup_remove: From 0a389962f4c7648f440dc10a154ff06f92ecb3ab Mon Sep 17 00:00:00 2001 From: Doug Philips Date: Sat, 22 Jul 2023 20:37:04 -0400 Subject: [PATCH 25/25] Implment verbose and dry-run for backing up vaults --- osm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osm.py b/osm.py index dcfe075..882c378 100755 --- a/osm.py +++ b/osm.py @@ -536,7 +536,7 @@ def diff_settings(dest, src): def backup_vault(vault_path): - print('#', vault_path) + print(f'# Backing up settings for: {vault_path}') vault_path = Path(vault_path) @@ -550,10 +550,10 @@ def backup_vault(vault_path): backup_file = Path(vault_path) / backup_dir_name / datestring() for backup_format in backup_formats: try: - backup_file = shutil.make_archive(backup_file, backup_format, root_dir=backup_from_dir) + backup_file = shutil.make_archive(backup_file, backup_format, root_dir=backup_from_dir, dry_run=DRY_RUN) if backup_file: - print("Backup made:", backup_file) - print() + verbose("Backup file:", backup_file) + verbose() return except ValueError: pass # Not a supported format, keep checking.