Skip to content
Draft
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
6 changes: 6 additions & 0 deletions doc/release-notes/12141-storage-driver-endpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Breaking Changes

All the endpoints related to Storage Drivers have been moved out of the Admin API.

- The endpoints GET, PUT AND DELETE for `/api/admin/dataverse/{alias}/storageDriver` has been moved to `/api/dataverses/{alias}/storageDriver`.
- The endpoint `/api/admin/dataverse/storageDrivers` has been moved and renamed to `/api/dataverses/{alias}/allowedStorageDrivers`. Regarding the change of the name, this endpoint will in the future only display the storageDrivers that are allowed on the specified collection, as of now, it will display the entire list of available Drivers on the installation.
12 changes: 6 additions & 6 deletions doc/sphinx-guides/source/admin/dataverses-datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,29 @@ Configure a Dataverse Collection to Store All New Files in a Specific File Store

To direct new files (uploaded when datasets are created or edited) for all datasets in a given Dataverse collection, the store can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Only accessible to superusers. ::
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph still says “Only accessible to superusers” but the new implementation in Dataverses appears to allow any authenticated user with the relevant object permission (e.g., EditDataverse) to set/reset the storage driver. Either enforce superuser-only access in the API or update this doc text to match the new authorization behavior.

Suggested change
To direct new files (uploaded when datasets are created or edited) for all datasets in a given Dataverse collection, the store can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Only accessible to superusers. ::
To direct new files (uploaded when datasets are created or edited) for all datasets in a given Dataverse collection, the store can be specified via the API as shown below, or by editing the 'General Information' for a Dataverse collection on the Dataverse collection page. Requires permission to edit the Dataverse collection (for example, the ``EditDataverse`` permission). ::

Copilot uses AI. Check for mistakes.

curl -H "X-Dataverse-key: $API_TOKEN" -X PUT -d $storageDriverLabel http://$SERVER/api/admin/dataverse/$dataverse-alias/storageDriver
curl -H "X-Dataverse-key: $API_TOKEN" -X PUT -d $storageDriverLabel http://$SERVER/api/dataverses/$dataverse-alias/storageDriver

(Note that for ``dataverse.files.store1.label=MyLabel``, you should pass ``MyLabel``.)

A store assigned directly to a collection can be seen using::

curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/$dataverse-alias/storageDriver
curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/dataverses/$dataverse-alias/storageDriver

This may be null. To get the effective storageDriver for a collection, which may be inherited from a parent collection or be the installation default, you can use::

curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/$dataverse-alias/storageDriver?getEffective=true
curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/dataverses/$dataverse-alias/storageDriver?getEffective=true

This will never be null.

(Note that for ``dataverse.files.store1.label=MyLabel``, the JSON response will include "name":"store1" and "label":"MyLabel".)

To delete a store assigned directly to a collection (so that the colllection's effective store is inherted from it's parent or is the global default), use::

curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/admin/dataverse/$dataverse-alias/storageDriver
curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE http://$SERVER/api/dataverses/$dataverse-alias/storageDriver

The available drivers can be listed with::
The available drivers within a collection can be listed with::

curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/admin/dataverse/storageDrivers
curl -H "X-Dataverse-key: $API_TOKEN" http://$SERVER/api/dataverses/$dataverse-alias/allowedStorageDrivers

(Individual datasets can be configured to use specific file stores as well. See the "Datasets" section below.)

Expand Down
90 changes: 1 addition & 89 deletions src/main/java/edu/harvard/iq/dataverse/api/Admin.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -2256,94 +2255,6 @@ public Response addRoleAssignementsToChildren(@Context ContainerRequestContext c
"InheritParentRoleAssignments does not list any roles on this instance");
}

@GET
@AuthRequired
@Path("/dataverse/{alias}/storageDriver")
public Response getStorageDriver(@Context ContainerRequestContext crc, @PathParam("alias") String alias,
@QueryParam("getEffective") Boolean getEffective) throws WrappedResponse {
Dataverse dataverse = dataverseSvc.findByAlias(alias);
if (dataverse == null) {
return error(Response.Status.NOT_FOUND, "Could not find dataverse based on alias supplied: " + alias + ".");
}
try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
if (!user.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}
} catch (WrappedResponse wr) {
return wr.getResponse();
}

