Skip to content

Commit b0d136c

Browse files
authored
Merge pull request #11230 from GlobalDataverseCommunityConsortium/DANS-bulk_file_delete
Delete Files From Dataset
2 parents 30736a5 + af2bcc5 commit b0d136c

File tree

5 files changed

+246
-0
lines changed

5 files changed

+246
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A new /api/datasets/{id}/deleteFiles call has beed added to the API, allowing delete of multiple file from the latest version of a dataset in one operation.

doc/sphinx-guides/source/api/native-api.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3553,6 +3553,39 @@ To update the blocks that are linked, send an array with those blocks.
35533553
35543554
To remove all links to blocks, send an empty array.
35553555
3556+
Delete Files from a Dataset
3557+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
3558+
3559+
Delete files from a dataset. This API call allows you to delete multiple files from a dataset in a single operation.
3560+
3561+
.. code-block:: bash
3562+
3563+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
3564+
export SERVER_URL=https://demo.dataverse.org
3565+
export PERSISTENT_IDENTIFIER=doi:10.5072/FK2ABCDEF
3566+
3567+
curl -H "X-Dataverse-key:$API_TOKEN" -X PUT "$SERVER_URL/api/datasets/:persistentId/deleteFiles?persistentId=$PERSISTENT_IDENTIFIER" \
3568+
-H "Content-Type: application/json" \
3569+
-d '{"fileIds": [1, 2, 3]}'
3570+
3571+
The fully expanded example above (without environment variables) looks like this:
3572+
3573+
.. code-block:: bash
3574+
3575+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X PUT "https://demo.dataverse.org/api/datasets/:persistentId/deleteFiles?persistentId=doi:10.5072/FK2ABCDEF" \
3576+
-H "Content-Type: application/json" \
3577+
-d '{"fileIds": [1, 2, 3]}'
3578+
3579+
The ``fileIds`` in the JSON payload should be an array of file IDs that you want to delete from the dataset.
3580+
3581+
You must have the appropriate permissions to delete files from the dataset.
3582+
3583+
Upon success, the API will return a JSON response with a success message and the number of files deleted.
3584+
3585+
The API call will report a 400 (BAD REQUEST) error if any of the files specified do not exist or are not in the latest version of the specified dataset.
3586+
The ``fileIds`` in the JSON payload should be an array of file IDs that you want to delete from the dataset.
3587+
3588+
35563589
Files
35573590
-----
35583591

src/main/java/edu/harvard/iq/dataverse/api/Datasets.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@
9999

100100
import static edu.harvard.iq.dataverse.api.ApiConstants.*;
101101
import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException;
102+
import edu.harvard.iq.dataverse.engine.command.exception.PermissionException;
102103
import edu.harvard.iq.dataverse.dataset.DatasetType;
103104
import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean;
104105
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*;
105106
import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder;
106107
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
107108
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
109+
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
108110

109111
@Path("datasets")
110112
public class Datasets extends AbstractApiBean {
@@ -5368,4 +5370,67 @@ public Response updateDatasetTypeLinksWithMetadataBlocks(@Context ContainerReque
53685370
}
53695371
}
53705372

