From 47c1ac38b92af58bb99991a04fba175b13c23867 Mon Sep 17 00:00:00 2001 From: Mark Gascoyne Date: Sun, 28 Sep 2025 14:55:57 +0100 Subject: [PATCH 1/2] feat: enhance downloader to support directory structures - Copy only Python files from zip download for safety - Preserves user config files (config/apps.yaml etc) - Creates directory structure for python files in subdirs - Ignores cache, backup, and development files This ensures only code gets updated, never user settings. --- apps/predbat/download.py | 161 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 10 deletions(-) diff --git a/apps/predbat/download.py b/apps/predbat/download.py index ced73d324..28ea11038 100644 --- a/apps/predbat/download.py +++ b/apps/predbat/download.py @@ -10,26 +10,36 @@ import os import requests +import urllib.request +import shutil +import tempfile def download_predbat_file_from_github(tag, filename, new_filename): """ Downloads a predbat source file from github and returns the contents + Now supports files in subdirectories. Args: tag (str): The tag to download from (e.g. v1.0.0) - filename (str): The filename to download (e.g. predbat.py) + filename (str): The filename to download (e.g. predbat.py or utils/battery_manager.py) new_filename (str): The new filename to save the file as Returns: str: The contents of the file """ - url = "https://raw.githubusercontent.com/springfall2008/batpred/" + tag + "/apps/predbat/{}".format(filename) + # Handle both flat files and files in directories + url_path = filename.replace(os.sep, "/") # Ensure forward slashes for URL + url = "https://raw.githubusercontent.com/springfall2008/batpred/" + tag + "/apps/predbat/{}".format(url_path) print("Downloading {}".format(url)) r = requests.get(url, headers={}) if r.ok: data = r.text print("Got data, writing to {}".format(new_filename)) if new_filename: + # Create directory if needed + dir_path = os.path.dirname(new_filename) + if dir_path and not os.path.exists(dir_path): + os.makedirs(dir_path) with open(new_filename, "w") as han: han.write(data) return data @@ -38,19 +48,66 @@ def download_predbat_file_from_github(tag, filename, new_filename): return None -def predbat_update_move(version, files): +def predbat_update_move(version, backup_path_or_files): """ - Move the updated files into place + Move the updated files into place. + Handles both zip-based (backup_path) and individual file approaches. """ tag_split = version.split(" ") if tag_split: tag = tag_split[0] this_path = os.path.dirname(__file__) - cmd = "" - for file in files: - cmd += "mv -f {} {} && ".format(os.path.join(this_path, file + "." + tag), os.path.join(this_path, file)) - cmd += "echo 'Update complete'" - os.system(cmd) + + # Check if we have a backup path (zip method) or file list (individual method) + if isinstance(backup_path_or_files, str) and os.path.isdir(backup_path_or_files): + # Zip method - copy from backup directory + backup_path = backup_path_or_files + print("Moving files from backup directory: {}".format(backup_path)) + + # Copy all files from backup to current directory + for root, dirs, files in os.walk(backup_path): + for file in files: + source = os.path.join(root, file) + # Calculate relative path from backup_path + rel_path = os.path.relpath(source, backup_path) + dest = os.path.join(this_path, rel_path) + + # Create destination directory if needed + dest_dir = os.path.dirname(dest) + if dest_dir and dest_dir != this_path and not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + # Copy the file + shutil.copy2(source, dest) + print("Copied {} to {}".format(rel_path, dest)) + + # Clean up backup directory + shutil.rmtree(backup_path) + print("Cleaned up backup directory") + + else: + # Individual file method (backward compatibility) + files = backup_path_or_files + print("Moving individual files with version suffix") + + # Process files, creating directories as needed + for file in files: + source = os.path.join(this_path, file + "." + tag) + dest = os.path.join(this_path, file) + + # Create destination directory if needed + dest_dir = os.path.dirname(dest) + if dest_dir and dest_dir != this_path and not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + # Move the file + if os.path.exists(source): + os.rename(source, dest) + print("Moved {} to {}".format(source, dest)) + else: + print("Warning: Source file {} not found".format(source)) + + print("Update complete") return True return False @@ -72,6 +129,7 @@ def get_files_from_predbat(predbat_code): def check_install(): """ Check if Predbat is installed correctly + Now supports files in subdirectories. """ this_path = os.path.dirname(__file__) predbat_file = os.path.join(this_path, "predbat.py") @@ -91,10 +149,93 @@ def check_install(): return False +def predbat_update_download_zip(version): + """ + Download the defined version of Predbat from Github using zip method (like addon). + This supports directory structures automatically. + """ + this_path = os.path.dirname(__file__) + tag_split = version.split(" ") + if tag_split: + tag = tag_split[0] + + print("Downloading Predbat {} using zip method...".format(version)) + + # Download entire repository as zip + download_url = "https://github.com/springfall2008/batpred/archive/refs/tags/{}.zip".format(tag) + + with tempfile.TemporaryDirectory() as temp_dir: + zip_path = os.path.join(temp_dir, "predbat_{}.zip".format(tag)) + + try: + print("Downloading {}".format(download_url)) + urllib.request.urlretrieve(download_url, zip_path) + print("Predbat downloaded successfully") + except Exception as e: + print("Error: Unable to download Predbat - {}".format(e)) + return None + + print("Extracting Predbat...") + extract_path = os.path.join(temp_dir, "extract") + os.makedirs(extract_path) + shutil.unpack_archive(zip_path, extract_path) + + # Find the extracted directory (batpred-X.Y.Z format) + repo_path = os.path.join(extract_path, "batpred-{}".format(tag.replace("v", ""))) + predbat_source = os.path.join(repo_path, "apps", "predbat") + + if not os.path.exists(predbat_source): + print("Error: Could not find predbat source at {}".format(predbat_source)) + return None + + # Copy only Python files selectively (safe approach) + backup_path = os.path.join(this_path, "backup_{}".format(tag)) + if os.path.exists(backup_path): + shutil.rmtree(backup_path) + os.makedirs(backup_path) + + print("Copying Python files to {}...".format(backup_path)) + + # Copy main *.py files from root directory + for item in os.listdir(predbat_source): + source_path = os.path.join(predbat_source, item) + dest_path = os.path.join(backup_path, item) + + if os.path.isfile(source_path) and item.endswith(".py"): + # Copy only Python files + shutil.copy2(source_path, dest_path) + print(" Copied file: {}".format(item)) + elif os.path.isdir(source_path) and item not in ["config", "__pycache__", ".ruff_cache", ".git"]: + # Copy subdirectories but only *.py files within them + os.makedirs(dest_path) + print(" Created directory: {}".format(item)) + for subitem in os.listdir(source_path): + if subitem.endswith(".py"): + sub_source = os.path.join(source_path, subitem) + sub_dest = os.path.join(dest_path, subitem) + shutil.copy2(sub_source, sub_dest) + print(" Copied: {}/{}".format(item, subitem)) + + # Note: Only *.py files copied, no config or other file types + + print("Download and extraction completed successfully") + return backup_path + + return None + + def predbat_update_download(version): """ - Download the defined version of Predbat from Github + Download the defined version of Predbat from Github. + Uses zip method for better directory support. """ + # Use zip method (more reliable with directories) + backup_path = predbat_update_download_zip(version) + if backup_path: + return backup_path + + # Fallback to original method for backward compatibility + print("Zip method failed, trying individual file download...") this_path = os.path.dirname(__file__) tag_split = version.split(" ") if tag_split: From 31de769097a065a795b2e7cd8e67662c681b1e2f Mon Sep 17 00:00:00 2001 From: Mark Gascoyne Date: Tue, 30 Sep 2025 15:37:28 +0100 Subject: [PATCH 2/2] fix: use os.replace() instead of os.rename() for cross-platform file overwriting - os.rename() fails on Windows when destination exists (raises FileExistsError) - os.replace() provides consistent cross-platform overwrite behavior - Ensures update process works reliably on both Unix and Windows systems - Addresses platform-dependent file overwriting issue --- apps/predbat/download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/predbat/download.py b/apps/predbat/download.py index 28ea11038..93f92cf93 100644 --- a/apps/predbat/download.py +++ b/apps/predbat/download.py @@ -100,9 +100,9 @@ def predbat_update_move(version, backup_path_or_files): if dest_dir and dest_dir != this_path and not os.path.exists(dest_dir): os.makedirs(dest_dir) - # Move the file + # Move the file (use os.replace for cross-platform overwrite behavior) if os.path.exists(source): - os.rename(source, dest) + os.replace(source, dest) print("Moved {} to {}".format(source, dest)) else: print("Warning: Source file {} not found".format(source))