From d1715eb13e4f69a120a01cf43831b29e84dd6f82 Mon Sep 17 00:00:00 2001 From: Bradnitski Date: Wed, 4 Feb 2026 13:16:11 +0100 Subject: [PATCH 1/2] Single repository restore #11492 --- .../com/enonic/xp/dump/SystemLoadParams.java | 18 ++ .../xp/repo/impl/dump/DumpServiceImpl.java | 155 +++++++++++------- .../impl/dump/reader/AbstractDumpReader.java | 61 ++++++- .../xp/repo/impl/dump/reader/DumpReader.java | 5 +- .../xp/core/dump/DumpServiceImplTest.java | 99 +++++++++++ modules/lib/package-lock.json | 7 + .../xp/impl/server/rest/SystemResource.java | 9 +- .../rest/model/SystemLoadRequestJson.java | 13 +- .../server/rest/task/LoadRunnableTask.java | 14 +- .../rest/task/LoadRunnableTaskTest.java | 44 ++++- 10 files changed, 357 insertions(+), 68 deletions(-) diff --git a/modules/core/core-api/src/main/java/com/enonic/xp/dump/SystemLoadParams.java b/modules/core/core-api/src/main/java/com/enonic/xp/dump/SystemLoadParams.java index ad0bd09c0db..4b9b0a67aba 100644 --- a/modules/core/core-api/src/main/java/com/enonic/xp/dump/SystemLoadParams.java +++ b/modules/core/core-api/src/main/java/com/enonic/xp/dump/SystemLoadParams.java @@ -1,5 +1,7 @@ package com.enonic.xp.dump; +import com.enonic.xp.repository.RepositoryIds; + public final class SystemLoadParams { private final String dumpName; @@ -12,6 +14,8 @@ public final class SystemLoadParams private final SystemLoadListener listener; + private final RepositoryIds repositories; + private SystemLoadParams( final Builder builder ) { this.dumpName = builder.dumpName; @@ -19,6 +23,7 @@ private SystemLoadParams( final Builder builder ) this.listener = builder.listener; this.upgrade = builder.upgrade; this.archive = builder.archive; + this.repositories = builder.repositories; } public String getDumpName() @@ -46,6 +51,11 @@ public boolean isArchive() return archive; } + public RepositoryIds getRepositories() + { + return repositories; + } + public static Builder create() { return new Builder(); @@ -63,6 +73,8 @@ public static final class Builder private SystemLoadListener listener; + private RepositoryIds repositories; + private Builder() { } @@ -97,6 +109,12 @@ public Builder archive( final boolean archive ) return this; } + public Builder repositories( final RepositoryIds repositories ) + { + this.repositories = repositories; + return this; + } + public SystemLoadParams build() { return new SystemLoadParams( this ); diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/DumpServiceImpl.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/DumpServiceImpl.java index 8d059c60c53..27b77e8e185 100644 --- a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/DumpServiceImpl.java +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/DumpServiceImpl.java @@ -305,92 +305,125 @@ public SystemLoadResult load( final SystemLoadParams params ) throw new RepoLoadException( "Cannot load system-dump; dump does not contain system repository" ); } - this.eventPublisher.publish( RepositoryEvents.restoreInitialized() ); + final RepositoryIds requestedRepositories = params.getRepositories(); + final boolean isPartialLoad = requestedRepositories != null && !requestedRepositories.isEmpty(); - if ( params.getListener() != null ) - { - final long branchesCount = dumpRepositories. - stream(). - flatMap( repositoryId -> dumpReader.getBranches( repositoryId ).stream() ). - count(); + doLoad( params, results, dumpReader, dumpRepositories, isPartialLoad ? requestedRepositories : null ); - params.getListener().totalBranches( branchesCount ); - } + this.eventPublisher.publish( RepositoryEvents.restored() ); + LOG.info( "Dump Load completed" ); + } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } + return results.build(); + } - final boolean includeVersions = params.isIncludeVersions(); + private void doLoad( final SystemLoadParams params, final SystemLoadResult.Builder results, final DumpReader dumpReader, + final RepositoryIds dumpRepositories, final RepositoryIds requestedRepositories ) + { + final boolean isPartialLoad = requestedRepositories != null && !requestedRepositories.isEmpty(); - final RepositorySettings currentSystemSettings = - repositoryEntryService.getRepositoryEntry( SystemConstants.SYSTEM_REPO_ID ).getSettings(); + // Determine which repositories to load (excluding system-repo for partial load) + final RepositoryIds repositoriesToLoad = isPartialLoad ? dumpRepositories.stream() + .filter( requestedRepositories::contains ) + .filter( Predicate.isEqual( SystemConstants.SYSTEM_REPO_ID ).negate() ) + .collect( RepositoryIds.collector() ) : dumpRepositories; - final Map repoSettings = SYSTEM_REPO_IDS.stream() - .collect( - Collectors.toMap( Function.identity(), repo -> repositoryEntryService.getRepositoryEntry( repo ).getSettings() ) ); + this.eventPublisher.publish( RepositoryEvents.restoreInitialized() ); - repositoryEntryService.findRepositoryEntryIds(). - stream(). - filter( Predicate.isEqual( SystemConstants.SYSTEM_REPO_ID ). - or( SYSTEM_REPO_IDS::contains ). - negate() ). - forEach( this::doDeleteRepository ); + if ( params.getListener() != null ) + { + final long branchesCount = + repositoriesToLoad.stream().flatMap( repositoryId -> dumpReader.getBranches( repositoryId ).stream() ).count(); + params.getListener().totalBranches( branchesCount ); + } - SYSTEM_REPO_IDS.forEach( this::doDeleteRepository ); + final boolean includeVersions = params.isIncludeVersions(); - // system-repo must be deleted last + // Collect settings for repositories before deleting them + final Map repoSettings = repositoriesToLoad.stream() + .filter( repositoryId -> repositoryEntryService.getRepositoryEntry( repositoryId ) != null ) + .collect( Collectors.toMap( Function.identity(), repo -> repositoryEntryService.getRepositoryEntry( repo ).getSettings() ) ); + + // Delete repositories + if ( isPartialLoad ) + { + RepositoryIds systemReposToDelete = + repositoriesToLoad.stream().filter( SYSTEM_REPO_IDS::contains ).collect( RepositoryIds.collector() ); + + // Partial load: delete only repositories that will be loaded + repositoriesToLoad.stream() + .filter( repositoryId -> repositoryEntryService.getRepositoryEntry( repositoryId ) != null ) + .filter( Predicate.not( SYSTEM_REPO_IDS::contains ) ) + .forEach( this::doDeleteRepository ); + + systemReposToDelete.forEach( this::doDeleteRepository ); + + } + else + { + // Full load: delete all repositories (system-repo last) + repositoryEntryService.findRepositoryEntryIds() + .stream() + .filter( Predicate.isEqual( SystemConstants.SYSTEM_REPO_ID ).or( SYSTEM_REPO_IDS::contains ).negate() ) + .forEach( this::doDeleteRepository ); + + SYSTEM_REPO_IDS.forEach( this::doDeleteRepository ); doDeleteRepository( SystemConstants.SYSTEM_REPO_ID ); - // Load system-repo to be able to read repository settings and data - initAndLoad( includeVersions, results, dumpReader, SystemConstants.SYSTEM_REPO_ID, currentSystemSettings, null, - AttachedBinaries.empty() ); + // Load system-repo first to be able to read repository settings and data + initAndLoad( includeVersions, results, dumpReader, SystemConstants.SYSTEM_REPO_ID, + repoSettings.get( SystemConstants.SYSTEM_REPO_ID ), null, AttachedBinaries.empty() ); // Transient repositories are not part of the dump. Clean them up. - final RepositoryIds repositoryEntryIds = repositoryEntryService.findRepositoryEntryIds(); - for ( RepositoryId repositoryId : repositoryEntryIds ) + for ( RepositoryId repositoryId : repositoryEntryService.findRepositoryEntryIds() ) { if ( repositoryEntryService.getRepositoryEntry( repositoryId ).isTransient() ) { repositoryEntryService.deleteRepositoryEntry( repositoryId ); } } + } + + // Load other system repositories (auditlog, scheduler, app) + SYSTEM_REPO_IDS.stream().filter( repositoriesToLoad::contains ).forEach( repositoryId -> { + final RepositorySettings settings = repoSettings.getOrDefault( repositoryId, RepositorySettings.create().build() ); + if ( dumpRepositories.contains( repositoryId ) ) + { + initAndLoad( includeVersions, results, dumpReader, repositoryId, settings, new PropertyTree(), AttachedBinaries.empty() ); + } + else if ( !isPartialLoad ) + { + // Full load: recreate missing system repos with current settings + initializeRepo( repositoryId, settings, null, AttachedBinaries.empty() ); + createRootNode( repositoryId ); + } + } ); + + // Load non-system repositories + repositoriesToLoad.stream() + .filter( Predicate.isEqual( SystemConstants.SYSTEM_REPO_ID ).or( SYSTEM_REPO_IDS::contains ).negate() ) + .forEach( repositoryId -> { + final RepositorySettings settings = repoSettings.getOrDefault( repositoryId, RepositorySettings.create().build() ); - // Load other system repositories - SYSTEM_REPO_IDS.forEach( repositoryId -> { - if ( dumpRepositories.contains( repositoryId ) ) + if ( isPartialLoad ) { - // Dump contains repository. Do a normal load. - initAndLoad( includeVersions, results, dumpReader, repositoryId, repoSettings.get( repositoryId ), new PropertyTree(), - AttachedBinaries.empty() ); + // Partial load: get data and attachments from dump's system-repo + final RepositoryEntry dumpRepoEntry = dumpReader.getRepositoryEntry( repositoryId ); + final PropertyTree data = dumpRepoEntry != null ? dumpRepoEntry.getData() : null; + final AttachedBinaries attachments = dumpRepoEntry != null ? dumpRepoEntry.getAttachments() : AttachedBinaries.empty(); + initAndLoad( includeVersions, results, dumpReader, repositoryId, settings, data, attachments ); } else { - // If it is an old dump it does not contain repo. It should be recreated with current settings - initializeRepo( repositoryId, repoSettings.get( repositoryId ), null, AttachedBinaries.empty() ); - createRootNode( repositoryId ); + // Full load: get data and attachments from loaded system-repo + final RepositoryEntry repository = repositoryEntryService.getRepositoryEntry( repositoryId ); + initAndLoad( includeVersions, results, dumpReader, repositoryId, repository.getSettings(), repository.getData(), + repository.getAttachments() ); } - } ); - - // Load non-system repositories - dumpRepositories. - stream(). - filter( Predicate.isEqual( SystemConstants.SYSTEM_REPO_ID ). - or( SYSTEM_REPO_IDS::contains ). - negate() ). - forEach( repositoryId -> { - final RepositoryEntry repository = repositoryEntryService.getRepositoryEntry( repositoryId ); - final RepositorySettings settings = repository.getSettings(); - final PropertyTree data = repository.getData(); - final AttachedBinaries attachedBinaries = repository.getAttachments(); - initAndLoad( includeVersions, results, dumpReader, repositoryId, settings, data, attachedBinaries ); - } ); - - this.eventPublisher.publish( RepositoryEvents.restored() ); - LOG.info( "Dump Load completed" ); - } - catch ( IOException e ) - { - throw new UncheckedIOException( e ); - } - return results.build(); } void verifyOrUpdateDumpVersion( final Path basePath, final SystemLoadParams params, final DumpReader dumpReader ) diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/AbstractDumpReader.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/AbstractDumpReader.java index ca28d1b1031..673b225cb10 100644 --- a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/AbstractDumpReader.java +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/AbstractDumpReader.java @@ -17,7 +17,6 @@ import com.google.common.io.LineProcessor; import com.enonic.xp.blob.BlobKey; -import com.enonic.xp.node.NodeVersionKey; import com.enonic.xp.blob.SegmentLevel; import com.enonic.xp.branch.Branch; import com.enonic.xp.branch.Branches; @@ -29,19 +28,29 @@ import com.enonic.xp.dump.SystemDumpResult; import com.enonic.xp.dump.SystemLoadListener; import com.enonic.xp.dump.VersionsLoadResult; +import com.enonic.xp.index.ChildOrder; +import com.enonic.xp.node.Node; +import com.enonic.xp.node.NodeId; import com.enonic.xp.node.NodeVersion; +import com.enonic.xp.node.NodeVersionKey; import com.enonic.xp.repo.impl.dump.FilePaths; import com.enonic.xp.repo.impl.dump.PathRef; import com.enonic.xp.repo.impl.dump.RepoDumpException; import com.enonic.xp.repo.impl.dump.RepoLoadException; import com.enonic.xp.repo.impl.dump.blobstore.BlobReference; +import com.enonic.xp.repo.impl.dump.model.BranchDumpEntry; import com.enonic.xp.repo.impl.dump.model.DumpMeta; import com.enonic.xp.repo.impl.dump.serializer.json.DumpMetaJsonSerializer; +import com.enonic.xp.repo.impl.dump.serializer.json.JsonDumpSerializer; import com.enonic.xp.repo.impl.node.NodeConstants; import com.enonic.xp.repo.impl.node.json.NodeVersionJsonSerializer; +import com.enonic.xp.repo.impl.repository.RepositoryEntry; +import com.enonic.xp.repo.impl.repository.RepositoryNodeTranslator; +import com.enonic.xp.repository.RepositoryConstants; import com.enonic.xp.repository.RepositoryId; import com.enonic.xp.repository.RepositoryIds; import com.enonic.xp.repository.RepositorySegmentUtils; +import com.enonic.xp.security.SystemConstants; public abstract class AbstractDumpReader implements DumpReader @@ -195,6 +204,56 @@ public DumpMeta getDumpMeta() return readDumpMetaData(); } + @Override + public RepositoryEntry getRepositoryEntry( final RepositoryId repositoryId ) + { + final PathRef tarFile = filePaths.branchMetaPath( SystemConstants.SYSTEM_REPO_ID, SystemConstants.BRANCH_SYSTEM ); + + if ( !exists( tarFile ) ) + { + return null; + } + + final JsonDumpSerializer serializer = new JsonDumpSerializer(); + final NodeId targetNodeId = NodeId.from( repositoryId.toString() ); + + try (TarArchiveInputStream tarInputStream = openStream( tarFile )) + { + TarArchiveEntry entry = tarInputStream.getNextEntry(); + while ( entry != null ) + { + final String content = readEntry( tarInputStream ); + final BranchDumpEntry branchDumpEntry = serializer.toBranchMetaEntry( content ); + + if ( targetNodeId.equals( branchDumpEntry.getNodeId() ) ) + { + final NodeVersionKey nodeVersionKey = branchDumpEntry.getMeta().nodeVersionKey(); + final NodeVersion nodeVersion = get( SystemConstants.SYSTEM_REPO_ID, nodeVersionKey ); + + final Node node = Node.create() + .id( branchDumpEntry.getNodeId() ) + .childOrder( ChildOrder.defaultOrder() ) + .data( nodeVersion.getData() ) + .name( repositoryId.toString() ) + .parentPath( RepositoryConstants.REPOSITORY_STORAGE_PARENT_PATH ) + .permissions( nodeVersion.getPermissions() ) + .attachedBinaries( nodeVersion.getAttachedBinaries() ) + .build(); + + return RepositoryNodeTranslator.toRepository( node ); + } + + entry = tarInputStream.getNextEntry(); + } + } + catch ( IOException e ) + { + throw new RepoDumpException( "Cannot read repository entry from dump", e ); + } + + return null; + } + @Override public void close() throws IOException diff --git a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/DumpReader.java b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/DumpReader.java index dd9d9f3330f..c9e31cfb82c 100644 --- a/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/DumpReader.java +++ b/modules/core/core-repo/src/main/java/com/enonic/xp/repo/impl/dump/reader/DumpReader.java @@ -6,14 +6,15 @@ import com.google.common.io.LineProcessor; import com.enonic.xp.blob.BlobKey; -import com.enonic.xp.node.NodeVersionKey; import com.enonic.xp.branch.Branch; import com.enonic.xp.branch.Branches; import com.enonic.xp.dump.BranchLoadResult; import com.enonic.xp.dump.CommitsLoadResult; import com.enonic.xp.dump.VersionsLoadResult; import com.enonic.xp.node.NodeVersion; +import com.enonic.xp.node.NodeVersionKey; import com.enonic.xp.repo.impl.dump.model.DumpMeta; +import com.enonic.xp.repo.impl.repository.RepositoryEntry; import com.enonic.xp.repository.RepositoryId; import com.enonic.xp.repository.RepositoryIds; @@ -35,4 +36,6 @@ public interface DumpReader ByteSource getBinary( RepositoryId repositoryId, BlobKey blobKey ); DumpMeta getDumpMeta(); + + RepositoryEntry getRepositoryEntry( RepositoryId repositoryId ); } diff --git a/modules/itest/itest-core/src/test/java/com/enonic/xp/core/dump/DumpServiceImplTest.java b/modules/itest/itest-core/src/test/java/com/enonic/xp/core/dump/DumpServiceImplTest.java index 1de297ae84a..fbf3bd5078a 100644 --- a/modules/itest/itest-core/src/test/java/com/enonic/xp/core/dump/DumpServiceImplTest.java +++ b/modules/itest/itest-core/src/test/java/com/enonic/xp/core/dump/DumpServiceImplTest.java @@ -47,6 +47,7 @@ import com.enonic.xp.node.Attributes; import com.enonic.xp.node.BinaryAttachment; import com.enonic.xp.node.CreateNodeParams; +import com.enonic.xp.node.DeleteNodeParams; import com.enonic.xp.node.GetActiveNodeVersionsParams; import com.enonic.xp.node.GetActiveNodeVersionsResult; import com.enonic.xp.node.GetNodeVersionsParams; @@ -1110,4 +1111,102 @@ private Node updateNode( final Node node ) n.data.setLong( "update", UPDATE_COUNTER.incrementAndGet() ); } ).build() ); } + + @Test + void partial_load_single_repository() + { + final RepositoryEntry repo1 = NodeHelper.runAsAdmin( () -> doCreateRepository( RepositoryId.from( "repo-to-load" ), false ) ); + final RepositoryEntry repo2 = NodeHelper.runAsAdmin( () -> doCreateRepository( RepositoryId.from( "repo-to-keep" ), false ) ); + + final Context repo1Context = ContextBuilder.from( ContextAccessor.current() ) + .repositoryId( repo1.getId() ) + .branch( RepositoryConstants.MASTER_BRANCH ) + .build(); + + final Context repo2Context = ContextBuilder.from( ContextAccessor.current() ) + .repositoryId( repo2.getId() ) + .branch( RepositoryConstants.MASTER_BRANCH ) + .build(); + + final Node nodeInRepo1 = repo1Context.callWith( () -> createNode( NodePath.ROOT, "node-in-repo1" ) ); + final Node nodeInRepo2 = repo2Context.callWith( () -> createNode( NodePath.ROOT, "node-in-repo2" ) ); + + NodeHelper.runAsAdmin( () -> doDump( SystemDumpParams.create().dumpName( "partialDump" ).build() ) ); + + // Delete node from repo1 after dump - this will be restored by partial load + repo1Context.runWith( () -> nodeService.delete( DeleteNodeParams.create().nodeId( nodeInRepo1.id() ).build() ) ); + repo1Context.runWith( () -> nodeService.refresh( RefreshMode.ALL ) ); + assertNull( repo1Context.callWith( () -> getNode( nodeInRepo1.id() ) ) ); + + // Add new node to repo1 after dump - this should be removed by partial load + final Node newNodeInRepo1 = repo1Context.callWith( () -> createNode( NodePath.ROOT, "new-node-in-repo1" ) ); + + // Add new node to repo2 after dump - this should survive partial load + final Node newNodeInRepo2 = repo2Context.callWith( () -> createNode( NodePath.ROOT, "new-node-in-repo2" ) ); + + // Partial load only repo1 + NodeHelper.runAsAdmin( () -> this.dumpService.load( SystemLoadParams.create() + .dumpName( "partialDump" ) + .includeVersions( true ) + .repositories( RepositoryIds.from( repo1.getId() ) ) + .build() ) ); + + // Verify repo1 was restored from dump (original node exists, new node removed) + final Node restoredNodeInRepo1 = repo1Context.callWith( () -> getNode( nodeInRepo1.id() ) ); + assertNotNull( restoredNodeInRepo1 ); + assertNull( repo1Context.callWith( () -> getNode( newNodeInRepo1.id() ) ) ); + + // Verify repo2 was NOT touched (both old and new nodes exist) + final Node existingNodeInRepo2 = repo2Context.callWith( () -> getNode( nodeInRepo2.id() ) ); + assertNotNull( existingNodeInRepo2 ); + final Node newNodeStillExists = repo2Context.callWith( () -> getNode( newNodeInRepo2.id() ) ); + assertNotNull( newNodeStillExists ); + } + + @Test + void partial_load_preserves_repositories_outside_list() + { + final RepositoryEntry repoInsideList = + NodeHelper.runAsAdmin( () -> doCreateRepository( RepositoryId.from( "repo-inside-list" ), false ) ); + final RepositoryEntry repoOutsideList = + NodeHelper.runAsAdmin( () -> doCreateRepository( RepositoryId.from( "repo-outside-list" ), false ) ); + + NodeHelper.runAsAdmin( () -> doDump( SystemDumpParams.create().dumpName( "partialDump2" ).build() ) ); + + final Repositories reposBefore = NodeHelper.runAsAdmin( this::doListRepositories ); + + // Partial load only repoInsideList + NodeHelper.runAsAdmin( () -> this.dumpService.load( SystemLoadParams.create() + .dumpName( "partialDump2" ) + .includeVersions( true ) + .repositories( RepositoryIds.from( repoInsideList.getId() ) ) + .build() ) ); + + final Repositories reposAfter = NodeHelper.runAsAdmin( this::doListRepositories ); + + // Both repositories should still exist + assertThat( reposAfter ).map( Repository::getId ).contains( repoInsideList.getId(), repoOutsideList.getId() ); + assertEquals( reposBefore.getIds().getSize(), reposAfter.getIds().getSize() ); + } + + @Test + void partial_load_does_not_delete_system_repo() + { + final RepositoryEntry userRepo = NodeHelper.runAsAdmin( () -> doCreateRepository( RepositoryId.from( "user-repo" ), false ) ); + + NodeHelper.runAsAdmin( () -> doDump( SystemDumpParams.create().dumpName( "partialDump3" ).build() ) ); + + // Partial load requesting system-repo should not touch it + NodeHelper.runAsAdmin( () -> this.dumpService.load( SystemLoadParams.create() + .dumpName( "partialDump3" ) + .includeVersions( true ) + .repositories( + RepositoryIds.from( userRepo.getId(), SystemConstants.SYSTEM_REPO_ID ) ) + .build() ) ); + + final Repositories reposAfter = NodeHelper.runAsAdmin( this::doListRepositories ); + + // System repo should still exist + assertThat( reposAfter ).map( Repository::getId ).contains( SystemConstants.SYSTEM_REPO_ID ); + } } diff --git a/modules/lib/package-lock.json b/modules/lib/package-lock.json index 67487ae02d1..d357793aae4 100644 --- a/modules/lib/package-lock.json +++ b/modules/lib/package-lock.json @@ -459,6 +459,7 @@ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -531,6 +532,7 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -735,6 +737,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1132,6 +1135,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2019,6 +2023,7 @@ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -2918,6 +2923,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3002,6 +3008,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java index c60cc4df725..837e49040b5 100644 --- a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java +++ b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java @@ -21,6 +21,8 @@ import com.enonic.xp.impl.server.rest.task.UpgradeRunnableTask; import com.enonic.xp.impl.server.rest.task.VacuumCommand; import com.enonic.xp.jaxrs.JaxRsComponent; +import com.enonic.xp.repository.RepositoryId; +import com.enonic.xp.repository.RepositoryIds; import com.enonic.xp.security.RoleKeys; import com.enonic.xp.task.SubmitLocalTaskParams; import com.enonic.xp.task.TaskId; @@ -64,10 +66,15 @@ public TaskResultJson dump( final SystemDumpRequestJson params ) @Path("load") public TaskResultJson load( final SystemLoadRequestJson params ) { + final RepositoryIds repositories = params.getRepositories() != null ? params.getRepositories() + .stream() + .map( RepositoryId::from ) + .collect( RepositoryIds.collector() ) : null; + final LoadRunnableTask task = LoadRunnableTask.create() .name( params.getName() ) .upgrade( params.isUpgrade() ) - .archive( params.isArchive() ) + .archive( params.isArchive() ).repositories( repositories ) .taskService( taskService ) .dumpService( dumpService ) .build(); diff --git a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/model/SystemLoadRequestJson.java b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/model/SystemLoadRequestJson.java index 2965ff7ffe3..bf5d17036c0 100644 --- a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/model/SystemLoadRequestJson.java +++ b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/model/SystemLoadRequestJson.java @@ -1,5 +1,7 @@ package com.enonic.xp.impl.server.rest.model; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonProperty; public class SystemLoadRequestJson @@ -10,12 +12,16 @@ public class SystemLoadRequestJson private final boolean archive; + private final List repositories; + public SystemLoadRequestJson( @JsonProperty("name") final String name, @JsonProperty("upgrade") final boolean upgrade, - @JsonProperty("archive") final boolean archive ) + @JsonProperty("archive") final boolean archive, + @JsonProperty("repositories") final List repositories ) { this.name = name; this.upgrade = upgrade; this.archive = archive; + this.repositories = repositories; } public String getName() @@ -32,4 +38,9 @@ public boolean isArchive() { return archive; } + + public List getRepositories() + { + return repositories; + } } diff --git a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTask.java b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTask.java index 2d75455c458..306a0b92500 100644 --- a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTask.java +++ b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTask.java @@ -6,6 +6,7 @@ import com.enonic.xp.dump.SystemLoadResult; import com.enonic.xp.impl.server.rest.model.SystemLoadResultJson; import com.enonic.xp.impl.server.rest.task.listener.SystemLoadListenerImpl; +import com.enonic.xp.repository.RepositoryIds; import com.enonic.xp.task.ProgressReportParams; import com.enonic.xp.task.ProgressReporter; import com.enonic.xp.task.RunnableTask; @@ -21,6 +22,8 @@ public class LoadRunnableTask private final boolean archive; + private final RepositoryIds repositories; + private final TaskService taskService; private final DumpService dumpService; @@ -32,6 +35,7 @@ private LoadRunnableTask( Builder builder ) this.name = builder.name; this.upgrade = builder.upgrade; this.archive = builder.archive; + this.repositories = builder.repositories; this.taskService = builder.taskService; this.dumpService = builder.dumpService; } @@ -61,7 +65,7 @@ private SystemLoadResultJson doLoadFromSystemDump() final SystemLoadResult systemLoadResult = this.dumpService.load( SystemLoadParams.create() .dumpName( name ) .upgrade( upgrade ) - .archive( archive ) + .archive( archive ).repositories( repositories ) .includeVersions( true ) .listener( loadDumpListener ) .build() ); @@ -77,6 +81,8 @@ public static class Builder private boolean archive; + private RepositoryIds repositories; + private DumpService dumpService; private TaskService taskService; @@ -105,6 +111,12 @@ public Builder archive( boolean archive ) return this; } + public Builder repositories( RepositoryIds repositories ) + { + this.repositories = repositories; + return this; + } + public Builder dumpService( final DumpService dumpService ) { this.dumpService = dumpService; diff --git a/modules/server/server-rest/src/test/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTaskTest.java b/modules/server/server-rest/src/test/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTaskTest.java index 659f4f82198..56439fabb55 100644 --- a/modules/server/server-rest/src/test/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTaskTest.java +++ b/modules/server/server-rest/src/test/java/com/enonic/xp/impl/server/rest/task/LoadRunnableTaskTest.java @@ -3,6 +3,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,6 +22,7 @@ import com.enonic.xp.home.HomeDirSupport; import com.enonic.xp.impl.server.rest.model.SystemLoadRequestJson; import com.enonic.xp.repository.RepositoryId; +import com.enonic.xp.repository.RepositoryIds; import com.enonic.xp.support.JsonTestHelper; import com.enonic.xp.task.ProgressReportParams; import com.enonic.xp.task.ProgressReporter; @@ -28,6 +30,7 @@ import com.enonic.xp.task.TaskInfo; import com.enonic.xp.task.TaskService; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -61,13 +64,18 @@ void setUp() private LoadRunnableTask createTask( final SystemLoadRequestJson params ) + { + return createTask( params, null ); + } + + private LoadRunnableTask createTask( final SystemLoadRequestJson params, final RepositoryIds repositories ) { return LoadRunnableTask.create() .taskService( taskService ) .dumpService( dumpService ) .name( params.getName() ) .upgrade( params.isUpgrade() ) - .archive( params.isArchive() ) + .archive( params.isArchive() ).repositories( repositories ) .build(); } @@ -97,7 +105,7 @@ void load_system() when( this.dumpService.load( any( SystemLoadParams.class ) ) ).thenReturn( systemLoadResult ); final LoadRunnableTask task = - createTask( new SystemLoadRequestJson( params.getDumpName(), params.isUpgrade(), params.isArchive() ) ); + createTask( new SystemLoadRequestJson( params.getDumpName(), params.isUpgrade(), params.isArchive(), null ) ); ProgressReporter progressReporter = mock( ProgressReporter.class ); @@ -111,4 +119,36 @@ void load_system() jsonTestHelper.stringToJson( result.getMessage() ) ); } + + @Test + void load_with_specific_repositories() + throws Exception + { + Files.createDirectory( dumpDir.resolve( "name" ) ); + + final RepositoryIds repositories = RepositoryIds.from( RepositoryId.from( "my-repo" ) ); + + SystemLoadResult systemLoadResult = SystemLoadResult.create() + .add( RepoLoadResult.create( RepositoryId.from( "my-repo" ) ) + .add( BranchLoadResult.create( Branch.create().value( "master" ).build() ).successful( 1L ).build() ) + .versions( VersionsLoadResult.create().successful( 1L ).build() ) + .build() ) + .build(); + + final TaskId taskId = TaskId.from( "taskId" ); + when( taskService.getTaskInfo( taskId ) ).thenReturn( + TaskInfo.create().id( taskId ).name( "load" ).application( ApplicationKey.SYSTEM ).startTime( Instant.now() ).build() ); + + final ArgumentCaptor loadParamsCaptor = ArgumentCaptor.forClass( SystemLoadParams.class ); + when( this.dumpService.load( loadParamsCaptor.capture() ) ).thenReturn( systemLoadResult ); + + final LoadRunnableTask task = createTask( new SystemLoadRequestJson( "name", false, true, List.of( "my-repo" ) ), repositories ); + + ProgressReporter progressReporter = mock( ProgressReporter.class ); + + task.run( taskId, progressReporter ); + + final SystemLoadParams capturedParams = loadParamsCaptor.getValue(); + assertEquals( repositories, capturedParams.getRepositories() ); + } } From bafe1da627e269091c38a6fe51291b1dff2e42b1 Mon Sep 17 00:00:00 2001 From: Bradnitski Viachaslau Date: Wed, 4 Feb 2026 13:20:54 +0100 Subject: [PATCH 2/2] Update modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/enonic/xp/impl/server/rest/SystemResource.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java index 837e49040b5..67941d097bc 100644 --- a/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java +++ b/modules/server/server-rest/src/main/java/com/enonic/xp/impl/server/rest/SystemResource.java @@ -74,7 +74,8 @@ public TaskResultJson load( final SystemLoadRequestJson params ) final LoadRunnableTask task = LoadRunnableTask.create() .name( params.getName() ) .upgrade( params.isUpgrade() ) - .archive( params.isArchive() ).repositories( repositories ) + .archive( params.isArchive() ) + .repositories( repositories ) .taskService( taskService ) .dumpService( dumpService ) .build();