if (getEffective != null && getEffective) {
return ok(JsonPrinter.jsonStorageDriver(dataverse.getEffectiveStorageDriverId()));
} else {
return ok(JsonPrinter.jsonStorageDriver(dataverse.getStorageDriverId()));
}
}

@PUT
@AuthRequired
@Path("/dataverse/{alias}/storageDriver")
public Response setStorageDriver(@Context ContainerRequestContext crc, @PathParam("alias") String alias, String label) throws WrappedResponse {
Dataverse dataverse = dataverseSvc.findByAlias(alias);
if (dataverse == null) {
return error(Response.Status.NOT_FOUND, "Could not find dataverse based on alias supplied: " + alias + ".");
}
try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
if (!user.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}
} catch (WrappedResponse wr) {
return wr.getResponse();
}
for (Entry<String, String> store: DataAccess.getStorageDriverLabels().entrySet()) {
if(store.getKey().equals(label)) {
dataverse.setStorageDriverId(store.getValue());
return ok("Storage set to: " + store.getKey() + "/" + store.getValue());
}
}
return error(Response.Status.BAD_REQUEST,
"No Storage Driver found for : " + label);
}

@DELETE
@AuthRequired
@Path("/dataverse/{alias}/storageDriver")
public Response resetStorageDriver(@Context ContainerRequestContext crc, @PathParam("alias") String alias) throws WrappedResponse {
Dataverse dataverse = dataverseSvc.findByAlias(alias);
if (dataverse == null) {
return error(Response.Status.NOT_FOUND, "Could not find dataverse based on alias supplied: " + alias + ".");
}
try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
if (!user.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}
} catch (WrappedResponse wr) {
return wr.getResponse();
}
dataverse.setStorageDriverId("");
return ok("Storage reset to default: " + DataAccess.DEFAULT_STORAGE_DRIVER_IDENTIFIER);
}

@GET
@AuthRequired
@Path("/dataverse/storageDrivers")
public Response listStorageDrivers(@Context ContainerRequestContext crc) throws WrappedResponse {
try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
if (!user.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}
} catch (WrappedResponse wr) {
return wr.getResponse();
}
JsonObjectBuilder bld = jsonObjectBuilder();
DataAccess.getStorageDriverLabels().entrySet().forEach(s -> bld.add(s.getKey(), s.getValue()));
return ok(bld);
}

@GET
@AuthRequired
@Path("/dataverse/{alias}/curationLabelSet")
Expand Down Expand Up @@ -2820,4 +2731,5 @@ public Response rateLimitStats(@Context ContainerRequestContext crc,
String csvData = cacheFactory.getStats(CacheFactoryBean.RATE_LIMIT_CACHE, deltaMinutesFilter != null ? String.valueOf(deltaMinutesFilter) : null);
return Response.ok(csvData).header("Content-Disposition", "attachment; filename=\"data.csv\"").build();
}

}
184 changes: 88 additions & 96 deletions src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@
import com.google.common.collect.Lists;
import com.google.api.client.util.ArrayMap;
import edu.harvard.iq.dataverse.*;
import static edu.harvard.iq.dataverse.api.AbstractApiBean.error;
import edu.harvard.iq.dataverse.api.auth.AuthRequired;
import edu.harvard.iq.dataverse.api.datadeposit.SwordServiceBean;
import edu.harvard.iq.dataverse.api.dto.*;
import edu.harvard.iq.dataverse.authorization.DataverseRole;

import edu.harvard.iq.dataverse.api.imports.ImportException;
import edu.harvard.iq.dataverse.api.imports.ImportServiceBean;
import edu.harvard.iq.dataverse.authorization.Permission;
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroup;
import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupProvider;
import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean;
import edu.harvard.iq.dataverse.authorization.groups.impl.ipaddress.ip.IpAddress;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.User;
import edu.harvard.iq.dataverse.dataset.DatasetType;
import edu.harvard.iq.dataverse.dataverse.DataverseUtil;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItem;
import edu.harvard.iq.dataverse.dataverse.featured.DataverseFeaturedItemServiceBean;
import edu.harvard.iq.dataverse.engine.command.DataverseRequest;
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
import edu.harvard.iq.dataverse.engine.command.impl.*;
import edu.harvard.iq.dataverse.pidproviders.PidProvider;
import edu.harvard.iq.dataverse.pidproviders.PidUtil;
Expand Down Expand Up @@ -73,6 +69,7 @@
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.StreamingOutput;

