mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-18 03:01:05 +01:00
Add the repository import and export with metadata for Subversion repositories (#1501)
* Add store exporter to collect the repository metadata * Add EnvironmentInformationXmlGenerator * Collect export data and put into compressed tar archive output stream * Create full repository export endpoint. * Add full repository export to ui * Ignore irrelevant files from config store directory * write metadata stores to file since a baos could teardown the server memory * Migrate store name for git lfs files (#1504) Changes the directory name for the git LFS blob store by removing the repository id from the store name. This is necessary for im- and exports of lfs blob stores, because the original name had the repository id as a part of it and therefore the old store would not be found when the repository is imported with another id. Existing blob files will be moved to the new store location by an update step. Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> * Introduce util for migrations (#1505) With this util it is more simple to rename or delete stores. * Rename files in export Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
@@ -30,9 +30,9 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.importexport.FullScmRepositoryExporter;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
@@ -43,6 +43,7 @@ import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
@@ -54,25 +55,25 @@ import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.StreamingOutput;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.checkSupport;
|
||||
import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.type;
|
||||
|
||||
public class RepositoryExportResource {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepositoryExportResource.class);
|
||||
|
||||
private final RepositoryManager manager;
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final FullScmRepositoryExporter fullScmRepositoryExporter;
|
||||
|
||||
@Inject
|
||||
public RepositoryExportResource(RepositoryManager manager,
|
||||
RepositoryServiceFactory serviceFactory) {
|
||||
RepositoryServiceFactory serviceFactory, FullScmRepositoryExporter fullScmRepositoryExporter) {
|
||||
this.manager = manager;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.fullScmRepositoryExporter = fullScmRepositoryExporter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,9 +81,9 @@ public class RepositoryExportResource {
|
||||
* only be used, if the repository type supports the {@link Command#BUNDLE}.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param namespace of the repository
|
||||
* @param name of the repository
|
||||
* @param type of the repository
|
||||
* @param namespace namespace of the repository
|
||||
* @param name name of the repository
|
||||
* @param type type of the repository
|
||||
* @return response with readable stream of repository dump
|
||||
* @since 2.13.0
|
||||
*/
|
||||
@@ -113,16 +114,72 @@ public class RepositoryExportResource {
|
||||
public Response exportRepository(@Context UriInfo uriInfo,
|
||||
@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("type") String type,
|
||||
@Pattern(regexp = "\\w{1,10}") @PathParam("type") String type,
|
||||
@DefaultValue("false") @QueryParam("compressed") boolean compressed
|
||||
) {
|
||||
Repository repository = getVerifiedRepository(namespace, name, type);
|
||||
return exportRepository(repository, compressed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports an existing repository with all additional metadata and environment information. The method can
|
||||
* only be used, if the repository type supports the {@link Command#BUNDLE}.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param namespace namespace of the repository
|
||||
* @param name name of the repository
|
||||
* @param type type of the repository
|
||||
* @return response with readable stream of repository dump
|
||||
* @since 2.13.0
|
||||
*/
|
||||
@GET
|
||||
@Path("{type}/full")
|
||||
@Consumes(VndMediaType.REPOSITORY)
|
||||
@Operation(summary = "Exports the repository", description = "Exports the repository with metadata and environment information.", tags = "Repository")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Repository export was successful"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "not authenticated / invalid credentials"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "not authorized, the current user has no privileges to read the repository"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response exportFullRepository(@Context UriInfo uriInfo,
|
||||
@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@Pattern(regexp = "\\w{1,10}") @PathParam("type") String type
|
||||
) {
|
||||
Repository repository = getVerifiedRepository(namespace, name, type);
|
||||
StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os);
|
||||
|
||||
return Response
|
||||
.ok(output, "application/x-gzip")
|
||||
.header("content-disposition", createContentDispositionHeaderValue(repository, "tar.gz"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Repository getVerifiedRepository(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("type") @Pattern(regexp = "\\w{1,10}") String type) {
|
||||
Repository repository = manager.get(new NamespaceAndName(namespace, name));
|
||||
RepositoryPermissions.read().check(repository);
|
||||
|
||||
if (!type.equals(repository.getType())) {
|
||||
throw new WrongTypeException(repository);
|
||||
}
|
||||
Type repositoryType = type(manager, type);
|
||||
checkSupport(repositoryType, Command.BUNDLE);
|
||||
|
||||
return exportRepository(repository, compressed);
|
||||
return repository;
|
||||
}
|
||||
|
||||
private Response exportRepository(Repository repository, boolean compressed) {
|
||||
@@ -140,24 +197,42 @@ public class RepositoryExportResource {
|
||||
}
|
||||
};
|
||||
|
||||
return createResponse(repository, compressed, output);
|
||||
}
|
||||
|
||||
private Response createResponse(Repository repository, boolean compressed, StreamingOutput output) {
|
||||
return Response
|
||||
.ok(output, compressed ? "application/x-gzip" : MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header("content-disposition", createContentDispositionHeaderValue(repository, compressed))
|
||||
.header("content-disposition", createContentDispositionHeaderValue(repository, compressed ? "dump.gz" : "dump"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String createContentDispositionHeaderValue(Repository repository, boolean compressed) {
|
||||
private String createContentDispositionHeaderValue(Repository repository, String filetype) {
|
||||
String timestamp = createFormattedTimestamp();
|
||||
return String.format(
|
||||
"attachment; filename = %s-%s-%s.%s",
|
||||
repository.getNamespace(),
|
||||
repository.getName(),
|
||||
timestamp,
|
||||
compressed ? "dump.gz" : "dump"
|
||||
filetype
|
||||
);
|
||||
}
|
||||
|
||||
private String createFormattedTimestamp() {
|
||||
return Instant.now().toString().replace(":", "-").split("\\.")[0];
|
||||
}
|
||||
|
||||
private static class WrongTypeException extends BadRequestException {
|
||||
|
||||
private static final String CODE = "4hSNNTBiu1";
|
||||
|
||||
public WrongTypeException(Repository repository) {
|
||||
super(entity(repository).build(), "illegal type for repository");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.importexport.FullScmRepositoryImporter;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryImportEvent;
|
||||
@@ -63,6 +64,7 @@ import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.ValidationUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
import sonia.scm.web.api.DtoValidator;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Email;
|
||||
@@ -87,8 +89,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.checkSupport;
|
||||
@@ -103,18 +103,21 @@ public class RepositoryImportResource {
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final ScmEventBus eventBus;
|
||||
private final FullScmRepositoryImporter fullScmRepositoryImporter;
|
||||
|
||||
@Inject
|
||||
public RepositoryImportResource(RepositoryManager manager,
|
||||
RepositoryDtoToRepositoryMapper mapper,
|
||||
RepositoryServiceFactory serviceFactory,
|
||||
ResourceLinks resourceLinks,
|
||||
ScmEventBus eventBus) {
|
||||
ScmEventBus eventBus,
|
||||
FullScmRepositoryImporter fullScmRepositoryImporter) {
|
||||
this.manager = manager;
|
||||
this.mapper = mapper;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.eventBus = eventBus;
|
||||
this.fullScmRepositoryImporter = fullScmRepositoryImporter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,6 +257,62 @@ public class RepositoryImportResource {
|
||||
return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a repository as SCM-Manager provided import archive. The method can
|
||||
* only be used, if the repository type supports the {@link Command#UNBUNDLE}. The
|
||||
* method will return a location header with the url to the imported
|
||||
* repository.
|
||||
*
|
||||
* @param uriInfo uri info
|
||||
* @param type repository type
|
||||
* @param input multi part form data which should contain a valid repository dto and the input stream of the bundle
|
||||
* @return empty response with location header which points to the imported
|
||||
* repository
|
||||
* @since 2.13.0
|
||||
*/
|
||||
@POST
|
||||
@Path("{type}/full")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
@Operation(
|
||||
summary = "Import repository from SCM-Manager repository archive",
|
||||
description = "Imports the repository with metadata from the provided bundle.",
|
||||
tags = "Repository"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "Repository import was successful"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "not authenticated / invalid credentials"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "not authorized, the current user has no privileges to import repositories"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response importFullRepository(@Context UriInfo uriInfo,
|
||||
@Pattern(regexp = "\\w{1,10}") @PathParam("type") String type,
|
||||
MultipartFormDataInput input) {
|
||||
RepositoryPermissions.create().check();
|
||||
Repository createdRepository = importFullRepositoryFromInput(input);
|
||||
return Response.created(URI.create(resourceLinks.repository().self(createdRepository.getNamespace(), createdRepository.getName()))).build();
|
||||
}
|
||||
|
||||
private Repository importFullRepositoryFromInput(MultipartFormDataInput input) {
|
||||
Map<String, List<InputPart>> formParts = input.getFormDataMap();
|
||||
InputStream inputStream = extractInputStream(formParts);
|
||||
RepositoryDto repositoryDto = extractRepositoryDto(formParts);
|
||||
return fullScmRepositoryImporter.importFromStream(mapper.map(repositoryDto), inputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start bundle import.
|
||||
*
|
||||
@@ -264,12 +323,8 @@ public class RepositoryImportResource {
|
||||
*/
|
||||
private Repository doImportFromBundle(String type, MultipartFormDataInput input, boolean compressed) {
|
||||
Map<String, List<InputPart>> formParts = input.getFormDataMap();
|
||||
RepositoryDto repositoryDto = extractFromInputPart(formParts.get("repository"), RepositoryDto.class);
|
||||
InputStream inputStream = extractFromInputPart(formParts.get("bundle"), InputStream.class);
|
||||
|
||||
checkNotNull(repositoryDto, "repository data is required");
|
||||
checkNotNull(inputStream, "bundle inputStream is required");
|
||||
checkArgument(!Strings.isNullOrEmpty(repositoryDto.getName()), "request does not contain name of the repository");
|
||||
InputStream inputStream = extractInputStream(formParts);
|
||||
RepositoryDto repositoryDto = extractRepositoryDto(formParts);
|
||||
|
||||
Type t = type(manager, type);
|
||||
checkSupport(t, Command.UNBUNDLE);
|
||||
@@ -315,6 +370,25 @@ public class RepositoryImportResource {
|
||||
};
|
||||
}
|
||||
|
||||
private RepositoryDto extractRepositoryDto(Map<String, List<InputPart>> formParts) {
|
||||
RepositoryDto repositoryDto = extractFromInputPart(formParts.get("repository"), RepositoryDto.class);
|
||||
checkNotNull(repositoryDto, "repository data is required");
|
||||
DtoValidator.validate(repositoryDto);
|
||||
return repositoryDto;
|
||||
}
|
||||
|
||||
private void checkNotNull(Object object, String errorMessage) {
|
||||
if (object == null) {
|
||||
throw new WebApplicationException(errorMessage, 400);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream extractInputStream(Map<String, List<InputPart>> formParts) {
|
||||
InputStream inputStream = extractFromInputPart(formParts.get("bundle"), InputStream.class);
|
||||
checkNotNull(inputStream, "bundle inputStream is required");
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
private <T> T extractFromInputPart(List<InputPart> input, Class<T> type) {
|
||||
try {
|
||||
if (input != null && !input.isEmpty()) {
|
||||
@@ -343,6 +417,7 @@ public class RepositoryImportResource {
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160")
|
||||
public static class RepositoryImportDto extends RepositoryDto implements ImportRepositoryDto {
|
||||
|
||||
@NotEmpty
|
||||
private String importUrl;
|
||||
private String username;
|
||||
|
||||
@@ -104,8 +104,9 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
linksBuilder.array(protocolLinks);
|
||||
}
|
||||
|
||||
if (repositoryService.isSupported(Command.BUNDLE)) {
|
||||
if (repositoryService.isSupported(Command.BUNDLE) && RepositoryPermissions.export(repository).isPermitted()) {
|
||||
linksBuilder.single(link("export", resourceLinks.repository().export(repository.getNamespace(), repository.getName(), repository.getType())));
|
||||
linksBuilder.single(link("fullExport", resourceLinks.repository().fullExport(repository.getNamespace(), repository.getName(), repository.getType())));
|
||||
}
|
||||
|
||||
if (repositoryService.isSupported(Command.TAGS)) {
|
||||
|
||||
@@ -53,6 +53,7 @@ public abstract class RepositoryTypeToRepositoryTypeDtoMapper extends BaseMapper
|
||||
}
|
||||
if (repositoryType.getSupportedCommands().contains(Command.UNBUNDLE)) {
|
||||
linksBuilder.array(Link.linkBuilder("import", resourceLinks.repository().importFromBundle(repositoryType.getName())).withName("bundle").build());
|
||||
linksBuilder.array(Link.linkBuilder("import", resourceLinks.repository().fullImport(repositoryType.getName())).withName("fullImport").build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -372,6 +372,11 @@ class ResourceLinks {
|
||||
String importFromBundle(String type) {
|
||||
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFromBundle").parameters(type).href();
|
||||
}
|
||||
|
||||
String fullImport(String type) {
|
||||
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFullRepository").parameters(type).href();
|
||||
}
|
||||
|
||||
String archive(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("archive").parameters().href();
|
||||
}
|
||||
@@ -383,6 +388,10 @@ class ResourceLinks {
|
||||
String export(String namespace, String name, String type) {
|
||||
return repositoryExportLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("export").parameters().method("exportRepository").parameters(type).href();
|
||||
}
|
||||
|
||||
String fullExport(String namespace, String name, String type) {
|
||||
return repositoryExportLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("export").parameters().method("exportFullRepository").parameters(type).href();
|
||||
}
|
||||
}
|
||||
|
||||
RepositoryCollectionLinks repositoryCollection() {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
import sonia.scm.util.SystemUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXB;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class EnvironmentInformationXmlGenerator {
|
||||
|
||||
private final PluginManager pluginManager;
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
@Inject
|
||||
public EnvironmentInformationXmlGenerator(PluginManager pluginManager, SCMContextProvider contextProvider) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
public byte[] generate() {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
ScmEnvironment scmEnvironment = new ScmEnvironment();
|
||||
writeCoreInformation(scmEnvironment);
|
||||
writePluginInformation(scmEnvironment);
|
||||
JAXB.marshal(scmEnvironment, baos);
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new ExportFailedException(
|
||||
ContextEntry.ContextBuilder.noContext(),
|
||||
"Could not generate SCM-Manager environment description.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCoreInformation(ScmEnvironment scmEnvironment) {
|
||||
scmEnvironment.setCoreVersion(contextProvider.getVersion());
|
||||
scmEnvironment.setArch(SystemUtil.getArch());
|
||||
scmEnvironment.setOs(SystemUtil.getOS());
|
||||
}
|
||||
|
||||
private void writePluginInformation(ScmEnvironment scmEnvironment) {
|
||||
List<EnvironmentPluginDescriptor> plugins = new ArrayList<>();
|
||||
for (InstalledPlugin plugin : pluginManager.getInstalled()) {
|
||||
PluginInformation pluginInformation = plugin.getDescriptor().getInformation();
|
||||
plugins.add(new EnvironmentPluginDescriptor(pluginInformation.getName(), pluginInformation.getVersion()));
|
||||
}
|
||||
scmEnvironment.setPlugins(new EnvironmentPluginsDescriptor(plugins));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
@XmlRootElement(name = "plugin")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Setter
|
||||
@Getter
|
||||
class EnvironmentPluginDescriptor {
|
||||
private String name;
|
||||
private String version;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.List;
|
||||
|
||||
@XmlRootElement(name = "plugins")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Setter
|
||||
@Getter
|
||||
class EnvironmentPluginsDescriptor {
|
||||
private List<EnvironmentPluginDescriptor> plugin;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.work.WorkdirProvider;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class FullScmRepositoryExporter {
|
||||
|
||||
static final String SCM_ENVIRONMENT_FILE_NAME = "scm-environment.xml";
|
||||
static final String METADATA_FILE_NAME = "metadata.xml";
|
||||
static final String STORE_DATA_FILE_NAME = "store-data.tar";
|
||||
private final EnvironmentInformationXmlGenerator environmentGenerator;
|
||||
private final RepositoryMetadataXmlGenerator metadataGenerator;
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final TarArchiveRepositoryStoreExporter storeExporter;
|
||||
private final WorkdirProvider workdirProvider;
|
||||
|
||||
@Inject
|
||||
public FullScmRepositoryExporter(EnvironmentInformationXmlGenerator environmentGenerator,
|
||||
RepositoryMetadataXmlGenerator metadataGenerator,
|
||||
RepositoryServiceFactory serviceFactory,
|
||||
TarArchiveRepositoryStoreExporter storeExporter, WorkdirProvider workdirProvider) {
|
||||
this.environmentGenerator = environmentGenerator;
|
||||
this.metadataGenerator = metadataGenerator;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.storeExporter = storeExporter;
|
||||
this.workdirProvider = workdirProvider;
|
||||
}
|
||||
|
||||
public void export(Repository repository, OutputStream outputStream) {
|
||||
try (
|
||||
RepositoryService service = serviceFactory.create(repository);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(outputStream);
|
||||
GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos);
|
||||
TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos);
|
||||
) {
|
||||
writeEnvironmentData(taos);
|
||||
writeMetadata(repository, taos);
|
||||
writeRepository(service, taos);
|
||||
writeStoreData(repository, taos);
|
||||
taos.finish();
|
||||
} catch (IOException e) {
|
||||
throw new ExportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Could not export repository with metadata",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEnvironmentData(TarArchiveOutputStream taos) throws IOException {
|
||||
byte[] envBytes = environmentGenerator.generate();
|
||||
TarArchiveEntry entry = new TarArchiveEntry(SCM_ENVIRONMENT_FILE_NAME);
|
||||
entry.setSize(envBytes.length);
|
||||
taos.putArchiveEntry(entry);
|
||||
taos.write(envBytes);
|
||||
taos.closeArchiveEntry();
|
||||
}
|
||||
|
||||
private void writeMetadata(Repository repository, TarArchiveOutputStream taos) throws IOException {
|
||||
byte[] metadataBytes = metadataGenerator.generate(repository);
|
||||
TarArchiveEntry entry = new TarArchiveEntry(METADATA_FILE_NAME);
|
||||
entry.setSize(metadataBytes.length);
|
||||
taos.putArchiveEntry(entry);
|
||||
taos.write(metadataBytes);
|
||||
taos.closeArchiveEntry();
|
||||
}
|
||||
|
||||
private void writeRepository(RepositoryService service, TarArchiveOutputStream taos) throws IOException {
|
||||
File newWorkdir = workdirProvider.createNewWorkdir();
|
||||
try {
|
||||
File repositoryFile = Files.createFile(Paths.get(newWorkdir.getPath(), "repository")).toFile();
|
||||
try (FileOutputStream repositoryFos = new FileOutputStream(repositoryFile)) {
|
||||
service.getBundleCommand().bundle(repositoryFos);
|
||||
}
|
||||
TarArchiveEntry entry = new TarArchiveEntry(service.getRepository().getName() + ".dump");
|
||||
entry.setSize(repositoryFile.length());
|
||||
taos.putArchiveEntry(entry);
|
||||
Files.copy(repositoryFile.toPath(), taos);
|
||||
taos.closeArchiveEntry();
|
||||
} finally {
|
||||
IOUtil.deleteSilently(newWorkdir);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeStoreData(Repository repository, TarArchiveOutputStream taos) throws IOException {
|
||||
File newWorkdir = workdirProvider.createNewWorkdir();
|
||||
try {
|
||||
File metadata = Files.createFile(Paths.get(newWorkdir.getPath(), "metadata")).toFile();
|
||||
try (FileOutputStream metadataFos = new FileOutputStream(metadata)) {
|
||||
storeExporter.export(repository, metadataFos);
|
||||
}
|
||||
TarArchiveEntry entry = new TarArchiveEntry(STORE_DATA_FILE_NAME);
|
||||
entry.setSize(metadata.length());
|
||||
taos.putArchiveEntry(entry);
|
||||
Files.copy(metadata.toPath(), taos);
|
||||
taos.closeArchiveEntry();
|
||||
} finally {
|
||||
IOUtil.deleteSilently(newWorkdir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.bind.JAXB;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static sonia.scm.importexport.FullScmRepositoryExporter.SCM_ENVIRONMENT_FILE_NAME;
|
||||
import static sonia.scm.importexport.FullScmRepositoryExporter.STORE_DATA_FILE_NAME;
|
||||
|
||||
public class FullScmRepositoryImporter {
|
||||
|
||||
private static final int _1_MB = 1000000;
|
||||
|
||||
private final RepositoryServiceFactory serviceFactory;
|
||||
private final RepositoryManager repositoryManager;
|
||||
private final ScmEnvironmentCompatibilityChecker compatibilityChecker;
|
||||
private final TarArchiveRepositoryStoreImporter storeImporter;
|
||||
|
||||
@Inject
|
||||
public FullScmRepositoryImporter(RepositoryServiceFactory serviceFactory,
|
||||
RepositoryManager repositoryManager,
|
||||
ScmEnvironmentCompatibilityChecker compatibilityChecker,
|
||||
TarArchiveRepositoryStoreImporter storeImporter) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.compatibilityChecker = compatibilityChecker;
|
||||
this.storeImporter = storeImporter;
|
||||
}
|
||||
|
||||
public Repository importFromStream(Repository repository, InputStream inputStream) {
|
||||
try {
|
||||
if (inputStream.available() > 0) {
|
||||
try (
|
||||
BufferedInputStream bif = new BufferedInputStream(inputStream);
|
||||
GzipCompressorInputStream gcis = new GzipCompressorInputStream(bif);
|
||||
TarArchiveInputStream tais = new TarArchiveInputStream(gcis)
|
||||
) {
|
||||
checkScmEnvironment(repository, tais);
|
||||
skipRepositoryMetadata(tais);
|
||||
Repository createdRepository = importRepositoryFromFile(repository, tais);
|
||||
importStoresForCreatedRepository(createdRepository, tais);
|
||||
return createdRepository;
|
||||
}
|
||||
} else {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Stream to import from is empty."
|
||||
);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Could not import repository data from stream; got io exception while reading",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void importStoresForCreatedRepository(Repository repository, TarArchiveInputStream tais) throws IOException {
|
||||
ArchiveEntry metadataEntry = tais.getNextEntry();
|
||||
if (metadataEntry.getName().equals(STORE_DATA_FILE_NAME) && !metadataEntry.isDirectory()) {
|
||||
// Inside the repository tar archive stream is another tar archive.
|
||||
// The nested tar archive is wrapped in another TarArchiveInputStream inside the storeImporter
|
||||
storeImporter.importFromTarArchive(repository, tais);
|
||||
} else {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Invalid import format. Missing metadata file 'scm-metadata.tar' in tar."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Repository importRepositoryFromFile(Repository repository, TarArchiveInputStream tais) throws IOException {
|
||||
ArchiveEntry repositoryEntry = tais.getNextEntry();
|
||||
if (repositoryEntry.getName().endsWith(".dump") && !repositoryEntry.isDirectory()) {
|
||||
return repositoryManager.create(repository, repo -> {
|
||||
try (RepositoryService service = serviceFactory.create(repo)) {
|
||||
service.getUnbundleCommand().unbundle(new NoneClosingInputStream(tais));
|
||||
} catch (IOException e) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Repository import failed. Could not import repository from file.",
|
||||
e
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Invalid import format. Missing repository dump file."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkScmEnvironment(Repository repository, TarArchiveInputStream tais) throws IOException {
|
||||
ArchiveEntry environmentEntry = tais.getNextEntry();
|
||||
if (environmentEntry.getName().equals(SCM_ENVIRONMENT_FILE_NAME) && !environmentEntry.isDirectory() && environmentEntry.getSize() < _1_MB) {
|
||||
boolean validEnvironment = compatibilityChecker.check(JAXB.unmarshal(new NoneClosingInputStream(tais), ScmEnvironment.class));
|
||||
if (!validEnvironment) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Incompatible SCM-Manager environment. Could not import file."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Invalid import format. Missing SCM-Manager environment description file 'scm-environment.xml' or file too big."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void skipRepositoryMetadata(TarArchiveInputStream tais) throws IOException {
|
||||
tais.getNextEntry();
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S4929") // we only want to override close here
|
||||
static class NoneClosingInputStream extends FilterInputStream {
|
||||
|
||||
NoneClosingInputStream(InputStream delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Avoid closing stream because JAXB tries to close the stream
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
|
||||
import javax.xml.bind.JAXB;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
class RepositoryMetadataXmlGenerator {
|
||||
|
||||
byte[] generate(Repository repository) {
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
RepositoryMetadata metadata = new RepositoryMetadata(repository);
|
||||
JAXB.marshal(metadata, baos);
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new ExportFailedException(
|
||||
ContextEntry.ContextBuilder.noContext(),
|
||||
"Could not generate SCM-Manager environment description.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@XmlRootElement(name = "metadata")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class RepositoryMetadata {
|
||||
|
||||
private String namespace;
|
||||
private String name;
|
||||
private String type;
|
||||
private String contact;
|
||||
private String description;
|
||||
private Collection<RepositoryPermission> permissions;
|
||||
|
||||
public RepositoryMetadata(Repository repository) {
|
||||
this(
|
||||
repository.getNamespace(),
|
||||
repository.getName(),
|
||||
repository.getType(),
|
||||
repository.getContact(),
|
||||
repository.getDescription(),
|
||||
repository.getPermissions());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
@XmlRootElement(name = "scm-environment")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
class ScmEnvironment {
|
||||
private EnvironmentPluginsDescriptor plugins;
|
||||
private String coreVersion;
|
||||
private String os;
|
||||
private String arch;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ScmEnvironmentCompatibilityChecker {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScmEnvironmentCompatibilityChecker.class);
|
||||
private final PluginManager pluginManager;
|
||||
private final SCMContextProvider scmContextProvider;
|
||||
|
||||
@Inject
|
||||
public ScmEnvironmentCompatibilityChecker(PluginManager pluginManager, SCMContextProvider scmContextProvider) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.scmContextProvider = scmContextProvider;
|
||||
}
|
||||
|
||||
boolean check(ScmEnvironment environment) {
|
||||
return isCoreVersionCompatible(scmContextProvider.getVersion(), environment.getCoreVersion())
|
||||
&& arePluginsCompatible(environment);
|
||||
}
|
||||
|
||||
private boolean isCoreVersionCompatible(String currentCoreVersion, String coreVersionFromImport) {
|
||||
boolean compatible = currentCoreVersion.equals(coreVersionFromImport);
|
||||
if (!compatible) {
|
||||
LOG.info(
|
||||
"SCM-Manager version is not compatible with dump. Dump can only be imported with SCM-Manager version: {}; you are running version {}",
|
||||
coreVersionFromImport,
|
||||
currentCoreVersion
|
||||
);
|
||||
}
|
||||
return compatible;
|
||||
}
|
||||
|
||||
private boolean arePluginsCompatible(ScmEnvironment environment) {
|
||||
List<PluginInformation> currentlyInstalledPlugins = pluginManager.getInstalled()
|
||||
.stream()
|
||||
.map(p -> p.getDescriptor().getInformation())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (EnvironmentPluginDescriptor plugin : environment.getPlugins().getPlugin()) {
|
||||
Optional<PluginInformation> matchingInstalledPlugin = findMatchingInstalledPlugin(currentlyInstalledPlugins, plugin);
|
||||
if (isPluginIncompatible(plugin, matchingInstalledPlugin)) {
|
||||
LOG.info(
|
||||
"The installed plugin \"{}\" with version \"{}\" doesn't match the plugin data version \"{}\" from the SCM-Manager environment the dump was created with.",
|
||||
matchingInstalledPlugin.get().getName(),
|
||||
matchingInstalledPlugin.get().getVersion(),
|
||||
plugin.getVersion()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isPluginIncompatible(EnvironmentPluginDescriptor plugin, Optional<PluginInformation> matchingInstalledPlugin) {
|
||||
return matchingInstalledPlugin.isPresent() && isPluginVersionIncompatible(plugin.getVersion(), matchingInstalledPlugin.get().getVersion());
|
||||
}
|
||||
|
||||
private Optional<PluginInformation> findMatchingInstalledPlugin(List<PluginInformation> currentlyInstalledPlugins, EnvironmentPluginDescriptor plugin) {
|
||||
return currentlyInstalledPlugins
|
||||
.stream()
|
||||
.filter(p -> p.getName().equalsIgnoreCase(plugin.getName()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private boolean isPluginVersionIncompatible(String previousPluginVersion, String installedPluginVersion) {
|
||||
return !installedPluginVersion.equals(previousPluginVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
import sonia.scm.store.ExportableStore;
|
||||
import sonia.scm.store.StoreEntryMetaData;
|
||||
import sonia.scm.store.StoreExporter;
|
||||
import sonia.scm.store.StoreType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
public class TarArchiveRepositoryStoreExporter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TarArchiveRepositoryStoreExporter.class);
|
||||
|
||||
private final StoreExporter storeExporter;
|
||||
|
||||
@Inject
|
||||
public TarArchiveRepositoryStoreExporter(StoreExporter storeExporter) {
|
||||
this.storeExporter = storeExporter;
|
||||
}
|
||||
|
||||
public void export(Repository repository, OutputStream output) {
|
||||
try (
|
||||
BufferedOutputStream bos = new BufferedOutputStream(output);
|
||||
final TarArchiveOutputStream taos = new TarArchiveOutputStream(bos)
|
||||
) {
|
||||
List<ExportableStore> exportableStores = storeExporter.listExportableStores(repository);
|
||||
for (ExportableStore store : exportableStores) {
|
||||
store.export((name, filesize) -> {
|
||||
StoreEntryMetaData storeMetaData = store.getMetaData();
|
||||
if (isOneOfStoreTypes(store, StoreType.DATA, StoreType.BLOB)) {
|
||||
String storePath = createStorePath(storeMetaData.getType().getValue(), storeMetaData.getName(), name);
|
||||
addEntryToArchive(taos, storePath, filesize);
|
||||
} else if (isOneOfStoreTypes(store, StoreType.CONFIG, StoreType.CONFIG_ENTRY)) {
|
||||
String storePath = createStorePath(storeMetaData.getType().getValue(), name);
|
||||
addEntryToArchive(taos, storePath, filesize);
|
||||
} else {
|
||||
LOG.debug("Skip file {} on export", name);
|
||||
}
|
||||
return createOutputStream(taos);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new ExportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Could not export repository metadata stores.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOneOfStoreTypes(ExportableStore store, StoreType... types) {
|
||||
return asList(types).contains(store.getMetaData().getType());
|
||||
}
|
||||
|
||||
private void addEntryToArchive(TarArchiveOutputStream taos, String storePath, long filesize) throws IOException {
|
||||
TarArchiveEntry entry = new TarArchiveEntry(storePath);
|
||||
entry.setSize(filesize);
|
||||
taos.putArchiveEntry(entry);
|
||||
}
|
||||
|
||||
private String createStorePath(String... pathParts) {
|
||||
StringBuilder storePath = new StringBuilder("stores");
|
||||
for (String part : pathParts) {
|
||||
storePath.append('/').append(part);
|
||||
}
|
||||
return storePath.toString();
|
||||
}
|
||||
|
||||
private OutputStream createOutputStream(TarArchiveOutputStream taos) {
|
||||
return new CloseArchiveOutputStream(taos);
|
||||
}
|
||||
|
||||
static class CloseArchiveOutputStream extends FilterOutputStream {
|
||||
|
||||
private final TarArchiveOutputStream delegate;
|
||||
|
||||
CloseArchiveOutputStream(TarArchiveOutputStream delegate) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
delegate.closeArchiveEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.store.RepositoryStoreImporter;
|
||||
import sonia.scm.store.StoreEntryMetaData;
|
||||
import sonia.scm.store.StoreType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class TarArchiveRepositoryStoreImporter {
|
||||
|
||||
private final RepositoryStoreImporter repositoryStoreImporter;
|
||||
|
||||
@Inject
|
||||
public TarArchiveRepositoryStoreImporter(RepositoryStoreImporter repositoryStoreImporter) {
|
||||
this.repositoryStoreImporter = repositoryStoreImporter;
|
||||
}
|
||||
|
||||
public void importFromTarArchive(Repository repository, InputStream inputStream) {
|
||||
try (TarArchiveInputStream tais = new TarArchiveInputStream(inputStream)) {
|
||||
ArchiveEntry entry = tais.getNextEntry();
|
||||
while (entry != null) {
|
||||
String[] entryPathParts = entry.getName().split(File.separator);
|
||||
validateStorePath(repository, entryPathParts);
|
||||
importStoreByType(repository, tais, entryPathParts);
|
||||
entry = tais.getNextEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ImportFailedException(ContextEntry.ContextBuilder.entity(repository).build(), "Could not import stores from metadata file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void importStoreByType(Repository repository, TarArchiveInputStream tais, String[] entryPathParts) {
|
||||
String storeType = entryPathParts[1];
|
||||
if (storeType.equals(StoreType.DATA.getValue())) {
|
||||
repositoryStoreImporter
|
||||
.doImport(repository)
|
||||
.importStore(new StoreEntryMetaData(StoreType.DATA, entryPathParts[2]))
|
||||
.importEntry(entryPathParts[3], tais);
|
||||
} else if (storeType.equals(StoreType.CONFIG.getValue())){
|
||||
repositoryStoreImporter
|
||||
.doImport(repository)
|
||||
.importStore(new StoreEntryMetaData(StoreType.CONFIG, ""))
|
||||
.importEntry(entryPathParts[2], tais);
|
||||
} else if(storeType.equals(StoreType.BLOB.getValue())) {
|
||||
repositoryStoreImporter
|
||||
.doImport(repository)
|
||||
.importStore(new StoreEntryMetaData(StoreType.BLOB, entryPathParts[2]))
|
||||
.importEntry(entryPathParts[3], tais);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateStorePath(Repository repository, String[] entryPathParts) {
|
||||
if (!isValidStorePath(entryPathParts)) {
|
||||
throw new ImportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
"Invalid store path in metadata file"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidStorePath(String[] entryPathParts) {
|
||||
//This prevents array out of bound exceptions
|
||||
if (entryPathParts.length > 1) {
|
||||
String storeType = entryPathParts[1];
|
||||
if (storeType.equals(StoreType.DATA.getValue()) || storeType.equals(StoreType.BLOB.getValue())) {
|
||||
return entryPathParts.length == 4;
|
||||
}
|
||||
if (storeType.equals(StoreType.CONFIG.getValue())) {
|
||||
return entryPathParts.length == 3;
|
||||
}
|
||||
}
|
||||
// We only support config and data stores yet
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ package sonia.scm.lifecycle.modules;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.TypeLiteral;
|
||||
import com.google.inject.throwingproviders.ThrowingProviderBinder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
@@ -52,12 +51,16 @@ import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.store.DefaultBlobDirectoryAccess;
|
||||
import sonia.scm.store.FileBlobStoreFactory;
|
||||
import sonia.scm.store.FileRepositoryUpdateIterator;
|
||||
import sonia.scm.store.FileStoreUpdateStepUtilFactory;
|
||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
import sonia.scm.store.JAXBDataStoreFactory;
|
||||
import sonia.scm.store.JAXBPropertyFileAccess;
|
||||
import sonia.scm.update.BlobDirectoryAccess;
|
||||
import sonia.scm.update.PropertyFileAccess;
|
||||
import sonia.scm.update.RepositoryUpdateIterator;
|
||||
import sonia.scm.update.StoreUpdateStepUtilFactory;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
import sonia.scm.update.V1PropertyDAO;
|
||||
import sonia.scm.update.xml.XmlV1PropertyDAO;
|
||||
@@ -105,6 +108,8 @@ public class BootstrapModule extends AbstractModule {
|
||||
bind(V1PropertyDAO.class, XmlV1PropertyDAO.class);
|
||||
bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class);
|
||||
bind(BlobDirectoryAccess.class, DefaultBlobDirectoryAccess.class);
|
||||
bind(RepositoryUpdateIterator.class, FileRepositoryUpdateIterator.class);
|
||||
bind(StoreUpdateStepUtilFactory.class, FileStoreUpdateStepUtilFactory.class);
|
||||
bind(new TypeLiteral<UpdateStepRepositoryMetadataAccess<Path>>() {}).to(new TypeLiteral<MetadataStore>() {});
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,8 @@ import sonia.scm.security.DefaultSecuritySystem;
|
||||
import sonia.scm.security.LoginAttemptHandler;
|
||||
import sonia.scm.security.RepositoryPermissionProvider;
|
||||
import sonia.scm.security.SecuritySystem;
|
||||
import sonia.scm.store.FileStoreExporter;
|
||||
import sonia.scm.store.StoreExporter;
|
||||
import sonia.scm.template.MustacheTemplateEngine;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
@@ -198,6 +200,7 @@ class ScmServletModule extends ServletModule {
|
||||
bind(NamespaceManager.class, DefaultNamespaceManager.class);
|
||||
bind(GroupCollector.class, DefaultGroupCollector.class);
|
||||
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);
|
||||
bind(StoreExporter.class, FileStoreExporter.class);
|
||||
|
||||
// bind sslcontext provider
|
||||
bind(SSLContext.class).toProvider(SSLContextProvider.class);
|
||||
|
||||
Reference in New Issue
Block a user