Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String> paths;

private String filesystem;

public Set<String> getPaths() {
return paths;
}

public void setPaths(Set<String> 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 + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -76,6 +78,10 @@ public class AdminController
@RestClient
PromoteService promoteService;

@Inject
@RestClient
StorageService storageService;

@Inject
private IndyTrackingConfiguration config;

Expand Down Expand Up @@ -410,6 +416,42 @@ public boolean deletionAdditionalGuardCheck( BatchDeleteRequest deleteRequest )
return isOk.get();
}

/**
* Post-action after successful batch delete: cleans up empty parent folders.
* <p>
* 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.
* </p>
*
* @param filesystem the target filesystem/storeKey as a string
* @param paths the set of deleted file paths
*/
public void cleanupEmptyFolders(String filesystem, Set<String> 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<String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
}
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}