diff --git a/src/main/java/org/commonjava/indy/service/tracking/client/storage/StorageBatchDeleteRequest.java b/src/main/java/org/commonjava/indy/service/tracking/client/storage/StorageBatchDeleteRequest.java new file mode 100644 index 0000000..dcfa8e8 --- /dev/null +++ b/src/main/java/org/commonjava/indy/service/tracking/client/storage/StorageBatchDeleteRequest.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2022-2023 Red Hat, Inc. (https://github.com/Commonjava/indy-tracking-service) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonjava.indy.service.tracking.client.storage; + +import java.util.Set; + +/** + * Delete multiple paths in one filesystem. + */ +public class StorageBatchDeleteRequest { + private Set paths; + + private String filesystem; + + public Set getPaths() { + return paths; + } + + public void setPaths(Set paths) { + this.paths = paths; + } + + public String getFilesystem() { + return filesystem; + } + + public void setFilesystem(String filesystem) { + this.filesystem = filesystem; + } + + @Override + public String toString() { + return "BatchDeleteRequest{" + + "paths=" + paths + + ", filesystem='" + filesystem + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/commonjava/indy/service/tracking/client/storage/StorageService.java b/src/main/java/org/commonjava/indy/service/tracking/client/storage/StorageService.java new file mode 100644 index 0000000..96370c0 --- /dev/null +++ b/src/main/java/org/commonjava/indy/service/tracking/client/storage/StorageService.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2022-2023 Red Hat, Inc. (https://github.com/Commonjava/indy-tracking-service) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonjava.indy.service.tracking.client.storage; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.commonjava.indy.service.security.jaxrs.CustomClientRequestFilter; + +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.Consumes; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + +@Path("/api/storage") +@RegisterRestClient(configKey = "storage-service-api") +@RegisterProvider(CustomClientRequestFilter.class) +public interface StorageService { + /** + * Delete empty folders by Storage BatchDeleteRequest as JSON body. + */ + @DELETE + @Path("/maint/folders/empty") + @Consumes(APPLICATION_JSON) + Response cleanupEmptyFolders(StorageBatchDeleteRequest request); +} \ No newline at end of file diff --git a/src/main/java/org/commonjava/indy/service/tracking/controller/AdminController.java b/src/main/java/org/commonjava/indy/service/tracking/controller/AdminController.java index babfaee..57401ee 100644 --- a/src/main/java/org/commonjava/indy/service/tracking/controller/AdminController.java +++ b/src/main/java/org/commonjava/indy/service/tracking/controller/AdminController.java @@ -58,6 +58,8 @@ import static org.commonjava.indy.service.tracking.util.TrackingUtils.readZipInputStreamAnd; import static org.commonjava.indy.service.tracking.util.TrackingUtils.zipTrackedContent; +import org.commonjava.indy.service.tracking.client.storage.StorageBatchDeleteRequest; +import org.commonjava.indy.service.tracking.client.storage.StorageService; @ApplicationScoped public class AdminController @@ -76,6 +78,10 @@ public class AdminController @RestClient PromoteService promoteService; + @Inject + @RestClient + StorageService storageService; + @Inject private IndyTrackingConfiguration config; @@ -410,6 +416,42 @@ public boolean deletionAdditionalGuardCheck( BatchDeleteRequest deleteRequest ) return isOk.get(); } + /** + * Post-action after successful batch delete: cleans up empty parent folders. + *

+ * For each deleted path, collects its immediate parent folder (one level up). + * Then calls the storage service to clean up these folders, relying on the + * storage API to handle ancestor folders as needed. + *

+ * + * @param filesystem the target filesystem/storeKey as a string + * @param paths the set of deleted file paths + */ + public void cleanupEmptyFolders(String filesystem, Set paths) { + logger.info("Post-action: cleanupEmptyFolder, filesystem={}, paths={}", filesystem, paths); + if (paths == null || paths.isEmpty()) { + logger.info("No paths to process for cleanup."); + return; + } + Set folders = new HashSet<>(); + for (String path : paths) { + int idx = path.lastIndexOf('/'); + if (idx > 0) { + String folder = path.substring(0, idx); + folders.add(folder); + } + } + StorageBatchDeleteRequest req = new StorageBatchDeleteRequest(); + req.setFilesystem(filesystem); + req.setPaths(folders); + try { + Response resp = storageService.cleanupEmptyFolders(req); + logger.info("Cleanup empty folders, req: {}, status {}", req, resp.getStatus()); + } catch (Exception e) { + logger.warn("Failed to cleanup folders, request: {}, error: {}", req, e.getMessage(), e); + } + } + private boolean isSuccess(Response resp) { return Response.Status.fromStatusCode(resp.getStatus()).getFamily() == Response.Status.Family.SUCCESSFUL; diff --git a/src/main/java/org/commonjava/indy/service/tracking/jaxrs/AdminResource.java b/src/main/java/org/commonjava/indy/service/tracking/jaxrs/AdminResource.java index c8a2718..55e0a8c 100644 --- a/src/main/java/org/commonjava/indy/service/tracking/jaxrs/AdminResource.java +++ b/src/main/java/org/commonjava/indy/service/tracking/jaxrs/AdminResource.java @@ -63,6 +63,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import static java.util.Collections.emptySet; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; @@ -95,6 +97,10 @@ public class AdminResource @Inject private IndyTrackingConfiguration config; + // Inject a managed Executor for running async post-actions without blocking the main thread + @Inject + Executor executor; + public AdminResource() { } @@ -424,7 +430,12 @@ public Response doDelete( @Context final UriInfo uriInfo, final BatchDeleteReque } } - return maintenanceService.doDelete( request ); + Response response = maintenanceService.doDelete( request ); + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + // Run the cleanupEmptyFolder post-action asynchronously + CompletableFuture.runAsync(() -> controller.cleanupEmptyFolders( + request.getStoreKey().toString(), request.getPaths()), executor); + } + return response; } - } \ No newline at end of file diff --git a/src/test/java/org/commonjava/indy/service/tracking/handler/MockableStorageService.java b/src/test/java/org/commonjava/indy/service/tracking/handler/MockableStorageService.java new file mode 100644 index 0000000..ada4d84 --- /dev/null +++ b/src/test/java/org/commonjava/indy/service/tracking/handler/MockableStorageService.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2022-2023 Red Hat, Inc. (https://github.com/Commonjava/indy-tracking-service) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.commonjava.indy.service.tracking.handler; + +import io.quarkus.test.Mock; +import jakarta.ws.rs.core.Response; +import org.apache.http.HttpStatus; +import org.commonjava.indy.service.tracking.client.storage.StorageBatchDeleteRequest; +import org.commonjava.indy.service.tracking.client.storage.StorageService; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Mock +@RestClient +public class MockableStorageService + implements StorageService +{ + @Override + public Response cleanupEmptyFolders( StorageBatchDeleteRequest request ) + { + return Response.status( HttpStatus.SC_OK ).build(); + } +}