diff --git a/common/source/java/ch/systemsx/cisd/common/servlet/HttpServletRequestUtils.java b/common/source/java/ch/systemsx/cisd/common/servlet/HttpServletRequestUtils.java index 9952b44a9667e7c86f23c360a0004908f2361ff4..01e075b8665d23b7d529d3d2b6e6dc5b0acba052 100644 --- a/common/source/java/ch/systemsx/cisd/common/servlet/HttpServletRequestUtils.java +++ b/common/source/java/ch/systemsx/cisd/common/servlet/HttpServletRequestUtils.java @@ -88,4 +88,29 @@ public class HttpServletRequestUtils } } } + + /** + * Returns a value of the specified request parameter as Boolean. If the value is null or the trimmed value is empty then returns False. + * + * @throws IllegalArgumentException when the parameter value is not a valid boolean + */ + public static final Boolean getBooleanParameter(HttpServletRequest request, String parameterName) + { + String parameterValue = getStringParameter(request, parameterName); + + if (parameterValue == null) + { + return Boolean.FALSE; + } else + { + try + { + return Boolean.valueOf(parameterValue); + } catch (NumberFormatException e) + { + throw new IllegalArgumentException("Parameter: " + parameterName + + " is not a valid boolean: " + parameterValue); + } + } + } } diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java index 487fc3f94bee1646df2c6e651aae10763447d5d8..0f16735ec5edcc4998eb59e0d755db339480562f 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/SessionWorkspaceFileUploadServlet.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.openbis.dss.generic.server; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; @@ -63,10 +64,7 @@ public class SessionWorkspaceFileUploadServlet extends HttpServlet private static final String STATUS_PARAM = "status"; - /** If present, the file being uploaded is just an empty folder, not a file upload. */ - private static final String IS_EMPTY_FOLDER = "emptyFolder"; - - private static final String UPLOAD_ID_PARAM = "uploadID"; + private static final String IS_EMPTY_FOLDER = "emptyFolder"; // If present, is just an empty folder, not a file upload private IDssServiceRpcGeneric service; @@ -89,22 +87,29 @@ public class SessionWorkspaceFileUploadServlet extends HttpServlet uploadRequest.validate(); - final String uploadId = uploadRequest.getUploadId(); - final String filePath = (uploadId != null) - ? uploadId + "/" + uploadRequest.getFileName() - : uploadRequest.getFileName(); - long bytes = - service.putFileSliceToSessionWorkspace(uploadRequest.getSessionId(), - filePath, uploadRequest.getStartByte(), - uploadRequest.getFile()); + long bytes = -1; + boolean isOk = false; + if (uploadRequest.isEmptyFolder()) + { + File folder = service.putDirToSessionWorkspace(uploadRequest.getSessionId(), + uploadRequest.getFileName(), true); + isOk = folder.exists(); + } else + { + bytes = service.putFileSliceToSessionWorkspace(uploadRequest.getSessionId(), + uploadRequest.getFileName(), uploadRequest.getStartByte(), + uploadRequest.getFile()); + isOk = bytes >= 0; + } Map<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put(ID_PARAM, uploadRequest.getId()); resultMap.put(START_BYTE_PARAM, uploadRequest.getStartByte()); resultMap.put(END_BYTE_PARAM, uploadRequest.getEndByte()); resultMap.put(FILE_NAME_PARAM, uploadRequest.getFileName()); + resultMap.put(IS_EMPTY_FOLDER, uploadRequest.isEmptyFolder()); resultMap.put(SIZE_PARAM, bytes); - resultMap.put(STATUS_PARAM, "ok"); + resultMap.put(STATUS_PARAM, (isOk)?"ok":"error"); SessionWorkspaceFileUploadResponse uploadResponse = new SessionWorkspaceFileUploadResponse(response); @@ -112,10 +117,10 @@ public class SessionWorkspaceFileUploadServlet extends HttpServlet uploadResponse.writeJson(resultMap); } - private static class SessionWorkspaceFileUploadRequest + private class SessionWorkspaceFileUploadRequest { - private final HttpServletRequest request; + private HttpServletRequest request; public SessionWorkspaceFileUploadRequest(HttpServletRequest request) { @@ -149,9 +154,9 @@ public class SessionWorkspaceFileUploadServlet extends HttpServlet return HttpServletRequestUtils.getStringParameter(request, FILE_NAME_PARAM); } - public String getUploadId() + public Boolean isEmptyFolder() { - return HttpServletRequestUtils.getStringParameter(request, UPLOAD_ID_PARAM); + return HttpServletRequestUtils.getBooleanParameter(request, IS_EMPTY_FOLDER); } public InputStream getFile() throws IOException @@ -186,10 +191,10 @@ public class SessionWorkspaceFileUploadServlet extends HttpServlet } - private static class SessionWorkspaceFileUploadResponse + private class SessionWorkspaceFileUploadResponse { - private final HttpServletResponse response; + private HttpServletResponse response; public SessionWorkspaceFileUploadResponse(HttpServletResponse response) { diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGeneric.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGeneric.java index 52a57a244c3fb737a30bc18e91e55fc3a09ba2e7..73751b6d1f6f84481187d3e6e4d1e595d45b44f9 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGeneric.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGeneric.java @@ -307,8 +307,7 @@ public class DssServiceRpcGeneric extends AbstractDssServiceRpc<IDssServiceRpcGe } @Override - public long putFileSliceToSessionWorkspace(String sessionToken, String filePath, - long slicePosition, InputStream sliceInputStream) throws IOExceptionUnchecked + public File putDirToSessionWorkspace(String sessionToken, String filePath, boolean isEmptyDirectory) throws IOExceptionUnchecked { getOpenBISService().checkSession(sessionToken); if (filePath.contains("../")) @@ -323,7 +322,17 @@ public class DssServiceRpcGeneric extends AbstractDssServiceRpc<IDssServiceRpcGe final File dir = new File(workspaceDir, subDir); dir.mkdirs(); final File file = new File(dir, filename); + if (isEmptyDirectory) { + file.mkdirs(); + } + return file; + } + @Override + public long putFileSliceToSessionWorkspace(String sessionToken, String filePath, + long slicePosition, InputStream sliceInputStream) throws IOExceptionUnchecked + { + final File file = putDirToSessionWorkspace(sessionToken, filePath, false); return FileUtilities.writeToFile(file, slicePosition, sliceInputStream); } diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGenericLogger.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGenericLogger.java index a67231e4a7518516a4856d98cef0e09d9caa7011..26c10fa231d89c46e2cd24f994ef07e27a7c516a 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGenericLogger.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/api/v1/DssServiceRpcGenericLogger.java @@ -133,6 +133,12 @@ public class DssServiceRpcGenericLogger extends AbstractServerLogger implements return 0; } + @Override + public File putDirToSessionWorkspace(String sessionToken, String filePath, boolean isEmptyDirectory) throws IOExceptionUnchecked { + logTracking(sessionToken, "putDirToSessionWorkspace", "FILE_PATH(%s) EMPTY_DIRECTORY(%s)", filePath, isEmptyDirectory); + return null; + } + @Override public long putFileSliceToSessionWorkspace(String sessionToken, String filePath, long slicePosition, InputStream sliceInputStream) throws IOExceptionUnchecked diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/api/v1/IDssServiceRpcGeneric.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/api/v1/IDssServiceRpcGeneric.java index d6b5c32180be95ab4750a3c71f850febd456a9fa..1a01d007ca28fa365c495fb18e4597a67b7463b4 100644 --- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/api/v1/IDssServiceRpcGeneric.java +++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/shared/api/v1/IDssServiceRpcGeneric.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.openbis.dss.generic.shared.api.v1; +import java.io.File; import java.io.InputStream; import java.util.List; import java.util.Map; @@ -198,6 +199,18 @@ public interface IDssServiceRpcGeneric extends IRpcService public long putFileToSessionWorkspace(String sessionToken, String filePath, InputStream inputStream) throws IOExceptionUnchecked; + /** + * Upload a file slice to the user's session workspace. If the file does not exist then it will created. + * + * @param sessionToken The session token. + * @param filePath The file path (including the sub-directory) to upload the slice to. + * @param isEmptyDirectory Creates the file as an empty directory if true, + * returns the File object without creating it if false. + * @return File object representing the given path + * @throws IOExceptionUnchecked Thrown if IOException occurs. + */ + public File putDirToSessionWorkspace(String sessionToken, String filePath, boolean isEmptyDirectory) throws IOExceptionUnchecked; + /** * Upload a file slice to the user's session workspace. If the file does not exist then it will created. * diff --git a/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java b/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java index 6e9d50baf699a7b4a005a8bdba170fb8773d37b8..e9a7c8798c3f7b0dc1d0f65f24ae34965993d53d 100644 --- a/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java +++ b/openbis_api/source/java/ch/ethz/sis/openbis/generic/OpenBISAPI.java @@ -2,8 +2,12 @@ package ch.ethz.sis.openbis.generic; import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi; import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.EntityKind; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId; +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier; import ch.ethz.sis.openbis.generic.dssapi.v3.IDataStoreServerApi; import ch.ethz.sis.openbis.generic.dssapi.v3.dto.dataset.create.UploadedDataSetCreation; +import ch.systemsx.cisd.common.exceptions.UserFailureException; import ch.systemsx.cisd.common.http.JettyHttpClientFactory; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogFactory; @@ -16,13 +20,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -42,7 +40,7 @@ public class OpenBISAPI { private static final int CHUNK_SIZE = 1048576; // 1 MiB - private static final Collection<Integer> ACCEPTABLE_STATUSES = List.of(200, 502); + private static final Collection<Integer> ACCEPTABLE_STATUSES = List.of(200); private final IApplicationServerApi asFacade; @@ -90,155 +88,132 @@ public class OpenBISAPI { asFacade.logout(sessionToken); } - private List<File> contentOf(final File item) + public DataSetPermId createUploadedDataSet(final UploadedDataSetCreation newDataSet) { - if (item.getName().startsWith(".")) - { - return Collections.emptyList(); - } else if (item.isFile()) - { - return Collections.singletonList(item); - } else - { - return Arrays.stream(Objects.requireNonNull(item.listFiles())) - .flatMap(file -> contentOf(file).stream()) - .collect(Collectors.toList()); - } + return dssFacade.createUploadedDataSet(sessionToken, newDataSet); } - @SuppressWarnings("resource") + + private Iterable<byte[]> streamFile(final File file, final int chunkSize) throws FileNotFoundException { final InputStream inputStream = new FileInputStream(file); - return () -> new Iterator<>() - { - public boolean hasMore = true; - - public boolean hasNext() - { - return hasMore; - } + return new Iterable<byte[]>() { + @Override + public Iterator<byte[]> iterator() { + return new Iterator<>() { + public boolean hasMore = true; - public byte[] next() - { - try - { - byte[] bytes = inputStream.readNBytes(chunkSize); - if (bytes.length < chunkSize) - { - hasMore = false; - inputStream.close(); + public boolean hasNext() { + return hasMore; } - return bytes; - } catch (final IOException e) - { - try - { - inputStream.close(); - } catch (final IOException ex) - { - throw new RuntimeException(ex); + + public byte[] next() { + try { + byte[] bytes = inputStream.readNBytes(chunkSize); + if (bytes.length < chunkSize) { + hasMore = false; + inputStream.close(); + } + return bytes; + } catch (final IOException e) { + try { + inputStream.close(); + } catch (final IOException ex) { + throw new RuntimeException(ex); + } + throw new RuntimeException(e); + } } - throw new RuntimeException(e); - } + }; } }; + } + + private String uploadFileWorkspaceDSSEmptyDir(String pathToDir) { + final org.eclipse.jetty.client.HttpClient client = JettyHttpClientFactory.getHttpClient(); + final Request httpRequest = client.newRequest(dssURL + "/session_workspace_file_upload") + .method(HttpMethod.POST); + httpRequest.param("sessionID", sessionToken); + httpRequest.param("id", "1"); + httpRequest.param("filename", pathToDir); + httpRequest.param("startByte", Long.toString(0)); + httpRequest.param("endByte", Long.toString(0)); + httpRequest.param("size", Long.toString(0)); + httpRequest.param("emptyFolder", Boolean.TRUE.toString()); + + try { + final ContentResponse response = httpRequest.send(); + final int status = response.getStatus(); + if (status != 200) + { + throw new IOException(response.getContentAsString()); + } + } catch (final IOException | TimeoutException | InterruptedException | ExecutionException e) + { + throw new RuntimeException(e); + } + return pathToDir; + } + private String uploadFileWorkspaceDSSFile(String pathToFile, File file) { + try { + long start = 0; + for (byte[] chunk : streamFile(file, CHUNK_SIZE)) { + final long end = start + chunk.length; + + final org.eclipse.jetty.client.HttpClient client = JettyHttpClientFactory.getHttpClient(); + final Request httpRequest = client.newRequest(dssURL + "/session_workspace_file_upload") + .method(HttpMethod.POST); + httpRequest.param("sessionID", sessionToken); + httpRequest.param("id", "1"); + httpRequest.param("filename", pathToFile); + httpRequest.param("startByte", Long.toString(start)); + httpRequest.param("endByte", Long.toString(end)); + httpRequest.param("size", Long.toString(file.length())); + + final ContentResponse response = httpRequest.send(); + final int status = response.getStatus(); + if (status != 200) { + throw new IOException(response.getContentAsString()); + } + } + } catch (final IOException | TimeoutException | InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + return pathToFile; } /** * Upload file or folder to the DSS SessionWorkspaceFileUploadServlet and return the ID to be used by createUploadedDataSet * This method hides the complexities of uploading a folder with many files and does the uploads in chunks. */ - public String uploadFileWorkspaceDSS(final Path fileOrFolder) + public String uploadFileWorkspaceDSS(final File fileOrFolder, final String... parents) { - // final ServiceFinder serviceFinder = new ServiceFinder("openbis", "session_workspace_file_upload"); - // serviceFinder.computeServerUrl() - - Objects.requireNonNull(sessionToken); + System.out.println("fileOrFolder: " + fileOrFolder); - final List<File> content = contentOf(fileOrFolder.toFile()); - if (content.isEmpty()) + if (fileOrFolder.exists() == false) { - throw new RuntimeException("The directory " + fileOrFolder + " is empty"); + throw new UserFailureException("Path doesn't exist: " + fileOrFolder); } - - final long totalSize = content.stream().reduce(0L, (size, f) -> + String fileNameOrFolderName = ""; + if (parents.length == 1) { - try - { - return size + Files.size(f.toPath()); - } catch (IOException e) - { - throw new RuntimeException(e); - } - }, Long::sum); + fileNameOrFolderName = parents[0] + "/"; + } + fileNameOrFolderName += fileOrFolder.getName(); - final String uploadId = UUID.randomUUID().toString(); - final org.eclipse.jetty.client.HttpClient client = JettyHttpClientFactory.getHttpClient(); - int id = 1; - try + if (fileOrFolder.isDirectory()) { - for (final File file : content) + uploadFileWorkspaceDSSEmptyDir(fileNameOrFolderName); + for (File file:fileOrFolder.listFiles()) { - final String fileName = file.getName(); - final long fileSize = file.length(); - final String prefix = "(" + id + "/" + content.size() + ") " + fileName + ": "; - final long size = Files.size(file.toPath()); - final long totalChunks = (size / CHUNK_SIZE) + (size % CHUNK_SIZE == 0 ? 0 : 1); - - OPERATION_LOG.info(prefix + "Starting upload of " + size + " bytes"); - - long start = 0; - for (var chunk : streamFile(file, CHUNK_SIZE)) - { - if (chunk.length == 0) - { - continue; - } - final long end = start + chunk.length; - - int status = 0; - while (status != 200) - { - final ContentProvider contentProvider = new BytesContentProvider(chunk); - - final Request httpRequest = client.newRequest(dssURL + "/session_workspace_file_upload") - .method(HttpMethod.POST); - httpRequest.param("sessionID", sessionToken); - httpRequest.param("id", Integer.toString(id++)); - httpRequest.param("filename", fileName); - httpRequest.param("startByte", Long.toString(start)); - httpRequest.param("endByte", Long.toString(end)); - httpRequest.param("size", Long.toString(fileSize)); - httpRequest.param("uploadID", uploadId); - httpRequest.content(contentProvider); - final ContentResponse response = httpRequest.send(); - - status = response.getStatus(); - OPERATION_LOG.info(prefix + "Chunk " + (start / CHUNK_SIZE + 1) + "/" + totalChunks - + " uploaded with status " + status); - - if (!ACCEPTABLE_STATUSES.contains(status)) - { - throw new IOException(response.getContentAsString()); - } - } - start += CHUNK_SIZE; - } - OPERATION_LOG.info(prefix + "Upload complete"); + uploadFileWorkspaceDSS(file, fileNameOrFolderName); } - } catch (final IOException | TimeoutException | InterruptedException | ExecutionException e) - { - throw new RuntimeException(e); + } else { + uploadFileWorkspaceDSSFile(fileNameOrFolderName, fileOrFolder); } - - return uploadId; - } - - public DataSetPermId createUploadedDataSet(final UploadedDataSetCreation newDataSet) - { - return dssFacade.createUploadedDataSet(sessionToken, newDataSet); + return fileNameOrFolderName; } }