5373+
@PUT
5374+
@AuthRequired
5375+
@Path("{id}/deleteFiles")
5376+
@Consumes(MediaType.APPLICATION_JSON)
5377+
public Response deleteDatasetFiles(@Context ContainerRequestContext crc, @PathParam("id") String id,
5378+
JsonArray fileIds) {
5379+
try {
5380+
getRequestAuthenticatedUserOrDie(crc);
5381+
} catch (WrappedResponse ex) {
5382+
return ex.getResponse();
5383+
}
5384+
return response(req -> {
5385+
Dataset dataset = findDatasetOrDie(id);
5386+
// Convert JsonArray to List<Long>
5387+
List<Long> fileIdList = new ArrayList<>();
5388+
for (JsonValue value : fileIds) {
5389+
fileIdList.add(((JsonNumber) value).longValue());
5390+
}
5391+
// Find the files to be deleted
5392+
List<FileMetadata> filesToDelete = dataset.getOrCreateEditVersion().getFileMetadatas().stream()
5393+
.filter(fileMetadata -> fileIdList.contains(fileMetadata.getDataFile().getId()))
5394+
.collect(Collectors.toList());
5395+
5396+
if (filesToDelete.isEmpty()) {
5397+
return badRequest("No files found with the provided IDs.");
5398+
}
5399+
5400+
if (filesToDelete.size() != fileIds.size()) {
5401+
return badRequest(
5402+
"Some files listed are not present in the latest dataset version and cannot be deleted.");
5403+
}
5404+
try {
5405+
5406+
UpdateDatasetVersionCommand update_cmd = new UpdateDatasetVersionCommand(dataset, req, filesToDelete);
5407+
5408+
commandEngine.submit(update_cmd);
5409+
for (FileMetadata fm : filesToDelete) {
5410+
DataFile dataFile = fm.getDataFile();
5411+
boolean deletePhysicalFile = !dataFile.isReleased();
5412+
if (deletePhysicalFile) {
5413+
try {
5414+
fileService.finalizeFileDelete(dataFile.getId(),
5415+
fileService.getPhysicalFileToDelete(dataFile));
5416+
} catch (IOException ioex) {
5417+
logger.warning("Failed to delete the physical file associated with the deleted datafile id="
5418+
+ dataFile.getId() + ", storage location: "
5419+
+ fileService.getPhysicalFileToDelete(dataFile));
5420+
}
5421+
}
5422+
}
5423+
} catch (PermissionException ex) {
5424+
return error(FORBIDDEN, "You do not have permission to delete files ont this dataset.");
5425+
} catch (CommandException ex) {
5426+
return error(BAD_REQUEST,
5427+
"File deletes failed for dataset ID " + id + " (CommandException): " + ex.getMessage());
5428+
} catch (EJBException ex) {
5429+
return error(jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR,
5430+
"File deletes failed for dataset ID " + id + "(EJBException): " + ex.getMessage());
5431+
}
5432+
return ok(fileIds.size() + " files deleted successfully");
5433+
5434+
}, getRequestUser(crc));
5435+
}
53715436
}

src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import static java.lang.Thread.sleep;
6464
import static org.hamcrest.CoreMatchers.*;
6565
import static org.hamcrest.Matchers.contains;
66+
import static org.hamcrest.Matchers.hasEntry;
6667
import static org.junit.jupiter.api.Assertions.*;
6768

