diff --git a/flightmonitor/controller/app_controller.py b/flightmonitor/controller/app_controller.py index 2cf8050..9a892c9 100644 --- a/flightmonitor/controller/app_controller.py +++ b/flightmonitor/controller/app_controller.py @@ -1051,6 +1051,48 @@ class AppController: if self.aircraft_db_manager: return self.aircraft_db_manager.delete_scan_session(scan_id) return False + + def get_aircraft_database_size_info(self) -> str: + """ + Retrieves the size of the aircraft database as a formatted string. + + Returns: + A string representing the size (e.g., "15.2 MB") or "N/A". + """ + if self.aircraft_db_manager: + size_bytes = self.aircraft_db_manager.get_database_size_bytes() + if size_bytes < 1024: + return f"{size_bytes} Bytes" + elif size_bytes < 1024**2: + return f"{size_bytes / 1024:.2f} KB" + elif size_bytes < 1024**3: + return f"{size_bytes / 1024**2:.2f} MB" + else: + return f"{size_bytes / 1024**3:.2f} GB" + return "N/A" + + def get_map_tile_cache_size_info(self) -> str: + """ + Retrieves the size of the map tile cache as a formatted string. + + Returns: + A string representing the size (e.g., "50.7 MB") or "N/A". + """ + if ( + self.main_window + and self.main_window.map_manager_instance + and self.main_window.map_manager_instance.tile_manager + ): + size_bytes = self.main_window.map_manager_instance.tile_manager.get_cache_size_bytes() + if size_bytes < 1024: + return f"{size_bytes} Bytes" + elif size_bytes < 1024**2: + return f"{size_bytes / 1024:.2f} KB" + elif size_bytes < 1024**3: + return f"{size_bytes / 1024**2:.2f} MB" + else: + return f"{size_bytes / 1024**3:.2f} GB" + return "N/A" def clear_aircraft_database(self) -> bool: """Clears the aircraft details table.""" diff --git a/flightmonitor/data/aircraft_database_manager.py b/flightmonitor/data/aircraft_database_manager.py index 4739571..02c1303 100644 --- a/flightmonitor/data/aircraft_database_manager.py +++ b/flightmonitor/data/aircraft_database_manager.py @@ -720,4 +720,19 @@ class AircraftDatabaseManager: elif self.conn and threading.get_ident() != self._main_thread_id: logger.warning( f"Attempt to close main connection from non-main thread ({threading.get_ident()}). Ignored." - ) \ No newline at end of file + ) + + def get_database_size_bytes(self) -> int: + """ + Calculates and returns the size of the main aircraft database file in bytes. + + Returns: + int: The size of the database file in bytes. Returns 0 if it doesn't exist. + """ + try: + if os.path.exists(self.db_path): + return os.path.getsize(self.db_path) + except OSError as e: + logger.error(f"Could not get size of database file {self.db_path}: {e}") + + return 0 \ No newline at end of file diff --git a/flightmonitor/gui/dialogs/maintenance_window.py b/flightmonitor/gui/dialogs/maintenance_window.py index ad8433b..f0b622e 100644 --- a/flightmonitor/gui/dialogs/maintenance_window.py +++ b/flightmonitor/gui/dialogs/maintenance_window.py @@ -33,7 +33,11 @@ class MaintenanceWindow(tk.Toplevel): self.geometry("800x600") self.minsize(700, 500) - # Widget references + # --- Tkinter Variables for dynamic labels --- + self.db_size_var = tk.StringVar(value="Calculating...") + self.cache_size_var = tk.StringVar(value="Calculating...") + + # --- Widget References --- self.recordings_tree: Optional[ttk.Treeview] = None self.sessions_tree: Optional[ttk.Treeview] = None @@ -56,6 +60,7 @@ class MaintenanceWindow(tk.Toplevel): """Initial population of all data tabs.""" self._populate_recordings_tab() self._populate_sessions_tab() + self._populate_general_tab() def _create_recordings_tab(self, notebook: ttk.Notebook): """Creates the tab for managing daily recording database files.""" @@ -238,19 +243,43 @@ class MaintenanceWindow(tk.Toplevel): container = ttk.Frame(notebook, padding="10") notebook.add(container, text="General Maintenance") - ac_db_frame = ttk.LabelFrame(container, text="Aircraft Database", padding=10) - ac_db_frame.pack(fill=tk.X, pady=(0, 10)) - ttk.Button(ac_db_frame, text="Clear Entire Aircraft Database...", command=self._clear_aircraft_db).pack(pady=5) - ttk.Button(ac_db_frame, text="Clear Scan History...", command=self._clear_scan_history).pack(pady=5) + status_frame = ttk.LabelFrame(container, text="Storage & Cache Status", padding=10) + status_frame.pack(fill=tk.X, pady=(0, 10)) + status_frame.columnconfigure(1, weight=1) + + ttk.Label(status_frame, text="Aircraft DB Size:").grid(row=0, column=0, sticky="w", padx=(0, 5)) + ttk.Label(status_frame, textvariable=self.db_size_var).grid(row=0, column=1, sticky="w") + + ttk.Label(status_frame, text="Map Cache Size:").grid(row=1, column=0, sticky="w", padx=(0, 5), pady=(5,0)) + ttk.Label(status_frame, textvariable=self.cache_size_var).grid(row=1, column=1, sticky="w", pady=(5,0)) + + ttk.Button(status_frame, text="Refresh Sizes", command=self._populate_general_tab).grid(row=0, column=2, rowspan=2, padx=(20,0)) - map_cache_frame = ttk.LabelFrame(container, text="Map Tile Cache", padding=10) + ac_db_frame = ttk.LabelFrame(container, text="Aircraft Database Actions", padding=10) + ac_db_frame.pack(fill=tk.X, pady=(0, 10)) + ttk.Button(ac_db_frame, text="Clear Entire Aircraft Database...", command=self._clear_aircraft_db).pack(pady=5, anchor="w") + ttk.Button(ac_db_frame, text="Clear Scan History...", command=self._clear_scan_history).pack(pady=5, anchor="w") + + map_cache_frame = ttk.LabelFrame(container, text="Map Cache Actions", padding=10) map_cache_frame.pack(fill=tk.X, pady=(0, 10)) - ttk.Button(map_cache_frame, text="Clear Map Tile Cache...", command=self._clear_map_cache).pack(pady=5) - + ttk.Button(map_cache_frame, text="Clear Map Tile Cache...", command=self._clear_map_cache).pack(pady=5, anchor="w") + + def _populate_general_tab(self): + """Fetches and displays the storage and cache sizes.""" + if not self.controller: + return + + db_size_str = self.controller.get_aircraft_database_size_info() + self.db_size_var.set(db_size_str) + + cache_size_str = self.controller.get_map_tile_cache_size_info() + self.cache_size_var.set(cache_size_str) + def _clear_aircraft_db(self): if messagebox.askyesno("Confirm Action", "Are you sure you want to delete ALL records from the aircraft database? This action cannot be undone.", parent=self): if self.controller and self.controller.clear_aircraft_database(): messagebox.showinfo("Success", "Aircraft database has been cleared.", parent=self) + self._populate_general_tab() # Refresh size display else: messagebox.showerror("Error", "Failed to clear the aircraft database.", parent=self) @@ -265,6 +294,7 @@ class MaintenanceWindow(tk.Toplevel): if messagebox.askyesno("Confirm Action", "Are you sure you want to delete all cached map tiles? They will be re-downloaded as needed.", parent=self): if self.controller and self.controller.clear_map_tile_cache(): messagebox.showinfo("Success", "Map tile cache has been cleared.", parent=self) + self._populate_general_tab() # Refresh size display else: messagebox.showerror("Error", "Failed to clear the map tile cache.", parent=self) diff --git a/flightmonitor/map/map_tile_manager.py b/flightmonitor/map/map_tile_manager.py index a9a35b6..e240ff9 100644 --- a/flightmonitor/map/map_tile_manager.py +++ b/flightmonitor/map/map_tile_manager.py @@ -954,3 +954,28 @@ class MapTileManager: logger.exception( f"Unexpected error clearing cache '{self.service_specific_cache_dir}': {e_clear_unexpected}" ) + + def get_cache_size_bytes(self) -> int: + """ + Calculates and returns the total size of the cache directory for this service in bytes. + + Returns: + int: The total size of the cache in bytes. Returns 0 if cache does not exist or is empty. + """ + total_size = 0 + cache_dir = self.service_specific_cache_dir + if not cache_dir.exists(): + return 0 + + try: + for dirpath, dirnames, filenames in os.walk(cache_dir): + for f in filenames: + fp = os.path.join(dirpath, f) + # skip if it is symbolic link + if not os.path.islink(fp): + total_size += os.path.getsize(fp) + except OSError as e: + logger.error(f"Could not calculate size of cache directory {cache_dir}: {e}") + return 0 + + return total_size