From b5d3556add4267c1373638fbc511ebb6eb81c4e7 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 1 Jun 2026 22:19:44 +0800 Subject: [PATCH] Fix project rename and duplicate issues Fixes #2759 When renaming a project: - Update self._filename to match the new project name - Rename the .gns3 file on disk to keep it in sync - Add error handling for file rename failures When duplicating a project: - Use self._filename (actual filename) instead of self.name - This handles the case where a project has been renamed - Prevents 'No such file or directory' errors The root cause was that project renaming only updated the project name in memory and in the .gns3 file content, but did not update the actual .gns3 filename. This caused duplicate operations to fail because they tried to read a file with the new name that didn't exist. --- gns3server/controller/project.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 4924f59e1..9452c21b5 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -428,7 +428,21 @@ class Project: @name.setter def name(self, val): + old_filename = self._filename self._name = val + self._filename = val + ".gns3" + + # Rename the .gns3 file on disk when the project name changes + if old_filename != self._filename: + old_path = os.path.join(self._path, old_filename) + new_path = os.path.join(self._path, self._filename) + if os.path.exists(old_path): + try: + shutil.move(old_path, new_path) + log.info(f"Project file renamed from '{old_filename}' to '{self._filename}'") + except OSError as e: + log.warning(f"Could not rename project file from '{old_filename}' to '{self._filename}': {e}") + self._filename = old_filename @property def id(self): @@ -1404,7 +1418,11 @@ class Project: # copy dir await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True) log.info("Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0)) - topology = json.loads(new_project_path.joinpath('{}.gns3'.format(self.name)).read_bytes()) + + # Read the topology file using the actual filename (self._filename), not self.name + # This handles the case where a project has been renamed but we need to read the actual file + old_gns3_file = new_project_path.joinpath(self._filename) + topology = json.loads(old_gns3_file.read_bytes()) project_name = name or topology["name"] # If the project name is already used we generate a new one project_name = self.controller.get_free_project_name(project_name) @@ -1428,7 +1446,8 @@ class Project: if os.path.isdir(snapshots_dir): await update_snapshots(snapshots_dir, new_project_path, project_name, new_project_id) - os.remove(new_project_path.joinpath('{}.gns3'.format(self.name))) + # Remove the old .gns3 file (which has the original project name) + os.remove(old_gns3_file) project = await self.controller.load_project(dot_gns3_path, load=False) log.info("Project '{}': fast duplicated in {:.4f} seconds".format(project.name, time.time() - t0)) return project