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:
Eduard Heimbuch
2021-01-08 09:19:33 +01:00
committed by GitHub
parent badcf3ecb4
commit adf7bac665
21 changed files with 451 additions and 49 deletions

View File

@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added ### Added
- Add repository export for Subversion ([#1488](https://github.com/scm-manager/scm-manager/pull/1488))
- Provide more options for Helm chart ([#1485](https://github.com/scm-manager/scm-manager/pull/1485)) - Provide more options for Helm chart ([#1485](https://github.com/scm-manager/scm-manager/pull/1485))
- Option to create a permanent link to a source file ([#1489](https://github.com/scm-manager/scm-manager/pull/1489)) - Option to create a permanent link to a source file ([#1489](https://github.com/scm-manager/scm-manager/pull/1489))
- add markdown codeblock renderer extension point ([#1492](https://github.com/scm-manager/scm-manager/pull/1492)) - add markdown codeblock renderer extension point ([#1492](https://github.com/scm-manager/scm-manager/pull/1492))

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -19,6 +19,12 @@ Ein archiviertes Repository kann nicht mehr verändert werden.
![Repository-Settings-General-Git](assets/repository-settings-general-git.png) ![Repository-Settings-General-Git](assets/repository-settings-general-git.png)
In dem Bereich "Repository exportieren" kann das Repository als Dump exportiert werden.
Für den Download kann zwischen einem komprimierten Archiv oder dem einfachen Dump-Format gewählt werden.
Diese Export-Funktion wird derzeit nur von Subversion Repositories unterstützt.
![Repository-Settings-General-Svn](assets/repository-settings-general-svn.png)
### Berechtigungen ### Berechtigungen
Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -17,6 +17,12 @@ repository is marked as archived, it can no longer be modified.
![Repository-Settings-General-Git](assets/repository-settings-general-git.png) ![Repository-Settings-General-Git](assets/repository-settings-general-git.png)
In the area "Repository Export" you may export this repository as dump file.
You can choose between compressed and uncompressed download format.
This export function is currently only supported by Subversion repositories.
![Repository-Settings-General-Svn](assets/repository-settings-general-svn.png)
### Permissions ### Permissions
Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable

View File

@@ -89,7 +89,7 @@ public final class BundleCommandRequest
* *
* @return {@link ByteSink} archive. * @return {@link ByteSink} archive.
*/ */
ByteSink getArchive() public ByteSink getArchive()
{ {
return archive; return archive;
} }

View File

@@ -242,6 +242,15 @@
"createButton": "Neues Repository erstellen", "createButton": "Neues Repository erstellen",
"importButton": "Repository importieren" "importButton": "Repository importieren"
}, },
"export": {
"subtitle": "Repository exportieren",
"compressed": {
"label": "Komprimieren",
"helpText": "Export Datei vor dem Download komprimieren. Reduziert die Downloadgröße."
},
"exportButton": "Repository exportieren",
"exportStarted": "Der Repository Export wurde gestartet. Abhängig von der Größe des Repository kann dies einige Momente dauern."
},
"sources": { "sources": {
"fileTree": { "fileTree": {
"name": "Name", "name": "Name",

View File

@@ -242,6 +242,15 @@
"createButton": "Create Repository", "createButton": "Create Repository",
"importButton": "Import Repository" "importButton": "Import Repository"
}, },
"export": {
"subtitle": "Repository Export",
"compressed": {
"label": "Compress",
"helpText": "Compress the export dump size to reduce the download size."
},
"exportButton": "Export Repository",
"exportStarted": "The repository export was started. Depending on the repository size this may take a while."
},
"sources": { "sources": {
"fileTree": { "fileTree": {
"name": "Name", "name": "Name",

View File

@@ -35,6 +35,7 @@ import RepositoryDangerZone from "./RepositoryDangerZone";
import { getLinks } from "../../modules/indexResource"; import { getLinks } from "../../modules/indexResource";
import { urls } from "@scm-manager/ui-components"; import { urls } from "@scm-manager/ui-components";
import { TranslationProps, withTranslation } from "react-i18next"; import { TranslationProps, withTranslation } from "react-i18next";
import ExportRepository from "./ExportRepository";
type Props = TranslationProps & type Props = TranslationProps &
RouteComponentProps & { RouteComponentProps & {
@@ -82,6 +83,7 @@ class EditRepo extends React.Component<Props> {
this.props.modifyRepo(repo, this.repoModified); this.props.modifyRepo(repo, this.repoModified);
}} }}
/> />
<ExportRepository repository={this.props.repository}/>
<ExtensionPoint name="repo-config.route" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repo-config.route" props={extensionProps} renderAll={true} />
<RepositoryDangerZone repository={repository} indexLinks={indexLinks} /> <RepositoryDangerZone repository={repository} indexLinks={indexLinks} />
</> </>

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
import React, { FC, useState } from "react";
import { Button, Checkbox, Level, Notification, Subtitle } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Link, Repository } from "@scm-manager/ui-types";
type Props = {
repository: Repository;
};
const ExportRepository: FC<Props> = ({ repository }) => {
const [t] = useTranslation("repos");
const [compressed, setCompressed] = useState(true);
const [loading, setLoading] = useState(false);
const createExportLink = () => {
let exportLink = (repository?._links.export as Link).href;
if (compressed) {
exportLink += "?compressed=true";
}
return exportLink;
};
if (!repository?._links?.export) {
return null;
}
return (
<>
<hr />
<Subtitle subtitle={t("export.subtitle")} />
<>
<Checkbox
checked={compressed}
label={t("export.compressed.label")}
onChange={setCompressed}
helpText={t("export.compressed.helpText")}
/>
<Level
right={
<a color="primary" href={createExportLink()} onClick={() => setLoading(true)}>
<Button color="primary" label={t("export.exportButton")} icon="file-export" />
</a>
}
/>
{loading && <Notification onClose={() => setLoading(false)}>{t("export.exportStarted")}</Notification>}
</>
</>
);
};
export default ExportRepository;

View File

@@ -231,6 +231,12 @@
<version>${jaxb.version}</version> <version>${jaxb.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
<!-- injection --> <!-- injection -->
<dependency> <dependency>

View File

@@ -39,6 +39,7 @@ public class RepositoryBasedResourceProvider {
private final Provider<FileHistoryRootResource> fileHistoryRootResource; private final Provider<FileHistoryRootResource> fileHistoryRootResource;
private final Provider<IncomingRootResource> incomingRootResource; private final Provider<IncomingRootResource> incomingRootResource;
private final Provider<AnnotateResource> annotateResource; private final Provider<AnnotateResource> annotateResource;
private final Provider<RepositoryExportResource> repositoryExportResource;
@Inject @Inject
public RepositoryBasedResourceProvider( public RepositoryBasedResourceProvider(
@@ -51,7 +52,9 @@ public class RepositoryBasedResourceProvider {
Provider<DiffRootResource> diffRootResource, Provider<DiffRootResource> diffRootResource,
Provider<ModificationsRootResource> modificationsRootResource, Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource, Provider<FileHistoryRootResource> fileHistoryRootResource,
Provider<IncomingRootResource> incomingRootResource, Provider<AnnotateResource> annotateResource) { Provider<IncomingRootResource> incomingRootResource,
Provider<AnnotateResource> annotateResource,
Provider<RepositoryExportResource> repositoryExportResource) {
this.tagRootResource = tagRootResource; this.tagRootResource = tagRootResource;
this.branchRootResource = branchRootResource; this.branchRootResource = branchRootResource;
this.changesetRootResource = changesetRootResource; this.changesetRootResource = changesetRootResource;
@@ -63,6 +66,7 @@ public class RepositoryBasedResourceProvider {
this.fileHistoryRootResource = fileHistoryRootResource; this.fileHistoryRootResource = fileHistoryRootResource;
this.incomingRootResource = incomingRootResource; this.incomingRootResource = incomingRootResource;
this.annotateResource = annotateResource; this.annotateResource = annotateResource;
this.repositoryExportResource = repositoryExportResource;
} }
public TagRootResource getTagRootResource() { public TagRootResource getTagRootResource() {
@@ -108,4 +112,8 @@ public class RepositoryBasedResourceProvider {
public AnnotateResource getAnnotateResource() { public AnnotateResource getAnnotateResource() {
return annotateResource.get(); return annotateResource.get();
} }
public RepositoryExportResource getRepositoryExportResource() {
return repositoryExportResource.get();
}
} }

View File

@@ -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];
}
}

View File

@@ -52,12 +52,10 @@ import sonia.scm.Type;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryImportEvent; import sonia.scm.repository.RepositoryImportEvent;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.RepositoryType;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.PullCommandBuilder; import sonia.scm.repository.api.PullCommandBuilder;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
@@ -87,13 +85,14 @@ import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList; 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 { public class RepositoryImportResource {
@@ -163,12 +162,11 @@ public class RepositoryImportResource {
@PathParam("type") String type, @Valid RepositoryImportDto request) { @PathParam("type") String type, @Valid RepositoryImportDto request) {
RepositoryPermissions.create().check(); RepositoryPermissions.create().check();
Type t = type(type); Type t = type(manager, type);
if (!t.getName().equals(request.getType())) { if (!t.getName().equals(request.getType())) {
throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST); throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST);
} }
checkSupport(t, Command.PULL);
checkSupport(t, Command.PULL, request);
logger.info("start {} import for external url {}", type, request.getImportUrl()); logger.info("start {} import for external url {}", type, request.getImportUrl());
@@ -272,9 +270,8 @@ public class RepositoryImportResource {
checkNotNull(inputStream, "bundle inputStream is required"); checkNotNull(inputStream, "bundle inputStream is required");
checkArgument(!Strings.isNullOrEmpty(repositoryDto.getName()), "request does not contain name of the repository"); checkArgument(!Strings.isNullOrEmpty(repositoryDto.getName()), "request does not contain name of the repository");
Type t = type(type); Type t = type(manager, type);
checkSupport(t, Command.UNBUNDLE);
checkSupport(t, Command.UNBUNDLE, "bundle");
Repository repository = mapper.map(repositoryDto); Repository repository = mapper.map(repositoryDto);
repository.setPermissions(singletonList( repository.setPermissions(singletonList(
@@ -340,42 +337,6 @@ public class RepositoryImportResource {
return null; 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 @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor

View File

@@ -332,6 +332,11 @@ public class RepositoryResource {
return resourceProvider.getAnnotateResource(); return resourceProvider.getAnnotateResource();
} }
@Path("export/")
public RepositoryExportResource export() {
return resourceProvider.getRepositoryExportResource();
}
private Supplier<Repository> loadBy(String namespace, String name) { private Supplier<Repository> loadBy(String namespace, String name) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName))); return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName)));

View File

@@ -103,6 +103,11 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
.collect(toList()); .collect(toList());
linksBuilder.array(protocolLinks); 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)) { if (repositoryService.isSupported(Command.TAGS)) {
linksBuilder.single(link("tags", resourceLinks.tag().all(repository.getNamespace(), repository.getName()))); linksBuilder.single(link("tags", resourceLinks.tag().all(repository.getNamespace(), repository.getName())));
} }

View File

@@ -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();
}
}

View File

@@ -341,10 +341,12 @@ class ResourceLinks {
static class RepositoryLinks { static class RepositoryLinks {
private final LinkBuilder repositoryLinkBuilder; private final LinkBuilder repositoryLinkBuilder;
private final LinkBuilder repositoryImportLinkBuilder; private final LinkBuilder repositoryImportLinkBuilder;
private final LinkBuilder repositoryExportLinkBuilder;
RepositoryLinks(ScmPathInfo pathInfo) { RepositoryLinks(ScmPathInfo pathInfo) {
repositoryLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class); repositoryLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class);
repositoryImportLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryImportResource.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) { String self(String namespace, String name) {
@@ -377,6 +379,10 @@ class ResourceLinks {
String unarchive(String namespace, String name) { String unarchive(String namespace, String name) {
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("unarchive").parameters().href(); 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() { RepositoryCollectionLinks repositoryCollection() {

View File

@@ -54,6 +54,7 @@ import sonia.scm.repository.RepositoryInitializer;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.RepositoryType; import sonia.scm.repository.RepositoryType;
import sonia.scm.repository.api.BundleCommandBuilder;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.ImportFailedException;
import sonia.scm.repository.api.PullCommandBuilder; import sonia.scm.repository.api.PullCommandBuilder;
@@ -66,6 +67,7 @@ import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@@ -98,7 +100,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.anyObject;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
@@ -167,6 +168,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer); super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer);
super.repositoryImportResource = new RepositoryImportResource(repositoryManager, dtoToRepositoryMapper, serviceFactory, resourceLinks, eventBus); super.repositoryImportResource = new RepositoryImportResource(repositoryManager, dtoToRepositoryMapper, serviceFactory, resourceLinks, eventBus);
super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory);
dispatcher.addSingletonResource(getRepositoryRootResource()); dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo); when(scmPathInfoStore.get()).thenReturn(uriInfo);
@@ -710,6 +712,59 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
verify(repositoryManager).unarchive(repository); verify(repositoryManager).unarchive(repository);
} }
@Test
public void shouldExportRepository() throws URISyntaxException {
String namespace = "space";
String name = "repo";
Repository repository = mockRepository(namespace, name);
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_OK, response.getStatus());
assertEquals(MediaType.APPLICATION_OCTET_STREAM, response.getOutputHeaders().get("Content-Type").get(0).toString());
verify(service).getBundleCommand();
}
@Test
public void shouldExportRepositoryCompressed() throws URISyntaxException {
String namespace = "space";
String name = "repo";
Repository repository = mockRepository(namespace, name);
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn?compressed=true");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_OK, response.getStatus());
assertEquals("application/x-gzip", response.getOutputHeaders().get("Content-Type").get(0).toString());
verify(service).getBundleCommand();
}
private void mockRepositoryHandler(Set<Command> cmds) {
RepositoryHandler repositoryHandler = mock(RepositoryHandler.class);
RepositoryType repositoryType = mock(RepositoryType.class);
when(manager.getHandler("svn")).thenReturn(repositoryHandler);
when(repositoryHandler.getType()).thenReturn(repositoryType);
when(repositoryType.getSupportedCommands()).thenReturn(cmds);
}
private PageResult<Repository> createSingletonPageResult(Repository repository) { private PageResult<Repository> createSingletonPageResult(Repository repository) {
return new PageResult<>(singletonList(repository), 0); return new PageResult<>(singletonList(repository), 0);
} }

View File

@@ -46,6 +46,7 @@ abstract class RepositoryTestBase {
RepositoryCollectionResource repositoryCollectionResource; RepositoryCollectionResource repositoryCollectionResource;
AnnotateResource annotateResource; AnnotateResource annotateResource;
RepositoryImportResource repositoryImportResource; RepositoryImportResource repositoryImportResource;
RepositoryExportResource repositoryExportResource;
RepositoryRootResource getRepositoryRootResource() { RepositoryRootResource getRepositoryRootResource() {
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider( RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
@@ -59,7 +60,8 @@ abstract class RepositoryTestBase {
of(modificationsRootResource), of(modificationsRootResource),
of(fileHistoryRootResource), of(fileHistoryRootResource),
of(incomingRootResource), of(incomingRootResource),
of(annotateResource)); of(annotateResource),
of(repositoryExportResource));
return new RepositoryRootResource( return new RepositoryRootResource(
of(new RepositoryResource( of(new RepositoryResource(
repositoryToDtoMapper, repositoryToDtoMapper,

View File

@@ -284,6 +284,16 @@ public class RepositoryToRepositoryDtoMapperTest {
dto.getLinks().getLinkBy("unarchive").get().getHref()); dto.getLinks().getLinkBy("unarchive").get().getHref());
} }
@Test
public void shouldCreateExportLink() {
Repository repository = createTestRepository();
repository.setType("svn");
RepositoryDto dto = mapper.map(repository);
assertEquals(
"http://example.com/base/v2/repositories/testspace/test/export/svn",
dto.getLinks().getLinkBy("export").get().getHref());
}
private ScmProtocol mockProtocol(String type, String protocol) { private ScmProtocol mockProtocol(String type, String protocol) {
return new MockScmProtocol(type, protocol); return new MockScmProtocol(type, protocol);
} }