import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
Expand All @@ -89,7 +86,6 @@
public class Dataverses extends AbstractApiBean {

private static final Logger logger = Logger.getLogger(Dataverses.class.getCanonicalName());
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss");

@EJB
ExplicitGroupServiceBean explicitGroupSvc;
Expand Down Expand Up @@ -1330,97 +1326,6 @@ public Response listAssignments(@Context ContainerRequestContext crc, @PathParam
), getRequestUser(crc));
}

/**
* This code for setting a dataverse logo via API was started when initially
* investigating https://github.com/IQSS/dataverse/issues/3559 but it isn't
* finished so it's commented out. See also * "No functionality should be
* GUI-only. Make all functionality reachable via the API" at
* https://github.com/IQSS/dataverse/issues/3440
*/
// File tempDir;
//
// TODO: Code duplicate in ThemeWidgetFragment. Maybe extract, make static and put some place else?
// Important: at least use JvmSettings.DOCROOT_DIRECTORY and not the hardcoded location!
// private void createTempDir(Dataverse editDv) {
// try {
// File tempRoot = java.nio.file.Files.createDirectories(Paths.get("../docroot/logos/temp")).toFile();
// tempDir = java.nio.file.Files.createTempDirectory(tempRoot.toPath(), editDv.getId().toString()).toFile();
// } catch (IOException e) {
// throw new RuntimeException("Error creating temp directory", e); // improve error handling
// }
// }
//
// private DataverseTheme initDataverseTheme(Dataverse editDv) {
// DataverseTheme dvt = new DataverseTheme();
// dvt.setLinkColor(DEFAULT_LINK_COLOR);
// dvt.setLogoBackgroundColor(DEFAULT_LOGO_BACKGROUND_COLOR);
// dvt.setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
// dvt.setTextColor(DEFAULT_TEXT_COLOR);
// dvt.setDataverse(editDv);
// return dvt;
// }
//
// @PUT
// @Path("{identifier}/logo")
// @Consumes(MediaType.MULTIPART_FORM_DATA)
// public Response setDataverseLogo(@PathParam("identifier") String dvIdtf,
// @FormDataParam("file") InputStream fileInputStream,
// @FormDataParam("file") FormDataContentDisposition contentDispositionHeader,
// @QueryParam("key") String apiKey) {
// boolean disabled = true;
// if (disabled) {
// return error(Status.FORBIDDEN, "Setting the dataverse logo via API needs more work.");
// }
// try {
// final DataverseRequest req = createDataverseRequest(findUserOrDie());
// final Dataverse editDv = findDataverseOrDie(dvIdtf);
//
// logger.finer("entering fileUpload");
// if (tempDir == null) {
// createTempDir(editDv);
// logger.finer("created tempDir");
// }
// File uploadedFile;
// try {
// String fileName = contentDispositionHeader.getFileName();
//
// uploadedFile = new File(tempDir, fileName);
// if (!uploadedFile.exists()) {
// uploadedFile.createNewFile();
// }
// logger.finer("created file");
// File file = null;
// file = FileUtil.inputStreamToFile(fileInputStream);
// if (file.length() > systemConfig.getUploadLogoSizeLimit()) {
// return error(Response.Status.BAD_REQUEST, "File is larger than maximum size: " + systemConfig.getUploadLogoSizeLimit() + ".");
// }
// java.nio.file.Files.copy(fileInputStream, uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// logger.finer("copied inputstream to file");
// editDv.setDataverseTheme(initDataverseTheme(editDv));
// editDv.getDataverseTheme().setLogo(fileName);
//
// } catch (IOException e) {
// logger.finer("caught IOException");
// logger.throwing("ThemeWidgetFragment", "handleImageFileUpload", e);
// throw new RuntimeException("Error uploading logo file", e); // improve error handling
// }
// // If needed, set the default values for the logo
// if (editDv.getDataverseTheme().getLogoFormat() == null) {
// editDv.getDataverseTheme().setLogoFormat(DataverseTheme.ImageFormat.SQUARE);
// }
// logger.finer("end handelImageFileUpload");
// UpdateDataverseThemeCommand cmd = new UpdateDataverseThemeCommand(editDv, uploadedFile, req);
// Dataverse saved = execCommand(cmd);
//
// /**
// * @todo delete the temp file:
// * docroot/logos/temp/1148114212463761832421/cc0.png
// */
// return ok("logo uploaded: " + saved.getDataverseTheme().getLogo());
// } catch (WrappedResponse ex) {
// return error(Status.BAD_REQUEST, "problem uploading logo: " + ex);
// }
// }
@POST
@AuthRequired
@Path("{identifier}/assignments")
Expand Down Expand Up @@ -2140,4 +2045,91 @@ public Response getRoleAssignmentHistory(@Context ContainerRequestContext crc,
return getRoleAssignmentHistoryResponse(dataverse, authenticatedUser, false, headers);
}, getRequestUser(crc));
}

