diff --git a/src/core/base.py b/src/core/base.py index e8c6de6..8a31e78 100644 --- a/src/core/base.py +++ b/src/core/base.py @@ -13,44 +13,17 @@ logger = logging.getLogger(__name__) class MigrationContext: """Holds state and connections for reading from Discord and writing to the target platform.""" - def __init__(self, config: AppConfig, target_platform: str | None = None, source_mode: str = "live"): + def __init__(self, config: AppConfig, target_platform: str | None = None, source_mode: str = "live", base_dir: str = ""): self.config = config self.source_mode = source_mode # If caller didn't specify, fall back to config value self.target_platform = target_platform or config.target_platform or "fluxer" - - server_id = config.target_server_id or "unconfigured" - fillers = {"000000000000000000", "DISCORD_SERVER_ID", "FLUXER_COMMUNITY_ID", - "STOAT_SERVER_ID", "TARGET_SERVER_ID", ""} - if server_id in fillers: - server_id = "unconfigured" - - # Try to find an existing state folder for this server_id - import os - - state_file: str | Path = "" - messages_file: str | Path = "" - - if server_id != "unconfigured": - logger.info(f"Probing for existing migration folder for Server ID: {server_id}") - for d in Path(".").iterdir(): - if d.is_dir() and d.name.endswith(f"-{server_id}"): - logger.info(f"Targeting existing folder: {d.name}") - state_file = d / "state-migration.json" - messages_file = d / "message-tracker.json" - break - else: - logger.info(f"No existing folder found for {server_id}. A new one will be created upon first state change.") - - self.state = MigrationState( - state_file=state_file, - messages_file=messages_file - ) + self.state = MigrationState() # Select the appropriate source reader if source_mode == "backup": from src.core.backup_reader import BackupReader - backup_path = self._find_backup_path(config.discord_server_id) + backup_path = self._find_backup_path(config.discord_server_id, base_dir) self.discord_reader = BackupReader(backup_path) logger.info(f"Source mode: BACKUP — reading from {backup_path}") else: @@ -76,16 +49,26 @@ class MigrationContext: self.is_running = False - @staticmethod - def _find_backup_path(server_id: str) -> Path: + def _find_backup_path(self, server_id: str, base_dir_str: str) -> Path: """Searches workspace for a DISCORD_BACKUP-{server_id} directory. Creates it if missing.""" + base_dir = Path(base_dir_str) if base_dir_str else Path(".") + + # 1. Search inside the specific workspace directory first + if base_dir.exists() and base_dir.is_dir(): + for d in base_dir.iterdir(): + if d.is_dir() and d.name.endswith(f"-{server_id}") and "DISCORD_BACKUP" in d.name: + logger.info(f"Found backup directory: {d}") + return d + + # 2. Fallback to global search if it wasn't found in the workspace for d in Path(".").rglob(f"DISCORD_BACKUP-{server_id}"): if d.is_dir(): - logger.info(f"Found backup directory: {d}") + logger.info(f"Found backup directory globally: {d}") return d - # If not found, create it in the current directory - new_path = Path(".") / f"DISCORD_BACKUP-{server_id}" + # If not found anywhere, create it inside the workspace + base_dir.mkdir(exist_ok=True) + new_path = base_dir / f"DISCORD_BACKUP-{server_id}" logger.info(f"No existing backup directory found for {server_id}. Creating new one: {new_path}") new_path.mkdir(exist_ok=True) return new_path diff --git a/src/core/state.py b/src/core/state.py index 26b1267..53a49e7 100644 --- a/src/core/state.py +++ b/src/core/state.py @@ -323,15 +323,21 @@ class MigrationState: new_folder = base / f"{clean_name}-{server_id}" logger.info(f"Setting active migration folder: {new_folder}") - # If we have an existing folder that is different, rename it - if self.state_file and self.state_file.parent.exists() and self.state_file.parent != new_folder: - # Check if it's actually in a server-specific folder (not roots) - if self.state_file.parent.name.endswith(f"-{server_id}"): - logger.info(f"Renaming active folder from {self.state_file.parent.name} to {new_folder.name}") - try: - self.state_file.parent.rename(new_folder) - except Exception as e: - logger.debug(f"Could not rename {self.state_file.parent} to {new_folder}: {e}") + # 1. Search base_dir to see if an older folder for this server_id exists + existing_folder: Path | None = None + if base.exists() and base.is_dir(): + for d in base.iterdir(): + if d.is_dir() and d.name.endswith(f"-{server_id}"): + existing_folder = d + break + + # 2. Rename it if it doesn't match the new desired name + if existing_folder and existing_folder != new_folder: + logger.info(f"Renaming existing folder {existing_folder.name} to {new_folder.name}") + try: + existing_folder.rename(new_folder) + except Exception as e: + logger.debug(f"Could not rename {existing_folder} to {new_folder}: {e}") new_folder.mkdir(parents=True, exist_ok=True) diff --git a/src/disco_reaper/exporter.py b/src/disco_reaper/exporter.py index 490c8b3..bd9d91d 100644 --- a/src/disco_reaper/exporter.py +++ b/src/disco_reaper/exporter.py @@ -300,7 +300,7 @@ class DiscordExporter: return data - async def export_channel_messages(self, channel_id: int, progress_callback=None, force=False, accumulated_count=0): + async def export_channel_messages(self, channel_id: int, progress_callback=None, force=False, accumulated_count=0, after_id: int | None = None): """Fetches and saves message history for a channel, handling incremental sync. Returns the total messages processed.""" channel = await self.reader.get_channel(channel_id) if not channel: @@ -372,7 +372,9 @@ class DiscordExporter: last_id = None # Load existing messages for incremental sync (skip if force) - if not force and json_file.exists(): + if after_id is not None: + last_id = after_id + elif not force and json_file.exists(): try: with open(json_file, "r", encoding="utf-8") as f: old_data = json.load(f) @@ -669,7 +671,7 @@ class DiscordExporter: return data - async def export_threads(self, channel_id: int, progress_callback=None, force=False, accumulated_count=0): + async def export_threads(self, channel_id: int, progress_callback=None, force=False, accumulated_count=0, after_id: int | None = None): """Exports active and archived threads for a channel. Returns accumulated message count.""" channel = await self.reader.get_channel(channel_id) if not hasattr(channel, "threads") and not hasattr(channel, "public_archived_threads"): @@ -711,7 +713,7 @@ class DiscordExporter: await asyncio.sleep(0) # important yield between threads # First backup the full thread — this creates {thread_id}.json with totalAttachmentSizeBytes - accumulated_count = await self.export_channel_messages(thread.id, progress_callback=progress_callback, force=force, accumulated_count=accumulated_count) + accumulated_count = await self.export_channel_messages(thread.id, progress_callback=progress_callback, force=force, accumulated_count=accumulated_count, after_id=after_id) thread_count += 1 # Then populate the forum root JSON with the starter message diff --git a/src/ui/backup_ops.py b/src/ui/backup_ops.py index 3a56c1b..821e7c6 100644 --- a/src/ui/backup_ops.py +++ b/src/ui/backup_ops.py @@ -47,7 +47,7 @@ class BackupPane(Container): self.cfg_name = cfg_name self.config_path = cfg_path self.config = load_config(cfg_path) - self.engine = MigrationContext(self.config, target_platform=self.config.target_platform or "fluxer") + self.engine = MigrationContext(self.config, target_platform=self.config.target_platform or "fluxer", base_dir=f"Reaper-{self.cfg_name}") self.exporter = DiscordExporter(self.engine.discord_reader, base_dir=f"Reaper-{self.cfg_name}") def compose(self) -> ComposeResult: @@ -74,7 +74,7 @@ class BackupPane(Container): def reload_config(self) -> None: self.config = load_config(self.config_path) - self.engine = MigrationContext(self.config, target_platform=self.config.target_platform or "fluxer") + self.engine = MigrationContext(self.config, target_platform=self.config.target_platform or "fluxer", base_dir=f"Reaper-{self.cfg_name}") self.exporter = DiscordExporter(self.engine.discord_reader, base_dir=f"Reaper-{self.cfg_name}") self._validate() diff --git a/src/ui/shuttle_ops.py b/src/ui/shuttle_ops.py index 346a9a2..14b9268 100644 --- a/src/ui/shuttle_ops.py +++ b/src/ui/shuttle_ops.py @@ -144,7 +144,7 @@ class ShuttlePane(Container): def _rebuild_engine(self): source = "backup" if self.config.tool_mode == "backup_transfer" else "live" - self.engine = MigrationContext(self.config, self.target_platform, source_mode=source) + self.engine = MigrationContext(self.config, self.target_platform, source_mode=source, base_dir=self._base_dir()) # ── labels ────────────────────────────────────────────────────────────