Skip to content

Commit ff42ebd

Browse files
authored
Merge pull request #11271 from GlobalDataverseCommunityConsortium/DANS-multifile-edit
DANS - Multifile edit
2 parents 768fb03 + 00d11a5 commit ff42ebd

File tree

5 files changed

+461
-3
lines changed

5 files changed

+461
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Update File Metadata API (PR #11271)
2+
3+
A new API endpoint has been added to allow updating file metadata for one or more files in a dataset.
4+
5+
See the [Native API documentation](https://guides.dataverse.org/en/latest/api/native-api.html) for details on usage.

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2597,6 +2597,53 @@ The fully expanded example above (without environment variables) looks like this
25972597
25982598
curl -H "X-Dataverse-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/datasets/:persistentId/add?persistentId=doi:10.5072/FK2/J8SJZB" -F 'jsonData={"description":"A remote image.","storageIdentifier":"trsa://themes/custom/qdr/images/CoreTrustSeal-logo-transparent.png","checksumType":"MD5","md5Hash":"509ef88afa907eaf2c17c1c8d8fde77e","label":"testlogo.png","fileName":"testlogo.png","mimeType":"image/png"}'
25992599
2600+
Update File Metadata
2601+
~~~~~~~~~~~~~~~~~~~~
2602+
2603+
Updates metadata for one or more files in a dataset. This API call allows you to modify file-level metadata without the need to replace the actual file content.
2604+
2605+
.. code-block:: bash
2606+
2607+
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
2608+
export SERVER_URL=https://demo.dataverse.org
2609+
export PERSISTENT_ID=doi:10.5072/FK2/J8SJZB
2610+
2611+
curl -H "X-Dataverse-key:$API_TOKEN" -X POST "$SERVER_URL/api/datasets/:persistentId/files/metadata?persistentId=$PERSISTENT_ID" --upload-file file-metadata-update.json
2612+
2613+
The fully expanded example above (without environment variables) looks like this:
2614+
2615+
.. code-block:: bash
2616+
2617+
curl -H "X-Dataverse-key:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X POST "https://demo.dataverse.org/api/datasets/:persistentId/files/metadata?:persistentId=doi:10.5072/FK2/J8SJZB" --upload-file file-metadata-update.json
2618+
2619+
The ``file-metadata-update.json`` file should contain a JSON array of objects, each representing a file to be updated. Here's an example structure:
2620+
2621+
.. code-block:: json
2622+
2623+
[
2624+
{
2625+
"dataFileId": 42,
2626+
"label": "Updated File Name",
2627+
"directoryLabel": "data/",
2628+
"description": "Updated file description",
2629+
"restricted": false,
2630+
"categories": ["Documentation", "Data"],
2631+
"provFreeForm": "Updated provenance information"
2632+
},
2633+
{
2634+
"dataFileId": 43,
2635+
"label": "Another Updated File",
2636+
"description": "Another updated description",
2637+
"restricted": true
2638+
}
2639+
]
2640+
2641+
Each object in the array must include the ``dataFileId`` field to identify the file. Other fields are optional and will only be updated if included.
2642+
2643+
The API will return a JSON object with information about the update operation, including any errors that occurred during the process.
2644+
2645+
Note: This API call requires appropriate permissions to edit the dataset and its files.
2646+
26002647
.. _cleanup-storage-api:
26012648

26022649
Cleanup Storage of a Dataset

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

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import edu.harvard.iq.dataverse.globus.GlobusServiceBean;
3737
import edu.harvard.iq.dataverse.globus.GlobusUtil;
3838
import edu.harvard.iq.dataverse.ingest.IngestServiceBean;
39+
import edu.harvard.iq.dataverse.ingest.IngestUtil;
3940
import edu.harvard.iq.dataverse.makedatacount.*;
4041
import edu.harvard.iq.dataverse.makedatacount.MakeDataCountLoggingServiceBean.MakeDataCountEntry;
4142
import edu.harvard.iq.dataverse.metrics.MetricsUtil;
@@ -95,7 +96,6 @@
9596
import java.util.logging.Logger;
9697
import java.util.regex.Pattern;
9798
import java.util.stream.Collectors;
98-
9999
import static edu.harvard.iq.dataverse.api.ApiConstants.*;
100100
import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException;
101101

@@ -4614,6 +4614,152 @@ public Response replaceFilesInDataset(@Context ContainerRequestContext crc,
46144614

46154615
}
46164616

4617+
@POST
4618+
@AuthRequired
4619+
@Path("{id}/files/metadata")
4620+
@Consumes(MediaType.APPLICATION_JSON)
4621+
public Response updateMultipleFileMetadata(@Context ContainerRequestContext crc, String jsonData,
4622+
@PathParam("id") String datasetId) {
4623+
try {
4624+
DataverseRequest req = createDataverseRequest(getRequestUser(crc));
4625+
Dataset dataset = findDatasetOrDie(datasetId);
4626+
User authUser = getRequestUser(crc);
4627+
4628+
// Verify that the user has EditDataset permission
4629+
if (!permissionSvc.requestOn(createDataverseRequest(authUser), dataset).has(Permission.EditDataset)) {
4630+
return error(Response.Status.FORBIDDEN, "You do not have permission to edit this dataset.");
4631+
}
4632+
4633+
// Parse the JSON array
4634+
JsonArray jsonArray = JsonUtil.getJsonArray(jsonData);
4635+
4636+
// Get the latest version of the dataset
4637+
DatasetVersion latestVersion = dataset.getLatestVersion();
4638+
List<FileMetadata> currentFileMetadatas = latestVersion.getFileMetadatas();
4639+
4640+
// Quick checks to verify all file ids in the JSON array are valid
4641+
Set<Long> validFileIds = currentFileMetadatas.stream().map(fm -> fm.getDataFile().getId())
4642+
.collect(Collectors.toSet());
4643+
4644+
// Extract all file IDs from the JSON array
4645+
Set<Long> jsonFileIds = jsonArray.stream().map(JsonValue::asJsonObject).map(jsonObj -> {
4646+
try {
4647+
return jsonObj.getJsonNumber("dataFileId").longValueExact();
4648+
} catch (NumberFormatException e) {
4649+
return null;
4650+
}
4651+
}).collect(Collectors.toSet());
4652+
4653+
if (jsonFileIds.size() != jsonArray.size()) {
4654+
return error(BAD_REQUEST, "One or more invalid dataFileId values were provided");
4655+
}
4656+
4657+
// Check if all JSON file IDs are valid
4658+
if (!validFileIds.containsAll(jsonFileIds)) {
4659+
Set<Long> invalidIds = new HashSet<>(jsonFileIds);
4660+
invalidIds.removeAll(validFileIds);
4661+
return error(BAD_REQUEST,
4662+
"The following files are not part of the current version of the Dataset. dataFileIds: " + invalidIds);
4663+
}
4664+
4665+
// Create editable fileMetadata if needed
4666+
if (!latestVersion.isDraft()) {
4667+
latestVersion = dataset.getOrCreateEditVersion();
4668+
currentFileMetadatas = latestVersion.getFileMetadatas();
4669+
}
4670+
4671+
// Create a map of fileId to FileMetadata for quick lookup
4672+
Map<Long, FileMetadata> fileMetadataMap = currentFileMetadatas.stream()
4673+
.collect(Collectors.toMap(fm -> fm.getDataFile().getId(), fm -> fm));
4674+
4675+
boolean publicInstall = settingsSvc.isTrueForKey(SettingsServiceBean.Key.PublicInstall, false);
4676+
4677+
int filesUpdated = 0;
4678+
for (JsonValue jsonValue : jsonArray) {
4679+
JsonObject jsonObj = jsonValue.asJsonObject();
4680+
Long fileId = jsonObj.getJsonNumber("dataFileId").longValueExact();
4681+
4682+
FileMetadata fmd = fileMetadataMap.get(fileId);
4683+
4684+
if (fmd == null) {
4685+
return error(BAD_REQUEST,
4686+
"File with dataFileId " + fileId + " is not part of the current version of the Dataset.");
4687+
}
4688+
4689+
// Handle restriction
4690+
if (jsonObj.containsKey("restrict")) {
4691+
boolean restrict = jsonObj.getBoolean("restrict");
4692+
if (restrict != fmd.isRestricted()) {
4693+
if (publicInstall && restrict) {
4694+
return error(BAD_REQUEST, "Restricting files is not permitted on a public installation.");
4695+
}
4696+
fmd.setRestricted(restrict);
4697+
if (!fmd.getDataFile().isReleased()) {
4698+
fmd.getDataFile().setRestricted(restrict);
4699+
}
4700+
4701+
} else {
4702+
// This file is already restricted or already unrestricted
4703+
String text = restrict ? "restricted" : "unrestricted";
4704+
return error(BAD_REQUEST, "File (dataFileId:" + fileId + ") is already " + text);
4705+
}
4706+
}
4707+
4708+
// Load optional params
4709+
OptionalFileParams optionalFileParams = new OptionalFileParams(jsonObj.toString());
4710+
4711+
// Check for filename conflicts
4712+
String incomingLabel = null;
4713+
if (jsonObj.containsKey("label")) {
4714+
incomingLabel = jsonObj.getString("label");
4715+
}
4716+
String incomingDirectoryLabel = null;
4717+
if (jsonObj.containsKey("directoryLabel")) {
4718+
incomingDirectoryLabel = jsonObj.getString("directoryLabel");
4719+
}
4720+
String existingLabel = fmd.getLabel();
4721+
String existingDirectoryLabel = fmd.getDirectoryLabel();
4722+
String pathPlusFilename = IngestUtil.getPathAndFileNameToCheck(incomingLabel, incomingDirectoryLabel,
4723+
existingLabel, existingDirectoryLabel);
4724+
4725+
// Create a copy of the fileMetadataMap without the current fmd
4726+
Map<Long, FileMetadata> fileMetadataMapCopy = new HashMap<>(fileMetadataMap);
4727+
fileMetadataMapCopy.remove(fileId);
4728+
4729+
List<FileMetadata> fmdListMinusCurrentFile = new ArrayList<>(fileMetadataMapCopy.values());
4730+
4731+
if (IngestUtil.conflictsWithExistingFilenames(pathPlusFilename, fmdListMinusCurrentFile)) {
4732+
return error(BAD_REQUEST, BundleUtil.getStringFromBundle("files.api.metadata.update.duplicateFile",
4733+
Arrays.asList(pathPlusFilename)));
4734+
}
4735+
4736+
// Apply optional params
4737+
optionalFileParams.addOptionalParams(fmd);
4738+
4739+
// Store updated FileMetadata
4740+
fileMetadataMap.put(fileId, fmd);
4741+
filesUpdated++;
4742+
}
4743+
4744+
latestVersion.setFileMetadatas(new ArrayList<>(fileMetadataMap.values()));
4745+
// Update the dataset version with all changes
4746+
UpdateDatasetVersionCommand updateCmd = new UpdateDatasetVersionCommand(dataset, req);
4747+
dataset = execCommand(updateCmd);
4748+
4749+
return ok("File metadata updates have been completed for " + filesUpdated + " files.");
4750+
} catch (WrappedResponse wr) {
4751+
return error(BAD_REQUEST,
4752+
"An error has occurred attempting to update the requested DataFiles, likely related to permissions.");
4753+
} catch (JsonException ex) {
4754+
logger.log(Level.WARNING, "Dataset metadata update: exception while parsing JSON: {0}", ex);
4755+
return error(BAD_REQUEST, BundleUtil.getStringFromBundle("file.addreplace.error.parsing"));
4756+
} catch (DataFileTagException de) {
4757+
return error(BAD_REQUEST, de.getMessage());
4758+
}catch (Exception e) {
4759+
logger.log(Level.WARNING, "Dataset metadata update: exception while processing:{0}", e);
4760+
return error(Response.Status.INTERNAL_SERVER_ERROR, "Error updating metadata for DataFiles: " + e);
4761+
}
4762+
}
46174763
/**
46184764
* API to find curation assignments and statuses
46194765
*

0 commit comments

Comments
 (0)