@GET
@AuthRequired
@Path("{identifier}/storageDriver")
public Response getStorageDriver(@Context ContainerRequestContext crc, @PathParam("identifier") String id,
@QueryParam("getEffective") Boolean getEffective) throws WrappedResponse {

Dataverse dataverse = findDataverseOrDie(id);

if (dataverse == null) {
return error(Response.Status.NOT_FOUND, "Could not find dataverse based on the identifier supplied: " + id + ".");
}

return response(req -> {
String storageDriver = execCommand(new GetDataverseStorageDriverCommand(req, findDataverseOrDie(id), getEffective));
return ok(JsonPrinter.jsonStorageDriver(storageDriver));
}, getRequestUser(crc));
}

@PUT
@AuthRequired
@Path("{identifier}/storageDriver")
public Response setStorageDriver(@Context ContainerRequestContext crc,
@PathParam("identifier") String id, String label) throws WrappedResponse {

Dataverse dataverse = findDataverseOrDie(id);

if (dataverse == null) {
return error(Response.Status.NOT_FOUND, "Could not find dataverse based on the identifier supplied: " + id + ".");
}

try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
DataverseRequest request = createDataverseRequest(user);
SetDataverseStorageDriverCommand setDriverCommand = new SetDataverseStorageDriverCommand(request, dataverse, label);
return ok(execCommand(setDriverCommand));

} catch (WrappedResponse wr) {
return handleWrappedResponse(wr);
}
}

@DELETE
@AuthRequired
@Path("{identifier}/storageDriver")
public Response resetStorageDriver(@Context ContainerRequestContext crc, @PathParam("identifier") String id) throws WrappedResponse {

Dataverse dataverse = findDataverseOrDie(id);
if (dataverse == null) {
return error(Response.Status.NOT_FOUND, "Could not find dataverse based on the identifier supplied: " + id + ".");
}

try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
DataverseRequest request = createDataverseRequest(user);
DeleteDataverseStorageDriverCommand deleteDriverCommand = new DeleteDataverseStorageDriverCommand(request, dataverse);
return ok(execCommand(deleteDriverCommand));
} catch (WrappedResponse wr) {
return handleWrappedResponse(wr);
}
}

@GET
@AuthRequired
@Path("{identifier}/allowedStorageDrivers")
public Response listStorageDrivers(@Context ContainerRequestContext crc, @PathParam("identifier") String id) throws WrappedResponse {

Dataverse dv = findDataverseOrDie(id);


/*
* TODO: This endpoint ad GetDataverseAllowedStorageDriverCommand needs to be completed,
* currently it mocks things that will be required to model the behavior requested by Jim Myers,
* which is to return the list of storage drivers that the dataverse can use.
* Currently it will return the full list of drivers available.
*/
try {
AuthenticatedUser user = getRequestAuthenticatedUserOrDie(crc);
DataverseRequest request = createDataverseRequest(user);
GetDataverseAllowedStorageDriverCommand getAllowedStorageDriversCommand = new GetDataverseAllowedStorageDriverCommand(request, dv);
return ok(execCommand(getAllowedStorageDriversCommand));
} catch (WrappedResponse wr) {
return handleWrappedResponse(wr);
}
}

}

Loading