6869
public class DatasetsIT {
@@ -5589,4 +5590,140 @@ public void testRequireFilesToPublishDatasets() {
55895590
publishDatasetResponse.prettyPrint();
55905591
publishDatasetResponse.then().assertThat().statusCode(OK.getStatusCode());
55915592
}
5593+
5594+
@Test
5595+
public void testDeleteFiles() {
5596+
Response createUser = UtilIT.createRandomUser();
5597+
String username = UtilIT.getUsernameFromResponse(createUser);
5598+
String apiToken = UtilIT.getApiTokenFromResponse(createUser);
5599+
5600+
Response createDataverseResponse = UtilIT.createRandomDataverse(apiToken);
5601+
String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse);
5602+
5603+
Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken);
5604+
Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse);
5605+
5606+
// Add files to the dataset
5607+
String pathToFile1 = "scripts/api/data/licenses/licenseCC0-1.0.json";
5608+
String pathToFile2 = "scripts/api/data/licenses/licenseCC-BY-4.0.json";
5609+
String pathToFile3 = "scripts/api/data/licenses/licenseCC-BY-NC-4.0.json";
5610+
String pathToFile4 = "scripts/api/data/licenses/licenseCC-BY-NC-ND-4.0.json";
5611+
String pathToFile5 = "scripts/api/data/licenses/licenseCC-BY-ND-4.0.json";
5612+
5613+
JsonObjectBuilder json = Json.createObjectBuilder();
5614+
json.add("description", "File 1");
5615+
Response addFile1Response = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile1, json.build(), apiToken);
5616+
Long file1Id = JsonPath.from(addFile1Response.body().asString()).getLong("data.files[0].dataFile.id");
5617+
5618+
json.add("description", "File 2");
5619+
Response addFile2Response = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile2, json.build(), apiToken);
5620+
Long file2Id = JsonPath.from(addFile2Response.body().asString()).getLong("data.files[0].dataFile.id");
5621+
5622+
json.add("description", "File 3");
5623+
Response addFile3Response = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile3, json.build(), apiToken);
5624+
Long file3Id = JsonPath.from(addFile3Response.body().asString()).getLong("data.files[0].dataFile.id");
5625+
5626+
json.add("description", "File 4");
5627+
Response addFile4Response = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile4, json.build(), apiToken);
5628+
Long file4Id = JsonPath.from(addFile4Response.body().asString()).getLong("data.files[0].dataFile.id");
5629+
5630+
json.add("description", "File 5");
5631+
Response addFile5Response = UtilIT.uploadFileViaNative(datasetId.toString(), pathToFile5, json.build(), apiToken);
5632+
Long file5Id = JsonPath.from(addFile5Response.body().asString()).getLong("data.files[0].dataFile.id");
5633+
5634+
// Delete files 1 and 2
5635+
JsonArrayBuilder fileIdsToDelete = Json.createArrayBuilder();
5636+
fileIdsToDelete.add(file1Id);
5637+
fileIdsToDelete.add(file2Id);
5638+
5639+
Response deleteFilesResponse = UtilIT.deleteDatasetFiles(datasetId.toString(), fileIdsToDelete.build(), apiToken);
5640+
deleteFilesResponse.then().assertThat()
5641+
.statusCode(OK.getStatusCode())
5642+
.body("data.message", startsWith("2"));
5643+
5644+
// Verify files were deleted
5645+
Response getDatasetResponse = UtilIT.nativeGet(datasetId, apiToken);
5646+
getDatasetResponse.then().assertThat()
5647+
.statusCode(OK.getStatusCode())
5648+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file1Id + " }.size()", equalTo(0))
5649+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file2Id + " }.size()", equalTo(0))
5650+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file3Id + " }.size()", equalTo(1))
5651+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file4Id + " }.size()", equalTo(1))
5652+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file5Id + " }.size()", equalTo(1));
5653+
5654+
5655+
// Test deleting after dataset publication
5656+
Response publishDataverseResponse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken);
5657+
publishDataverseResponse.then().assertThat().statusCode(OK.getStatusCode());
5658+
5659+
// Publish the dataset
5660+
Response publishDatasetResponse = UtilIT.publishDatasetViaNativeApi(datasetId, "major", apiToken);
5661+
publishDatasetResponse.then().assertThat()
5662+
.statusCode(OK.getStatusCode());
5663+
5664+
// Delete files 3 and 4 from the published dataset
5665+
fileIdsToDelete = Json.createArrayBuilder();
5666+
fileIdsToDelete.add(file3Id);
5667+
fileIdsToDelete.add(file4Id);
5668+
5669+
deleteFilesResponse = UtilIT.deleteDatasetFiles(datasetId.toString(), fileIdsToDelete.build(), apiToken);
5670+
deleteFilesResponse.then().assertThat()
5671+
.statusCode(OK.getStatusCode())
5672+
.body("data.message", startsWith("2"));
5673+
5674+
// Verify files were deleted
5675+
getDatasetResponse = UtilIT.nativeGet(datasetId, apiToken);
5676+
getDatasetResponse.then().assertThat()
5677+
.statusCode(OK.getStatusCode())
5678+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file3Id + " }.size()", equalTo(0))
5679+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file4Id + " }.size()", equalTo(0))
5680+
.body("data.latestVersion.files.findAll { it.dataFile.id == " + file5Id + " }.size()", equalTo(1));
5681+
5682+
// Test error conditions
5683+
5684+
// Try to delete a non-existent file
5685+
fileIdsToDelete = Json.createArrayBuilder();
5686+
fileIdsToDelete.add(999999L);
5687+
5688+
deleteFilesResponse = UtilIT.deleteDatasetFiles(datasetId.toString(), fileIdsToDelete.build(), apiToken);
5689+
deleteFilesResponse.then().assertThat()
5690+
.statusCode(BAD_REQUEST.getStatusCode())
5691+
.body("message", containsString("No files"));
5692+
5693+
// Try to delete files from a non-existent dataset
5694+
deleteFilesResponse = UtilIT.deleteDatasetFiles("999999", fileIdsToDelete.build(), apiToken);
5695+
deleteFilesResponse.then().assertThat()
5696+
.statusCode(NOT_FOUND.getStatusCode());
5697+
5698+
// Try to delete files without proper permissions
5699+
// Create a second user
5700+
Response createSecondUser = UtilIT.createRandomUser();
5701+
String unauthorizedUsername = UtilIT.getUsernameFromResponse(createSecondUser);
5702+
String unauthorizedUserApiToken = UtilIT.getApiTokenFromResponse(createSecondUser);
5703+
5704+
//Reset to a valid file id
5705+
fileIdsToDelete = Json.createArrayBuilder();
5706+
fileIdsToDelete.add(file5Id);
5707+
deleteFilesResponse = UtilIT.deleteDatasetFiles(datasetId.toString(), fileIdsToDelete.build(), unauthorizedUserApiToken);
5708+
deleteFilesResponse.then().assertThat()
5709+
.statusCode(FORBIDDEN.getStatusCode());
5710+
5711+
// Make the user a superuser to destroy dataset
5712+
Response makeSuperUserResponse = UtilIT.setSuperuserStatus(username, true);
5713+
makeSuperUserResponse.then().assertThat()
5714+
.statusCode(OK.getStatusCode());
5715+
5716+
// Clean up
5717+
Response destroyDatasetResponse = UtilIT.destroyDataset(datasetId, apiToken);
5718+
assertEquals(200, destroyDatasetResponse.getStatusCode());
5719+
5720+
Response deleteDataverseResponse = UtilIT.deleteDataverse(dataverseAlias, apiToken);
5721+
assertEquals(200, deleteDataverseResponse.getStatusCode());
5722+
5723+
Response deleteUnauthorizedUserResponse = UtilIT.deleteUser(unauthorizedUsername);
5724+
assertEquals(200, deleteUnauthorizedUserResponse.getStatusCode());
5725+
5726+
Response deleteUserResponse = UtilIT.deleteUser(username);
5727+
assertEquals(200, deleteUserResponse.getStatusCode());
5728+
}
55925729
}

src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.*;
1212
import java.util.logging.Logger;
1313
import jakarta.json.Json;
14+
import jakarta.json.JsonArray;
1415
import jakarta.json.JsonObjectBuilder;
1516
import jakarta.json.JsonArrayBuilder;
1617
import jakarta.json.JsonObject;
@@ -4570,4 +4571,13 @@ static Response deleteDataverseFeaturedItems(String dataverseAlias, String apiTo
45704571
.header(API_TOKEN_HTTP_HEADER, apiToken)
45714572
.delete("/api/dataverses/" + dataverseAlias + "/featuredItems");
45724573
}
4574+
4575+
public static Response deleteDatasetFiles(String datasetId, JsonArray fileIds, String apiToken) {
4576+
String path = String.format("/api/datasets/%s/deleteFiles", datasetId);
4577+
return given()
4578+
.header(API_TOKEN_HTTP_HEADER, apiToken)
4579+
.contentType(ContentType.JSON)
4580+
.body(fileIds.toString())
4581+
.put(path);
4582+
}
45734583
}

0 commit comments

Comments
 (0)