fix duplicate folders for target server

This commit is contained in:
rambros 2026-03-06 11:22:30 +05:30
parent 17d20df80a
commit fe5e680fa4
5 changed files with 42 additions and 51 deletions

View file

@ -13,44 +13,17 @@ logger = logging.getLogger(__name__)
class MigrationContext: class MigrationContext:
"""Holds state and connections for reading from Discord and writing to the target platform.""" """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.config = config
self.source_mode = source_mode self.source_mode = source_mode
# If caller didn't specify, fall back to config value # If caller didn't specify, fall back to config value
self.target_platform = target_platform or config.target_platform or "fluxer" self.target_platform = target_platform or config.target_platform or "fluxer"
self.state = MigrationState()
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
)
# Select the appropriate source reader # Select the appropriate source reader
if source_mode == "backup": if source_mode == "backup":
from src.core.backup_reader import BackupReader 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) self.discord_reader = BackupReader(backup_path)
logger.info(f"Source mode: BACKUP — reading from {backup_path}") logger.info(f"Source mode: BACKUP — reading from {backup_path}")
else: else:
@ -76,16 +49,26 @@ class MigrationContext:
self.is_running = False self.is_running = False
@staticmethod def _find_backup_path(self, server_id: str, base_dir_str: str) -> Path:
def _find_backup_path(server_id: str) -> Path:
"""Searches workspace for a DISCORD_BACKUP-{server_id} directory. Creates it if missing.""" """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}"): for d in Path(".").rglob(f"DISCORD_BACKUP-{server_id}"):
if d.is_dir(): if d.is_dir():
logger.info(f"Found backup directory: {d}") logger.info(f"Found backup directory globally: {d}")
return d return d
# If not found, create it in the current directory # If not found anywhere, create it inside the workspace
new_path = Path(".") / f"DISCORD_BACKUP-{server_id}" 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}") logger.info(f"No existing backup directory found for {server_id}. Creating new one: {new_path}")
new_path.mkdir(exist_ok=True) new_path.mkdir(exist_ok=True)
return new_path return new_path

View file

@ -323,15 +323,21 @@ class MigrationState:
new_folder = base / f"{clean_name}-{server_id}" new_folder = base / f"{clean_name}-{server_id}"
logger.info(f"Setting active migration folder: {new_folder}") logger.info(f"Setting active migration folder: {new_folder}")
# If we have an existing folder that is different, rename it # 1. Search base_dir to see if an older folder for this server_id exists
if self.state_file and self.state_file.parent.exists() and self.state_file.parent != new_folder: existing_folder: Path | None = None
# Check if it's actually in a server-specific folder (not roots) if base.exists() and base.is_dir():
if self.state_file.parent.name.endswith(f"-{server_id}"): for d in base.iterdir():
logger.info(f"Renaming active folder from {self.state_file.parent.name} to {new_folder.name}") if d.is_dir() and d.name.endswith(f"-{server_id}"):
try: existing_folder = d
self.state_file.parent.rename(new_folder) break
except Exception as e:
logger.debug(f"Could not rename {self.state_file.parent} to {new_folder}: {e}") # 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) new_folder.mkdir(parents=True, exist_ok=True)

View file

@ -300,7 +300,7 @@ class DiscordExporter:
return data 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.""" """Fetches and saves message history for a channel, handling incremental sync. Returns the total messages processed."""
channel = await self.reader.get_channel(channel_id) channel = await self.reader.get_channel(channel_id)
if not channel: if not channel:
@ -372,7 +372,9 @@ class DiscordExporter:
last_id = None last_id = None
# Load existing messages for incremental sync (skip if force) # 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: try:
with open(json_file, "r", encoding="utf-8") as f: with open(json_file, "r", encoding="utf-8") as f:
old_data = json.load(f) old_data = json.load(f)
@ -669,7 +671,7 @@ class DiscordExporter:
return data 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.""" """Exports active and archived threads for a channel. Returns accumulated message count."""
channel = await self.reader.get_channel(channel_id) channel = await self.reader.get_channel(channel_id)
if not hasattr(channel, "threads") and not hasattr(channel, "public_archived_threads"): 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 await asyncio.sleep(0) # important yield between threads
# First backup the full thread — this creates {thread_id}.json with totalAttachmentSizeBytes # 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 thread_count += 1
# Then populate the forum root JSON with the starter message # Then populate the forum root JSON with the starter message

View file

@ -47,7 +47,7 @@ class BackupPane(Container):
self.cfg_name = cfg_name self.cfg_name = cfg_name
self.config_path = cfg_path self.config_path = cfg_path
self.config = load_config(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}") self.exporter = DiscordExporter(self.engine.discord_reader, base_dir=f"Reaper-{self.cfg_name}")
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
@ -74,7 +74,7 @@ class BackupPane(Container):
def reload_config(self) -> None: def reload_config(self) -> None:
self.config = load_config(self.config_path) 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.exporter = DiscordExporter(self.engine.discord_reader, base_dir=f"Reaper-{self.cfg_name}")
self._validate() self._validate()

View file

@ -144,7 +144,7 @@ class ShuttlePane(Container):
def _rebuild_engine(self): def _rebuild_engine(self):
source = "backup" if self.config.tool_mode == "backup_transfer" else "live" 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 ──────────────────────────────────────────────────────────── # ── labels ────────────────────────────────────────────────────────────