mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-05 07:48:14 +02:00
Subversion repository export
Add the repository export function for Subversion repositories. The repository will be exported as dump file which can be downloaded directly or inside a gzip compressed archive.
This commit is contained in:
@@ -39,6 +39,7 @@ public class RepositoryBasedResourceProvider {
|
||||
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||
private final Provider<IncomingRootResource> incomingRootResource;
|
||||
private final Provider<AnnotateResource> annotateResource;
|
||||
private final Provider<RepositoryExportResource> repositoryExportResource;
|
||||
|
||||
@Inject
|
||||
public RepositoryBasedResourceProvider(
|
||||
@@ -51,7 +52,9 @@ public class RepositoryBasedResourceProvider {
|
||||
Provider<DiffRootResource> diffRootResource,
|
||||
Provider<ModificationsRootResource> modificationsRootResource,
|
||||
Provider<FileHistoryRootResource> fileHistoryRootResource,
|
||||
Provider<IncomingRootResource> incomingRootResource, Provider<AnnotateResource> annotateResource) {
|
||||
Provider<IncomingRootResource> incomingRootResource,
|
||||
Provider<AnnotateResource> annotateResource,
|
||||
Provider<RepositoryExportResource> repositoryExportResource) {
|
||||
this.tagRootResource = tagRootResource;
|
||||
this.branchRootResource = branchRootResource;
|
||||
this.changesetRootResource = changesetRootResource;
|
||||
@@ -63,6 +66,7 @@ public class RepositoryBasedResourceProvider {
|
||||
this.fileHistoryRootResource = fileHistoryRootResource;
|
||||
this.incomingRootResource = incomingRootResource;
|
||||
this.annotateResource = annotateResource;
|
||||
this.repositoryExportResource = repositoryExportResource;
|
||||
}
|
||||
|
||||
public TagRootResource getTagRootResource() {
|
||||
@@ -108,4 +112,8 @@ public class RepositoryBasedResourceProvider {
|
||||
public AnnotateResource getAnnotateResource() {
|
||||
return annotateResource.get();
|
||||
}
|
||||
|
||||
public RepositoryExportResource getRepositoryExportResource() {
|
||||
return repositoryExportResource.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.api.v2.resources;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.Type;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
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.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;
|
||||
|
||||
@Inject
|
||||
public RepositoryExportResource(RepositoryManager manager,
|
||||
RepositoryServiceFactory serviceFactory) {
|
||||
this.manager = manager;
|
||||
this.serviceFactory = serviceFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports an existing repository without additional metadata. The method can
|
||||
* 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
|
||||
* @return response with readable stream of repository dump
|
||||
* @since 2.13.0
|
||||
*/
|
||||
@GET
|
||||
@Path("{type}")
|
||||
@Consumes(VndMediaType.REPOSITORY)
|
||||
@Operation(summary = "Exports the repository", description = "Exports the repository.", 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 exportRepository(@Context UriInfo uriInfo,
|
||||
@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("type") String type,
|
||||
@DefaultValue("false") @QueryParam("compressed") boolean compressed
|
||||
) {
|
||||
Repository repository = manager.get(new NamespaceAndName(namespace, name));
|
||||
RepositoryPermissions.read().check(repository);
|
||||
|
||||
Type repositoryType = type(manager, type);
|
||||
checkSupport(repositoryType, Command.BUNDLE);
|
||||
|
||||
return exportRepository(repository, compressed);
|
||||
}
|
||||
|
||||
private Response exportRepository(Repository repository, boolean compressed) {
|
||||
StreamingOutput output = os -> {
|
||||
try (RepositoryService service = serviceFactory.create(repository)) {
|
||||
if (compressed) {
|
||||
GzipCompressorOutputStream gzipCompressorOutputStream = new GzipCompressorOutputStream(os);
|
||||
service.getBundleCommand().bundle(gzipCompressorOutputStream);
|
||||
gzipCompressorOutputStream.finish();
|
||||
} else {
|
||||
service.getBundleCommand().bundle(os);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "repository export failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
return Response
|
||||
.ok(output, compressed ? "application/x-gzip" : MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header("content-disposition", createContentDispositionHeaderValue(repository, compressed))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String createContentDispositionHeaderValue(Repository repository, boolean compressed) {
|
||||
String timestamp = createFormattedTimestamp();
|
||||
return String.format(
|
||||
"attachment; filename = %s-%s-%s.%s",
|
||||
repository.getNamespace(),
|
||||
repository.getName(),
|
||||
timestamp,
|
||||
compressed ? "dump.gz" : "dump"
|
||||
);
|
||||
}
|
||||
|
||||
private String createFormattedTimestamp() {
|
||||
return Instant.now().toString().replace(":", "-").split("\\.")[0];
|
||||
}
|
||||
}
|
||||
@@ -52,12 +52,10 @@ import sonia.scm.Type;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryImportEvent;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.PullCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
@@ -87,13 +85,14 @@ import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
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;
|
||||
import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.type;
|
||||
|
||||
public class RepositoryImportResource {
|
||||
|
||||
@@ -163,12 +162,11 @@ public class RepositoryImportResource {
|
||||
@PathParam("type") String type, @Valid RepositoryImportDto request) {
|
||||
RepositoryPermissions.create().check();
|
||||
|
||||
Type t = type(type);
|
||||
Type t = type(manager, type);
|
||||
if (!t.getName().equals(request.getType())) {
|
||||
throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
checkSupport(t, Command.PULL, request);
|
||||
checkSupport(t, Command.PULL);
|
||||
|
||||
logger.info("start {} import for external url {}", type, request.getImportUrl());
|
||||
|
||||
@@ -272,9 +270,8 @@ public class RepositoryImportResource {
|
||||
checkNotNull(inputStream, "bundle inputStream is required");
|
||||
checkArgument(!Strings.isNullOrEmpty(repositoryDto.getName()), "request does not contain name of the repository");
|
||||
|
||||
Type t = type(type);
|
||||
|
||||
checkSupport(t, Command.UNBUNDLE, "bundle");
|
||||
Type t = type(manager, type);
|
||||
checkSupport(t, Command.UNBUNDLE);
|
||||
|
||||
Repository repository = mapper.map(repositoryDto);
|
||||
repository.setPermissions(singletonList(
|
||||
@@ -340,42 +337,6 @@ public class RepositoryImportResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check repository type for support for the given command.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param cmd command
|
||||
* @param request request object
|
||||
*/
|
||||
private void checkSupport(Type type, Command cmd, Object request) {
|
||||
if (!(type instanceof RepositoryType)) {
|
||||
logger.warn("type {} is not a repository type", type.getName());
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Set<Command> cmds = ((RepositoryType) type).getSupportedCommands();
|
||||
|
||||
if (!cmds.contains(cmd)) {
|
||||
logger.warn("type {} does not support this type of import: {}",
|
||||
type.getName(), request);
|
||||
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private Type type(String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
|
||||
if (handler == null) {
|
||||
logger.warn("no handler for type {} found", type);
|
||||
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
return handler.getType();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
|
||||
@@ -332,6 +332,11 @@ public class RepositoryResource {
|
||||
return resourceProvider.getAnnotateResource();
|
||||
}
|
||||
|
||||
@Path("export/")
|
||||
public RepositoryExportResource export() {
|
||||
return resourceProvider.getRepositoryExportResource();
|
||||
}
|
||||
|
||||
private Supplier<Repository> loadBy(String namespace, String name) {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName)));
|
||||
|
||||
@@ -103,6 +103,11 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
.collect(toList());
|
||||
linksBuilder.array(protocolLinks);
|
||||
}
|
||||
|
||||
if (repositoryService.isSupported(Command.BUNDLE)) {
|
||||
linksBuilder.single(link("export", resourceLinks.repository().export(repository.getNamespace(), repository.getName(), repository.getType())));
|
||||
}
|
||||
|
||||
if (repositoryService.isSupported(Command.TAGS)) {
|
||||
linksBuilder.single(link("tags", resourceLinks.tag().all(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.api.v2.resources;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.repository.RepositoryHandler;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.Command;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Set;
|
||||
|
||||
class RepositoryTypeSupportChecker {
|
||||
|
||||
private RepositoryTypeSupportChecker() {
|
||||
}
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepositoryTypeSupportChecker.class);
|
||||
|
||||
/**
|
||||
* Check repository type for support for the given command.
|
||||
*
|
||||
* @param type repository type
|
||||
* @param cmd command
|
||||
*/
|
||||
static void checkSupport(Type type, Command cmd) {
|
||||
if (!(type instanceof RepositoryType)) {
|
||||
logger.warn("type {} is not a repository type", type.getName());
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Set<Command> cmds = ((RepositoryType) type).getSupportedCommands();
|
||||
if (!cmds.contains(cmd)) {
|
||||
logger.warn("type {} does not support this command {}",
|
||||
type.getName(),
|
||||
cmd.name());
|
||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
static Type type(RepositoryManager manager, String type) {
|
||||
RepositoryHandler handler = manager.getHandler(type);
|
||||
if (handler == null) {
|
||||
logger.warn("no handler for type {} found", type);
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
}
|
||||
return handler.getType();
|
||||
}
|
||||
}
|
||||
@@ -341,10 +341,12 @@ class ResourceLinks {
|
||||
static class RepositoryLinks {
|
||||
private final LinkBuilder repositoryLinkBuilder;
|
||||
private final LinkBuilder repositoryImportLinkBuilder;
|
||||
private final LinkBuilder repositoryExportLinkBuilder;
|
||||
|
||||
RepositoryLinks(ScmPathInfo pathInfo) {
|
||||
repositoryLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class);
|
||||
repositoryImportLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryImportResource.class);
|
||||
repositoryExportLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, RepositoryExportResource.class);
|
||||
}
|
||||
|
||||
String self(String namespace, String name) {
|
||||
@@ -377,6 +379,10 @@ class ResourceLinks {
|
||||
String unarchive(String namespace, String name) {
|
||||
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("unarchive").parameters().href();
|
||||
}
|
||||
|
||||
String export(String namespace, String name, String type) {
|
||||
return repositoryExportLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("export").parameters().method("exportRepository").parameters(type).href();
|
||||
}
|
||||
}
|
||||
|
||||
RepositoryCollectionLinks repositoryCollection() {
|
||||
|
||||
Reference in New Issue
Block a user