diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 02d7085892..f89579e67e 100644 --- a/scm-ui/ui-types/src/Repositories.ts +++ b/scm-ui/ui-types/src/Repositories.ts @@ -39,6 +39,12 @@ export type RepositoryCreation = Repository & { contextEntries: { [key: string]: any }; }; +export type RepositoryImport = Repository & { + importUrl?: string; + username?: string; + password?: string; +}; + export type Namespace = { namespace: string; _links: Links; diff --git a/scm-ui/ui-webapp/src/containers/Main.tsx b/scm-ui/ui-webapp/src/containers/Main.tsx index 73b5c9370e..b7742867f8 100644 --- a/scm-ui/ui-webapp/src/containers/Main.tsx +++ b/scm-ui/ui-webapp/src/containers/Main.tsx @@ -37,7 +37,7 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import CreateUser from "../users/containers/CreateUser"; import SingleUser from "../users/containers/SingleUser"; import RepositoryRoot from "../repos/containers/RepositoryRoot"; -import Create from "../repos/containers/Create"; +import AddRepository from "../repos/containers/AddRepository"; import Groups from "../groups/containers/Groups"; import SingleGroup from "../groups/containers/SingleGroup"; @@ -77,8 +77,8 @@ class Main extends React.Component { - - + + diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index 1166049614..4bbe2bf374 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -25,7 +25,7 @@ import React, { FC, useEffect, useState } from "react"; import styled from "styled-components"; import { useTranslation } from "react-i18next"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; -import { Repository, RepositoryType } from "@scm-manager/ui-types"; +import { Repository, RepositoryType, RepositoryImport } from "@scm-manager/ui-types"; import { Checkbox, InputField, Level, Select, SubmitButton, Textarea } from "@scm-manager/ui-components"; import * as validator from "./repositoryValidation"; import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos"; @@ -56,13 +56,13 @@ const Columns = styled.div` type Props = { createRepository?: (repo: RepositoryCreation, shouldInit: boolean) => void; - modifyRepository?: (repo: RepositoryCreation) => void; - importRepository?: (repo: RepositoryCreation) => void; + importRepository?: (repo: RepositoryImport) => void; + modifyRepository?: (repo: Repository) => void; repository?: Repository; repositoryTypes?: RepositoryType[]; namespaceStrategy?: string; loading?: boolean; - indexResources: any; + indexResources?: any; }; type RepositoryCreation = Repository & { @@ -118,19 +118,14 @@ const RepositoryForm: FC = ({ const submit = (event: React.FormEvent) => { event.preventDefault(); - const submitForm = evaluateSubmit(); - if (isValid() && submitForm) { - submitForm(repo, initRepository); - } - }; - - const evaluateSubmit = () => { - if (isImportPage()) { - return importRepository; - } else if (isCreatePage()) { - return createRepository; - } else { - return modifyRepository; + if (isValid()) { + if (importRepository && isImportPage()) { + importRepository({ ...repo, url: importUrl, username, password }); + } else if (createRepository && isCreatePage()) { + createRepository(repo, initRepository); + } else if (modifyRepository) { + modifyRepository(repo); + } } }; @@ -167,13 +162,8 @@ const RepositoryForm: FC = ({ return ""; }; - const isImportPage = () => { - return resolveLocation() === "import"; - }; - - const isCreatePage = () => { - return resolveLocation() === "create"; - }; + const isImportPage = () => resolveLocation() === "import"; + const isCreatePage = () => resolveLocation() === "create"; const createSelectOptions = (repositoryTypes?: RepositoryType[]) => { if (repositoryTypes) { @@ -311,7 +301,7 @@ const RepositoryForm: FC = ({ if (!repo.name) { const match = url.match(/([^\/]+)\.git/i); if (match && match[1]) { - setRepo({ ...repo, name: match[1] }); + handleNameChange(match[1]); } } setImportUrl(url); @@ -319,9 +309,8 @@ const RepositoryForm: FC = ({ const disabled = !isModifiable() && isEditMode(); - const getSubmitButtonTranslationKey = () => { - return isImportPage() ? "repositoryForm.submitImport" : "repositoryForm.submitCreate"; - }; + const getSubmitButtonTranslationKey = () => + isImportPage() ? "repositoryForm.submitImport" : "repositoryForm.submitCreate"; const submitButton = disabled ? null : ( void ) => void; + importRepoFromUrl: (link: string, repository: RepositoryImport, callback: (repo: Repository) => void) => void; resetForm: () => void; // context props history: History; }; -class Create extends React.Component { +class AddRepository extends React.Component { componentDidMount() { this.props.resetForm(); this.props.fetchRepositoryTypesIfNeeded(); @@ -87,18 +100,15 @@ class Create extends React.Component { repositoryTypes, namespaceStrategies, createRepo, + importRepoFromUrl, error, - indexResources + indexResources, + repoLink, + t } = this.props; - const { t, repoLink } = this.props; return ( - + { createRepository={(repo, initRepository) => { createRepo(repoLink, repo, initRepository, (repo: Repository) => this.repoCreated(repo)); }} + importRepository={repo => { + importRepoFromUrl(repoLink, repo, (repo: Repository) => this.repoCreated(repo)); + }} indexResources={indexResources} /> @@ -145,10 +158,13 @@ const mapDispatchToProps = (dispatch: any) => { createRepo: (link: string, repository: RepositoryCreation, initRepository: boolean, callback: () => void) => { dispatch(createRepo(link, repository, initRepository, callback)); }, + importRepoFromUrl: (link: string, repository: RepositoryImport, callback: () => void) => { + dispatch(importRepoFromUrl(link, repository, callback)); + }, resetForm: () => { dispatch(createRepoReset()); } }; }; -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("repos")(Create)); +export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("repos")(AddRepository)); diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx index dfe540bc58..2c56dc612b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx @@ -75,7 +75,7 @@ class EditRepo extends React.Component { { + modifyRepository={repo => { this.props.modifyRepo(repo, this.repoModified); }} /> diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index 48cb02ba05..5c2f7e8633 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -25,11 +25,14 @@ import { apiClient } from "@scm-manager/ui-components"; import * as types from "../../modules/types"; import { - Action, Namespace, + Action, + Namespace, NamespaceCollection, Repository, RepositoryCollection, - RepositoryCreation + RepositoryCreation, + RepositoryImport, + Link } from "@scm-manager/ui-types"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; @@ -55,6 +58,12 @@ export const CREATE_REPO_SUCCESS = `${CREATE_REPO}_${types.SUCCESS_SUFFIX}`; export const CREATE_REPO_FAILURE = `${CREATE_REPO}_${types.FAILURE_SUFFIX}`; export const CREATE_REPO_RESET = `${CREATE_REPO}_${types.RESET_SUFFIX}`; +export const IMPORT_REPO = "scm/repos/IMPORT_REPO"; +export const IMPORT_REPO_PENDING = `${IMPORT_REPO}_${types.PENDING_SUFFIX}`; +export const IMPORT_REPO_SUCCESS = `${IMPORT_REPO}_${types.SUCCESS_SUFFIX}`; +export const IMPORT_REPO_FAILURE = `${IMPORT_REPO}_${types.FAILURE_SUFFIX}`; +export const IMPORT_REPO_RESET = `${IMPORT_REPO}_${types.RESET_SUFFIX}`; + export const MODIFY_REPO = "scm/repos/MODIFY_REPO"; export const MODIFY_REPO_PENDING = `${MODIFY_REPO}_${types.PENDING_SUFFIX}`; export const MODIFY_REPO_SUCCESS = `${MODIFY_REPO}_${types.SUCCESS_SUFFIX}`; @@ -178,7 +187,7 @@ export function fetchNamespacesFailure(err: Error): Action { // fetch repo export function fetchRepoByLink(repo: Repository) { - return fetchRepo(repo._links.self.href, repo.namespace, repo.name); + return fetchRepo((repo._links.self as Link).href, repo.namespace, repo.name); } export function fetchRepoByName(link: string, namespace: string, name: string) { @@ -232,6 +241,50 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error): }; } +// import repo + +export function importRepoFromUrl(link: string, repository: RepositoryImport, callback?: (repo: Repository) => void) { + const importLink = link + `import/${repository.type}/url`; + return function(dispatch: any) { + dispatch(importRepoPending()); + return apiClient + .post(importLink, repository, CONTENT_TYPE) + .then(response => { + const location = response.headers.get("Location"); + dispatch(importRepoSuccess()); + return apiClient.get(location); + }) + .then(response => response.json()) + .then(response => { + if (callback) { + callback(response); + } + }) + .catch(err => { + dispatch(importRepoFailure(err)); + }); + }; +} + +export function importRepoPending(): Action { + return { + type: CREATE_REPO_PENDING + }; +} + +export function importRepoSuccess(): Action { + return { + type: CREATE_REPO_SUCCESS + }; +} + +export function importRepoFailure(err: Error): Action { + return { + type: CREATE_REPO_FAILURE, + payload: err + }; +} + // create repo export function createRepo( @@ -294,7 +347,7 @@ export function modifyRepo(repository: Repository, callback?: () => void) { dispatch(modifyRepoPending(repository)); return apiClient - .put(repository._links.update.href, repository, CONTENT_TYPE) + .put((repository._links.update as Link).href, repository, CONTENT_TYPE) .then(() => { dispatch(modifyRepoSuccess(repository)); if (callback) { @@ -353,7 +406,7 @@ export function deleteRepo(repository: Repository, callback?: () => void) { return function(dispatch: any) { dispatch(deleteRepoPending(repository)); return apiClient - .delete(repository._links.delete.href) + .delete((repository._links.delete as Link).href) .then(() => { dispatch(deleteRepoSuccess(repository)); if (callback) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java index 64399d30a5..9dd27e040a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java @@ -25,17 +25,18 @@ package sonia.scm.api.v2.resources; import com.google.common.base.Strings; -import com.google.common.io.Files; import com.google.inject.Inject; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.Links; 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 lombok.ToString; -import lombok.Value; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.FeatureNotSupportedException; import sonia.scm.NotFoundException; import sonia.scm.Type; import sonia.scm.repository.InternalRepositoryException; @@ -47,7 +48,6 @@ import sonia.scm.repository.RepositoryType; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; -import sonia.scm.util.IOUtil; import sonia.scm.web.VndMediaType; import javax.ws.rs.Consumes; @@ -56,19 +56,10 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkArgument; @@ -80,12 +71,14 @@ public class RepositoryImportResource { private final RepositoryManager manager; private final RepositoryServiceFactory serviceFactory; + private final ResourceLinks resourceLinks; @Inject public RepositoryImportResource(RepositoryManager manager, - RepositoryServiceFactory serviceFactory) { + RepositoryServiceFactory serviceFactory, ResourceLinks resourceLinks) { this.manager = manager; this.serviceFactory = serviceFactory; + this.resourceLinks = resourceLinks; } // /** @@ -171,7 +164,7 @@ public class RepositoryImportResource { */ @POST @Path("{type}/url") - @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + @Consumes(VndMediaType.REPOSITORY) @Operation(summary = "Import repository from url", description = "Imports the repository for the given url.", tags = "Repository") @ApiResponse( responseCode = "201", @@ -198,7 +191,7 @@ public class RepositoryImportResource { ) ) public Response importFromUrl(@Context UriInfo uriInfo, - @PathParam("type") String type, UrlImportRequest request) { + @PathParam("type") String type, RepositoryImportDto request) { RepositoryPermissions.create().check(); checkNotNull(request, "request is required"); checkArgument(!Strings.isNullOrEmpty(request.getName()), @@ -220,17 +213,7 @@ public class RepositoryImportResource { handleImportFailure(ex, repository); } - return Response.created(createRepositoryLocation(uriInfo, repository)).build(); - } - - private URI createRepositoryLocation(UriInfo uriInfo, Repository repository) { - return URI.create( - String.format( - "%s/repos/%s", - uriInfo.getBaseUri().toString().replace("/api/", "/"), - repository.getNamespaceAndName() - ) - ); + return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build(); } // /** @@ -408,81 +391,81 @@ public class RepositoryImportResource { return repository; } - /** - * Start bundle import. - * - * @param type repository type - * @param name name of the repository - * @param inputStream bundle stream - * @param compressed true if the bundle is gzip compressed - * @return imported repository - */ - private Repository doImportFromBundle(String type, String namespace, String name, - InputStream inputStream, boolean compressed) { - RepositoryPermissions.create().check(); - - checkArgument(!Strings.isNullOrEmpty(name), - "request does not contain name of the repository"); - checkNotNull(inputStream, "bundle inputStream is required"); - - Repository repository; - - try { - Type t = type(type); - checkSupport(t, Command.UNBUNDLE, "bundle"); - repository = create(namespace, name, type); - importFromBundle(repository, inputStream, compressed); - } catch (IOException ex) { - logger.warn("could not create temporary file", ex); - - throw new WebApplicationException(ex); - } - - return repository; - } - - private void importFromBundle(Repository repository, InputStream inputStream, boolean compressed) throws IOException { - File file = File.createTempFile("scm-import-", ".bundle"); - - try (RepositoryService service = serviceFactory.create(repository)) { - long length = Files.asByteSink(file).writeFrom(inputStream); - - logger.info("copied {} bytes to temp, start bundle import", length); - service.getUnbundleCommand().setCompressed(compressed).unbundle(file); - } catch (InternalRepositoryException ex) { - handleImportFailure(ex, repository); - } finally { - IOUtil.delete(file); - } - } - - private List findImportableTypes() { - List types = new ArrayList<>(); - Collection handlerTypes = manager.getTypes(); - - for (Type t : handlerTypes) { - RepositoryHandler handler = manager.getHandler(t.getName()); - - if (handler != null) { - try { - if (handler.getImportHandler() != null) { - types.add(t); - } - } catch (FeatureNotSupportedException ex) { - if (logger.isTraceEnabled()) { - logger.trace("import handler is not supported", ex); - } else if (logger.isInfoEnabled()) { - logger.info("{} handler does not support import of repositories", - t.getName()); - } - } - } else if (logger.isWarnEnabled()) { - logger.warn("could not find handler for type {}", t.getName()); - } - } - - return types; - } +// /** +// * Start bundle import. +// * +// * @param type repository type +// * @param name name of the repository +// * @param inputStream bundle stream +// * @param compressed true if the bundle is gzip compressed +// * @return imported repository +// */ +// private Repository doImportFromBundle(String type, String namespace, String name, +// InputStream inputStream, boolean compressed) { +// RepositoryPermissions.create().check(); +// +// checkArgument(!Strings.isNullOrEmpty(name), +// "request does not contain name of the repository"); +// checkNotNull(inputStream, "bundle inputStream is required"); +// +// Repository repository; +// +// try { +// Type t = type(type); +// checkSupport(t, Command.UNBUNDLE, "bundle"); +// repository = create(namespace, name, type); +// importFromBundle(repository, inputStream, compressed); +// } catch (IOException ex) { +// logger.warn("could not create temporary file", ex); +// +// throw new WebApplicationException(ex); +// } +// +// return repository; +// } +// +// private void importFromBundle(Repository repository, InputStream inputStream, boolean compressed) throws IOException { +// File file = File.createTempFile("scm-import-", ".bundle"); +// +// try (RepositoryService service = serviceFactory.create(repository)) { +// long length = Files.asByteSink(file).writeFrom(inputStream); +// +// logger.info("copied {} bytes to temp, start bundle import", length); +// service.getUnbundleCommand().setCompressed(compressed).unbundle(file); +// } catch (InternalRepositoryException ex) { +// handleImportFailure(ex, repository); +// } finally { +// IOUtil.delete(file); +// } +// } +// +// private List findImportableTypes() { +// List types = new ArrayList<>(); +// Collection handlerTypes = manager.getTypes(); +// +// for (Type t : handlerTypes) { +// RepositoryHandler handler = manager.getHandler(t.getName()); +// +// if (handler != null) { +// try { +// if (handler.getImportHandler() != null) { +// types.add(t); +// } +// } catch (FeatureNotSupportedException ex) { +// if (logger.isTraceEnabled()) { +// logger.trace("import handler is not supported", ex); +// } else if (logger.isInfoEnabled()) { +// logger.info("{} handler does not support import of repositories", +// t.getName()); +// } +// } +// } else if (logger.isWarnEnabled()) { +// logger.warn("could not find handler for type {}", t.getName()); +// } +// } +// +// return types; +// } /** * Handle creation failures. @@ -518,49 +501,49 @@ public class RepositoryImportResource { Response.Status.INTERNAL_SERVER_ERROR); } - /** - * Import repositories from a specific type. - * - * @param repositories repository list - * @param type type of repository - */ - private void importFromDirectory(List repositories, String type) { - RepositoryHandler handler = manager.getHandler(type); - - if (handler != null) { - logger.info("start directory import for repository type {}", type); - - try { - List repositoryNames = - handler.getImportHandler().importRepositories(manager); - - if (repositoryNames != null) { - for (String repositoryName : repositoryNames) { - // TODO #8783 - /*Repository repository = null; //manager.get(type, repositoryName); - - if (repository != null) - { - repositories.add(repository); - } - else if (logger.isWarnEnabled()) - { - logger.warn("could not find imported repository {}", - repositoryName); - }*/ - } - } - } catch (FeatureNotSupportedException ex) { - throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); - } catch (IOException ex) { - throw new WebApplicationException(ex); - } catch (InternalRepositoryException ex) { - throw new WebApplicationException(ex); - } - } else { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - } +// /** +// * Import repositories from a specific type. +// * +// * @param repositories repository list +// * @param type type of repository +// */ +// private void importFromDirectory(List repositories, String type) { +// RepositoryHandler handler = manager.getHandler(type); +// +// if (handler != null) { +// logger.info("start directory import for repository type {}", type); +// +// try { +// List repositoryNames = +// handler.getImportHandler().importRepositories(manager); +// +// if (repositoryNames != null) { +// for (String repositoryName : repositoryNames) { +// // TODO #8783 +// /*Repository repository = null; //manager.get(type, repositoryName); +// +// if (repository != null) +// { +// repositories.add(repository); +// } +// else if (logger.isWarnEnabled()) +// { +// logger.warn("could not find imported repository {}", +// repositoryName); +// }*/ +// } +// } +// } catch (FeatureNotSupportedException ex) { +// throw new WebApplicationException(ex, Response.Status.BAD_REQUEST); +// } catch (IOException ex) { +// throw new WebApplicationException(ex); +// } catch (InternalRepositoryException ex) { +// throw new WebApplicationException(ex); +// } +// } else { +// throw new WebApplicationException(Response.Status.BAD_REQUEST); +// } +// } private Type type(String type) { RepositoryHandler handler = manager.getHandler(type); @@ -574,18 +557,17 @@ public class RepositoryImportResource { return handler.getType(); } - /** - * Request for importing external repositories which are accessible via url. - */ - @XmlRootElement(name = "import") - @XmlAccessorType(XmlAccessType.FIELD) - @Value - @ToString - public static class UrlImportRequest { - private String namespace; - private String name; + @Getter + @Setter + @NoArgsConstructor + @SuppressWarnings("java:S2160") + public static class RepositoryImportDto extends RepositoryDto { private String url; private String username; private String password; + + RepositoryImportDto(Links links, Embedded embedded) { + super(links, embedded); + } } }