{"record":{"id":"6a6e5c4a4929252b","repo":"passbolt/passbolt_api","slug":"folder-id-0-is-already-v5","errorCode":null,"errorMessage":"Folder ID \"{0}\" is already V5","messagePattern":"Folder ID \"(.+?)\" is already V5","errorType":"exception","errorClass":"InternalErrorException","httpStatus":500,"severity":"error","filePath":"plugins/PassboltCe/Metadata/src/Service/Migration/MigrateAllV4FoldersToV5Service.php","lineNumber":100,"sourceCode":"            ->find()\n            ->contain(['Permissions.Users.Gpgkeys'])\n            ->where(['name IS NOT NULL'])\n            ->all()\n            ->toArray();\n\n        if (empty($folders)) {\n            $this->addError(['error_message' => __('No folders to migrate.')]);\n\n            return $this->getResult();\n        }\n\n        foreach ($folders as $folder) {\n            $dto = MetadataFolderDto::fromArray($folder->toArray());\n\n            try {\n                if ($dto->isV5()) {\n                    $msg = __('Folder ID \"{0}\" is already V5', $folder->id);\n                    throw new InternalErrorException($msg);\n                }\n                if (count($folder->permissions) === 0) {\n                    $msg = __('No permission found for folder ID {0}', $folder->id);\n                    throw new InternalErrorException($msg);\n                }\n\n                if (count($folder->permissions) === 1) {\n                    $this->migratePersonal($dto, $folder);\n                } else {\n                    $this->migrateShared($dto, $folder);\n                }\n                $this->addMigrated($folder);\n            } catch (Exception $e) {\n                // Continue with next resource if any error\n                $error = ['folder_id' => $folder->id, 'error_message' => $e->getMessage()];\n                if (Configure::read('debug')) {\n                    $error['trace'] = $e->getTraceAsString();\n                }","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltCe/Metadata/src/Service/Migration/MigrateAllV4FoldersToV5Service.php#L82-L118","documentation":"During bulk V4-to-V5 folder migration, each folder is converted to a MetadataFolderDto and checked; if the DTO already reports isV5() (metadata populated, name nulled), an InternalErrorException is thrown because migrating an already-migrated folder is an invariant violation in the batch. The message includes the folder ID to locate the offender.","triggerScenarios":"Running the migrate-all-folders command/service on a database where some folders were already migrated to v5 (partially completed earlier run, or folders migrated manually/via another job) — the query selecting candidate folders matched them again.","commonSituations":"Re-running a failed migration job without cleaning up; concurrent migration workers double-processing folders; snapshot/restore mixing pre- and post-migration data.","solutions":["Exclude already-v5 folders from the query (add a condition on metadata IS NOT NULL / name IS NULL).","Skip-and-log already-migrated folders instead of throwing, to make the migration idempotent.","Re-run the migration only after confirming no partial state: check folders where metadata is set but name is not null.","Ensure only one migration job runs at a time (use a lock) to avoid double processing."],"exampleFix":"// before: select all folders, throw on already-v5\n$folders = $foldersTable->find()->contain('Permissions')->all();\n// after: filter at query time\n$folders = $foldersTable->find()\n    ->where(['metadata IS' => null])\n    ->contain('Permissions')->all();","handlingStrategy":"validation","validationCode":"$remaining = TableRegistry::getTableLocator()->get('Folders')\n    ->find()->where(['metadata IS' => null])->count();\nif ($remaining === 0) { /* nothing to migrate, skip the job */ }","typeGuard":"if ($dto->isV5()) { continue; } // skip already-migrated folders","tryCatchPattern":"try {\n    $service->migrate($uac);\n} catch (\\Cake\\Http\\Exception\\InternalErrorException $e) {\n    if (str_contains($e->getMessage(), 'is already V5')) {\n        // exclude that folder id and resume migration\n    }\n}","preventionTips":["Make the candidate query exclude already-v5 folders so runs are idempotent","Use a lock so only one migration job runs at a time","Track migration progress (folder IDs done) to allow safe resume"],"tags":["migration","folders","idempotency","metadata"],"backgroundTag":"internal-invariant-violation","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}