mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 07:25:44 +01: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:
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
### 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))
|
||||
- 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))
|
||||
|
||||
BIN
docs/de/user/repo/assets/repository-settings-general-svn.png
Normal file
BIN
docs/de/user/repo/assets/repository-settings-general-svn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -19,6 +19,12 @@ Ein archiviertes Repository kann nicht mehr verändert werden.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### Berechtigungen
|
||||
|
||||
Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren
|
||||
|
||||
BIN
docs/en/user/repo/assets/repository-settings-general-svn.png
Normal file
BIN
docs/en/user/repo/assets/repository-settings-general-svn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@@ -17,6 +17,12 @@ repository is marked as archived, it can no longer be modified.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### Permissions
|
||||
|
||||
Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable
|
||||
|
||||
@@ -89,7 +89,7 @@ public final class BundleCommandRequest
|
||||
*
|
||||
* @return {@link ByteSink} archive.
|
||||
*/
|
||||
ByteSink getArchive()
|
||||
public ByteSink getArchive()
|
||||
{
|
||||
return archive;
|
||||
}
|
||||
|
||||
@@ -242,6 +242,15 @@
|
||||
"createButton": "Neues Repository erstellen",
|
||||
"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": {
|
||||
"fileTree": {
|
||||
"name": "Name",
|
||||
|
||||
@@ -242,6 +242,15 @@
|
||||
"createButton": "Create 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": {
|
||||
"fileTree": {
|
||||
"name": "Name",
|
||||
|
||||
@@ -35,6 +35,7 @@ import RepositoryDangerZone from "./RepositoryDangerZone";
|
||||
import { getLinks } from "../../modules/indexResource";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
import { TranslationProps, withTranslation } from "react-i18next";
|
||||
import ExportRepository from "./ExportRepository";
|
||||
|
||||
type Props = TranslationProps &
|
||||
RouteComponentProps & {
|
||||
@@ -82,6 +83,7 @@ class EditRepo extends React.Component<Props> {
|
||||
this.props.modifyRepo(repo, this.repoModified);
|
||||
}}
|
||||
/>
|
||||
<ExportRepository repository={this.props.repository}/>
|
||||
<ExtensionPoint name="repo-config.route" props={extensionProps} renderAll={true} />
|
||||
<RepositoryDangerZone repository={repository} indexLinks={indexLinks} />
|
||||
</>
|
||||
|
||||
73
scm-ui/ui-webapp/src/repos/containers/ExportRepository.tsx
Normal file
73
scm-ui/ui-webapp/src/repos/containers/ExportRepository.tsx
Normal 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;
|
||||
@@ -231,6 +231,12 @@
|
||||
<version>${jaxb.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.20</version>
|
||||
</dependency>
|
||||
|
||||
<!-- injection -->
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -54,6 +54,7 @@ import sonia.scm.repository.RepositoryInitializer;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
import sonia.scm.repository.api.BundleCommandBuilder;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
import sonia.scm.repository.api.PullCommandBuilder;
|
||||
@@ -66,6 +67,7 @@ import sonia.scm.web.RestDispatcher;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
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.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyObject;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -167,6 +168,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
|
||||
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer);
|
||||
super.repositoryImportResource = new RepositoryImportResource(repositoryManager, dtoToRepositoryMapper, serviceFactory, resourceLinks, eventBus);
|
||||
super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory);
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(scmPathInfoStore.get()).thenReturn(uriInfo);
|
||||
@@ -710,6 +712,59 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
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) {
|
||||
return new PageResult<>(singletonList(repository), 0);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ abstract class RepositoryTestBase {
|
||||
RepositoryCollectionResource repositoryCollectionResource;
|
||||
AnnotateResource annotateResource;
|
||||
RepositoryImportResource repositoryImportResource;
|
||||
RepositoryExportResource repositoryExportResource;
|
||||
|
||||
RepositoryRootResource getRepositoryRootResource() {
|
||||
RepositoryBasedResourceProvider repositoryBasedResourceProvider = new RepositoryBasedResourceProvider(
|
||||
@@ -59,7 +60,8 @@ abstract class RepositoryTestBase {
|
||||
of(modificationsRootResource),
|
||||
of(fileHistoryRootResource),
|
||||
of(incomingRootResource),
|
||||
of(annotateResource));
|
||||
of(annotateResource),
|
||||
of(repositoryExportResource));
|
||||
return new RepositoryRootResource(
|
||||
of(new RepositoryResource(
|
||||
repositoryToDtoMapper,
|
||||
|
||||
@@ -284,6 +284,16 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
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) {
|
||||
return new MockScmProtocol(type, protocol);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user