mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-03 16:28:59 +02:00
@@ -84,6 +84,7 @@ public class MeDtoFactory extends HalAppenderMapper {
|
||||
|
||||
private MeDto createDto(User user) {
|
||||
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
|
||||
|
||||
if (UserPermissions.delete(user).isPermitted()) {
|
||||
linksBuilder.single(link("delete", resourceLinks.me().delete(user.getName())));
|
||||
}
|
||||
@@ -100,6 +101,8 @@ public class MeDtoFactory extends HalAppenderMapper {
|
||||
linksBuilder.single(link("apiKeys", resourceLinks.apiKeyCollection().self(user.getName())));
|
||||
}
|
||||
|
||||
linksBuilder.single(link("notifications", resourceLinks.me().notifications()));
|
||||
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), new Me(), user);
|
||||
|
||||
|
||||
@@ -65,13 +65,15 @@ public class MeResource {
|
||||
private final PasswordService passwordService;
|
||||
|
||||
private final Provider<ApiKeyResource> apiKeyResourceProvider;
|
||||
private final Provider<NotificationResource> notificationResourceProvider;
|
||||
|
||||
@Inject
|
||||
public MeResource(MeDtoFactory meDtoFactory, UserManager userManager, PasswordService passwordService, Provider<ApiKeyResource> apiKeyResourceProvider) {
|
||||
public MeResource(MeDtoFactory meDtoFactory, UserManager userManager, PasswordService passwordService, Provider<ApiKeyResource> apiKeyResourceProvider, Provider<NotificationResource> notificationResourceProvider) {
|
||||
this.meDtoFactory = meDtoFactory;
|
||||
this.userManager = userManager;
|
||||
this.passwordService = passwordService;
|
||||
this.apiKeyResourceProvider = apiKeyResourceProvider;
|
||||
this.notificationResourceProvider = notificationResourceProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,4 +146,9 @@ public class MeResource {
|
||||
public ApiKeyResource apiKeys() {
|
||||
return apiKeyResourceProvider.get();
|
||||
}
|
||||
|
||||
@Path("notifications")
|
||||
public NotificationResource notifications() {
|
||||
return notificationResourceProvider.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Data;
|
||||
import sonia.scm.notifications.StoredNotification;
|
||||
import sonia.scm.notifications.Type;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
public class NotificationDto extends HalRepresentation {
|
||||
|
||||
private Instant createdAt;
|
||||
private Type type;
|
||||
private String link;
|
||||
private String message;
|
||||
|
||||
public NotificationDto(StoredNotification notification, Links links) {
|
||||
super(links);
|
||||
this.type = notification.getType();
|
||||
this.link = notification.getLink();
|
||||
this.message = notification.getMessage();
|
||||
this.createdAt = notification.getCreatedAt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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 de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import sonia.scm.notifications.NotificationChannelId;
|
||||
import sonia.scm.notifications.NotificationStore;
|
||||
import sonia.scm.notifications.StoredNotification;
|
||||
import sonia.scm.security.SessionId;
|
||||
import sonia.scm.sse.Channel;
|
||||
import sonia.scm.sse.ChannelRegistry;
|
||||
import sonia.scm.sse.Registration;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
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.UriInfo;
|
||||
import javax.ws.rs.sse.Sse;
|
||||
import javax.ws.rs.sse.SseEventSink;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@OpenAPIDefinition(tags = {
|
||||
@Tag(name = "Notifications", description = "Notification related endpoints")
|
||||
})
|
||||
public class NotificationResource {
|
||||
|
||||
private final NotificationStore store;
|
||||
private final ChannelRegistry channelRegistry;
|
||||
|
||||
@Inject
|
||||
public NotificationResource(NotificationStore store, ChannelRegistry channelRegistry) {
|
||||
this.store = store;
|
||||
this.channelRegistry = channelRegistry;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.NOTIFICATION_COLLECTION)
|
||||
@Operation(
|
||||
summary = "Notifications",
|
||||
description = "Returns all notifications for the current user",
|
||||
tags = "Notifications",
|
||||
operationId = "notifications_get_all"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.NOTIFICATION_COLLECTION,
|
||||
schema = @Schema(implementation = HalRepresentation.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public HalRepresentation getAll(@Context UriInfo uriInfo) {
|
||||
return new HalRepresentation(
|
||||
createCollectionLinks(uriInfo),
|
||||
createEmbeddedNotifications(uriInfo)
|
||||
);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{id}")
|
||||
@Operation(
|
||||
summary = "Dismiss",
|
||||
description = "Dismiss the notification with the given id",
|
||||
tags = "Notifications",
|
||||
operationId = "notifications_dismiss"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "no content"
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response dismiss(@PathParam("id") String id) {
|
||||
store.remove(id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("")
|
||||
@Operation(
|
||||
summary = "Dismiss all",
|
||||
description = "Dismiss all notifications for the current user",
|
||||
tags = "Notifications",
|
||||
operationId = "notifications_dismiss_all"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "no content"
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response dismissAll() {
|
||||
store.clear();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("subscribe")
|
||||
@Produces(MediaType.SERVER_SENT_EVENTS)
|
||||
@Operation(
|
||||
summary = "Subscribe",
|
||||
description = "Subscribe to the sse event stream of notification for the current user",
|
||||
tags = "Notifications",
|
||||
operationId = "notifications_subscribe"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200"
|
||||
)
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public void subscribe(@Context Sse sse, @Context SseEventSink eventSink, @QueryParam(SessionId.PARAMETER) SessionId sessionId) {
|
||||
Channel channel = channelRegistry.channel(NotificationChannelId.current());
|
||||
channel.register(new Registration(sessionId, sse, eventSink));
|
||||
}
|
||||
|
||||
private Embedded createEmbeddedNotifications(UriInfo uriInfo) {
|
||||
List<NotificationDto> notifications = store.getAll()
|
||||
.stream()
|
||||
.map(n -> map(uriInfo, n))
|
||||
.collect(Collectors.toList());
|
||||
return Embedded.embedded("notifications", notifications);
|
||||
}
|
||||
|
||||
private NotificationDto map(UriInfo uriInfo, StoredNotification storedNotification) {
|
||||
String href = uriInfo.getAbsolutePathBuilder().path(storedNotification.getId()).build().toASCIIString();
|
||||
Links links = Links.linkingTo().single(Link.link("dismiss", href)).build();
|
||||
return new NotificationDto(storedNotification, links);
|
||||
}
|
||||
|
||||
private Links createCollectionLinks(UriInfo uriInfo) {
|
||||
String self = selfLink(uriInfo);
|
||||
return Links.linkingTo()
|
||||
.self(self)
|
||||
.single(Link.link("clear", self))
|
||||
.single(Link.link("subscribe", subscribeLink(uriInfo)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String selfLink(UriInfo uriInfo) {
|
||||
return uriInfo.getAbsolutePath().toASCIIString();
|
||||
}
|
||||
|
||||
private String subscribeLink(UriInfo uriInfo) {
|
||||
return uriInfo.getRequestUriBuilder().path("subscribe").build().toASCIIString();
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.Type;
|
||||
import sonia.scm.importexport.ExportFileExtensionResolver;
|
||||
import sonia.scm.importexport.ExportNotificationHandler;
|
||||
import sonia.scm.importexport.ExportService;
|
||||
import sonia.scm.importexport.FullScmRepositoryExporter;
|
||||
import sonia.scm.importexport.RepositoryImportExportEncryption;
|
||||
@@ -98,6 +99,7 @@ public class RepositoryExportResource {
|
||||
private final RepositoryExportInformationToDtoMapper informationToDtoMapper;
|
||||
private final ExportFileExtensionResolver fileExtensionResolver;
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final ExportNotificationHandler notificationHandler;
|
||||
|
||||
@Inject
|
||||
public RepositoryExportResource(RepositoryManager manager,
|
||||
@@ -108,8 +110,8 @@ public class RepositoryExportResource {
|
||||
RepositoryExportInformationToDtoMapper informationToDtoMapper,
|
||||
ExportFileExtensionResolver fileExtensionResolver,
|
||||
ResourceLinks resourceLinks,
|
||||
MeterRegistry registry
|
||||
) {
|
||||
MeterRegistry registry,
|
||||
ExportNotificationHandler notificationHandler) {
|
||||
this.manager = manager;
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.fullScmRepositoryExporter = fullScmRepositoryExporter;
|
||||
@@ -119,6 +121,7 @@ public class RepositoryExportResource {
|
||||
this.fileExtensionResolver = fileExtensionResolver;
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.repositoryExportHandler = this.createExportHandlerPool(registry);
|
||||
this.notificationHandler = notificationHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,11 +315,7 @@ public class RepositoryExportResource {
|
||||
Repository repository = getVerifiedRepository(namespace, name);
|
||||
RepositoryPermissions.export(repository).check();
|
||||
checkRepositoryIsAlreadyExporting(repository);
|
||||
return exportAsync(repository, request.isAsync(), () -> {
|
||||
Response response = exportFullRepository(repository, request.getPassword(), request.isAsync());
|
||||
exportService.setExportFinished(repository);
|
||||
return response;
|
||||
});
|
||||
return exportAsync(repository, request.isAsync(), () -> exportFullRepository(repository, request.getPassword(), request.isAsync()));
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@@ -458,6 +457,7 @@ public class RepositoryExportResource {
|
||||
return Response.status(204).build();
|
||||
} else {
|
||||
StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os, password);
|
||||
exportService.setExportFinished(repository);
|
||||
|
||||
return Response
|
||||
.ok(output, "application/x-gzip")
|
||||
@@ -485,6 +485,7 @@ public class RepositoryExportResource {
|
||||
return createResponse(repository, fileExtension, compressed, output);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
notificationHandler.handleFailedExport(repository);
|
||||
throw new ExportFailedException(entity(repository).build(), "repository export failed", e);
|
||||
}
|
||||
}
|
||||
@@ -533,7 +534,7 @@ public class RepositoryExportResource {
|
||||
return executorService;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("java:S110") // is ok for this type of exceptions
|
||||
private static class WrongTypeException extends BadRequestException {
|
||||
|
||||
private static final String CODE = "4hSNNTBiu1";
|
||||
|
||||
@@ -194,10 +194,12 @@ class ResourceLinks {
|
||||
|
||||
static class MeLinks {
|
||||
private final LinkBuilder meLinkBuilder;
|
||||
private final LinkBuilder notificationLinkBuilder;
|
||||
private UserLinks userLinks;
|
||||
|
||||
MeLinks(ScmPathInfo pathInfo, UserLinks user) {
|
||||
meLinkBuilder = new LinkBuilder(pathInfo, MeResource.class);
|
||||
notificationLinkBuilder = new LinkBuilder(pathInfo, MeResource.class, NotificationResource.class);
|
||||
userLinks = user;
|
||||
}
|
||||
|
||||
@@ -216,6 +218,10 @@ class ResourceLinks {
|
||||
public String passwordChange() {
|
||||
return meLinkBuilder.method("changePassword").parameters().href();
|
||||
}
|
||||
|
||||
String notifications() {
|
||||
return notificationLinkBuilder.method("notifications").parameters().method("getAll").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public ApiKeyCollectionLinks apiKeyCollection() {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import sonia.scm.notifications.Notification;
|
||||
import sonia.scm.notifications.NotificationSender;
|
||||
import sonia.scm.notifications.Type;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ExportNotificationHandler {
|
||||
|
||||
private final NotificationSender notificationSender;
|
||||
|
||||
@Inject
|
||||
public ExportNotificationHandler(NotificationSender notificationSender) {
|
||||
this.notificationSender = notificationSender;
|
||||
}
|
||||
|
||||
public void handleFailedExport(Repository repository) {
|
||||
notificationSender.send(getExportFailedNotification(repository));
|
||||
}
|
||||
|
||||
public void handleSuccessfulExport(Repository repository) {
|
||||
notificationSender.send(getExportSuccessfulNotification(repository));
|
||||
}
|
||||
|
||||
private Notification getExportFailedNotification(Repository repository) {
|
||||
return new Notification(Type.ERROR, "/repo/" + repository.getNamespaceAndName() + "/settings/general", "exportFailed");
|
||||
}
|
||||
|
||||
private Notification getExportSuccessfulNotification(Repository repository) {
|
||||
return new Notification(Type.SUCCESS, "/repo/" + repository.getNamespaceAndName() + "/settings/general", "exportFinished");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,12 +55,14 @@ public class ExportService {
|
||||
private final BlobStoreFactory blobStoreFactory;
|
||||
private final DataStoreFactory dataStoreFactory;
|
||||
private final ExportFileExtensionResolver fileExtensionResolver;
|
||||
private final ExportNotificationHandler notificationHandler;
|
||||
|
||||
@Inject
|
||||
public ExportService(BlobStoreFactory blobStoreFactory, DataStoreFactory dataStoreFactory, ExportFileExtensionResolver fileExtensionResolver) {
|
||||
public ExportService(BlobStoreFactory blobStoreFactory, DataStoreFactory dataStoreFactory, ExportFileExtensionResolver fileExtensionResolver, ExportNotificationHandler notificationHandler) {
|
||||
this.blobStoreFactory = blobStoreFactory;
|
||||
this.dataStoreFactory = dataStoreFactory;
|
||||
this.fileExtensionResolver = fileExtensionResolver;
|
||||
this.notificationHandler = notificationHandler;
|
||||
}
|
||||
|
||||
public OutputStream store(Repository repository, boolean withMetadata, boolean compressed, boolean encrypted) {
|
||||
@@ -69,6 +71,7 @@ public class ExportService {
|
||||
try {
|
||||
return storeNewBlob(repository.getId()).getOutputStream();
|
||||
} catch (IOException e) {
|
||||
notificationHandler.handleFailedExport(repository);
|
||||
throw new ExportFailedException(
|
||||
entity(repository).build(),
|
||||
"Could not store repository export to blob file",
|
||||
@@ -120,6 +123,7 @@ public class ExportService {
|
||||
RepositoryExportInformation info = dataStore.get(repository.getId());
|
||||
info.setStatus(ExportStatus.FINISHED);
|
||||
dataStore.put(repository.getId(), info);
|
||||
notificationHandler.handleSuccessfulExport(repository);
|
||||
}
|
||||
|
||||
public boolean isExporting(Repository repository) {
|
||||
|
||||
@@ -59,6 +59,7 @@ public class FullScmRepositoryExporter {
|
||||
private final WorkdirProvider workdirProvider;
|
||||
private final RepositoryExportingCheck repositoryExportingCheck;
|
||||
private final RepositoryImportExportEncryption repositoryImportExportEncryption;
|
||||
private final ExportNotificationHandler notificationHandler;
|
||||
|
||||
@Inject
|
||||
public FullScmRepositoryExporter(EnvironmentInformationXmlGenerator environmentGenerator,
|
||||
@@ -67,7 +68,7 @@ public class FullScmRepositoryExporter {
|
||||
TarArchiveRepositoryStoreExporter storeExporter,
|
||||
WorkdirProvider workdirProvider,
|
||||
RepositoryExportingCheck repositoryExportingCheck,
|
||||
RepositoryImportExportEncryption repositoryImportExportEncryption) {
|
||||
RepositoryImportExportEncryption repositoryImportExportEncryption, ExportNotificationHandler notificationHandler) {
|
||||
this.environmentGenerator = environmentGenerator;
|
||||
this.metadataGenerator = metadataGenerator;
|
||||
this.serviceFactory = serviceFactory;
|
||||
@@ -75,13 +76,19 @@ public class FullScmRepositoryExporter {
|
||||
this.workdirProvider = workdirProvider;
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
this.repositoryImportExportEncryption = repositoryImportExportEncryption;
|
||||
this.notificationHandler = notificationHandler;
|
||||
}
|
||||
|
||||
public void export(Repository repository, OutputStream outputStream, String password) {
|
||||
repositoryExportingCheck.withExportingLock(repository, () -> {
|
||||
exportInLock(repository, outputStream, password);
|
||||
return null;
|
||||
});
|
||||
try {
|
||||
repositoryExportingCheck.withExportingLock(repository, () -> {
|
||||
exportInLock(repository, outputStream, password);
|
||||
return null;
|
||||
});
|
||||
} catch (ExportFailedException ex) {
|
||||
notificationHandler.handleFailedExport(repository);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void exportInLock(Repository repository, OutputStream outputStream, String password) {
|
||||
|
||||
@@ -63,6 +63,8 @@ import sonia.scm.net.ahc.ContentTransformer;
|
||||
import sonia.scm.net.ahc.DefaultAdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.JsonContentTransformer;
|
||||
import sonia.scm.net.ahc.XmlContentTransformer;
|
||||
import sonia.scm.notifications.DefaultNotificationSender;
|
||||
import sonia.scm.notifications.NotificationSender;
|
||||
import sonia.scm.plugin.DefaultPluginManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
@@ -263,6 +265,8 @@ class ScmServletModule extends ServletModule {
|
||||
bind(PermissionProvider.class).to(RepositoryPermissionProvider.class);
|
||||
|
||||
bind(HealthCheckService.class).to(DefaultHealthCheckService.class);
|
||||
|
||||
bind(NotificationSender.class).to(DefaultNotificationSender.class);
|
||||
}
|
||||
|
||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.notifications;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import sonia.scm.sse.Channel;
|
||||
import sonia.scm.sse.ChannelRegistry;
|
||||
import sonia.scm.sse.Message;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class DefaultNotificationSender implements NotificationSender {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String MESSAGE_NAME = "notification";
|
||||
|
||||
private final NotificationStore store;
|
||||
private final ChannelRegistry channelRegistry;
|
||||
|
||||
@Inject
|
||||
public DefaultNotificationSender(NotificationStore store, ChannelRegistry channelRegistry) {
|
||||
this.store = store;
|
||||
this.channelRegistry = channelRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Notification notification, String recipient) {
|
||||
store.add(notification, recipient);
|
||||
Channel channel = channelRegistry.channel(new NotificationChannelId(recipient));
|
||||
channel.broadcast(message(notification));
|
||||
}
|
||||
|
||||
private Message message(Notification notification) {
|
||||
return new Message(MESSAGE_NAME, Notification.class, notification);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.notifications;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class NotificationChannelId {
|
||||
|
||||
private final String username;
|
||||
|
||||
public NotificationChannelId(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public static NotificationChannelId current() {
|
||||
return new NotificationChannelId(
|
||||
SecurityUtils.getSubject().getPrincipal().toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.notifications;
|
||||
|
||||
import com.github.legman.Subscribe;
|
||||
import com.google.common.util.concurrent.Striped;
|
||||
import lombok.Data;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.user.UserEvent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlElementWrapper;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Singleton
|
||||
@SuppressWarnings("UnstableApiUsage") // striped is still marked as beta
|
||||
public class NotificationStore {
|
||||
|
||||
private static final String NAME = "notifications";
|
||||
private final DataStore<StoredNotifications> store;
|
||||
private final KeyGenerator keyGenerator;
|
||||
private final Striped<ReadWriteLock> locks = Striped.readWriteLock(10);
|
||||
|
||||
@Inject
|
||||
public NotificationStore(DataStoreFactory dataStoreFactory, KeyGenerator keyGenerator) {
|
||||
this.store = dataStoreFactory.withType(StoredNotifications.class)
|
||||
.withName(NAME)
|
||||
.build();
|
||||
this.keyGenerator = keyGenerator;
|
||||
}
|
||||
|
||||
public String add(Notification notification, String username) {
|
||||
Lock lock = locks.get(username).writeLock();
|
||||
try {
|
||||
lock.lock();
|
||||
StoredNotifications notifications = get(username);
|
||||
String id = keyGenerator.createKey();
|
||||
notifications.getEntries().add(new StoredNotification(id, notification));
|
||||
store.put(username, notifications);
|
||||
return id;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private StoredNotifications get(String username) {
|
||||
return store.getOptional(username).orElse(new StoredNotifications());
|
||||
}
|
||||
|
||||
public List<StoredNotification> getAll() {
|
||||
String username = getCurrentUsername();
|
||||
Lock lock = locks.get(username).readLock();
|
||||
try {
|
||||
lock.lock();
|
||||
StoredNotifications notifications = get(username);
|
||||
return Collections.unmodifiableList(notifications.getEntries());
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(String id) {
|
||||
String username = getCurrentUsername();
|
||||
Lock lock = locks.get(username).writeLock();
|
||||
try {
|
||||
lock.lock();
|
||||
StoredNotifications notifications = get(username);
|
||||
List<StoredNotification> entries = notifications.getEntries()
|
||||
.stream()
|
||||
.filter(n -> !id.equals(n.id))
|
||||
.collect(Collectors.toList());
|
||||
notifications.setEntries(entries);
|
||||
store.put(username, notifications);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
String username = getCurrentUsername();
|
||||
Lock lock = locks.get(username).writeLock();
|
||||
try {
|
||||
lock.lock();
|
||||
StoredNotifications notifications = get(username);
|
||||
notifications.getEntries().clear();
|
||||
store.put(username, notifications);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private String getCurrentUsername() {
|
||||
return SecurityUtils.getSubject().getPrincipal().toString();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void handle(UserEvent event) {
|
||||
if (event.getEventType() == HandlerEventType.DELETE) {
|
||||
String username = event.getItem().getName();
|
||||
Lock lock = locks.get(username).writeLock();
|
||||
try {
|
||||
lock.lock();
|
||||
store.remove(username);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
static class StoredNotifications {
|
||||
@XmlElement(name = "notification")
|
||||
@XmlElementWrapper(name = "notifications")
|
||||
private List<StoredNotification> entries;
|
||||
|
||||
public List<StoredNotification> getEntries() {
|
||||
if (entries == null) {
|
||||
entries = new ArrayList<>();
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.notifications;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sonia.scm.xml.XmlInstantAdapter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class StoredNotification {
|
||||
|
||||
String id;
|
||||
Type type;
|
||||
String link;
|
||||
String message;
|
||||
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
||||
Instant createdAt;
|
||||
|
||||
StoredNotification(String id, Notification notification) {
|
||||
this.id = id;
|
||||
this.createdAt = notification.getCreatedAt();
|
||||
this.type = notification.getType();
|
||||
this.link = notification.getLink();
|
||||
this.message = notification.getMessage();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user