From ec17ad7808251fb8c8516961009cd09962e2f4cc Mon Sep 17 00:00:00 2001 From: Stephen Salinas Date: Wed, 26 Sep 2018 08:49:56 -0400 Subject: [PATCH 1/4] Add endpoint to open files in browser from the mesos download endpoint --- .../singularity/resources/TaskResource.java | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java b/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java index 84495331ac..a2e9aa84e8 100644 --- a/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java +++ b/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.concurrent.ExecutionException; +import javax.activation.MimetypesFileTypeMap; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -26,6 +27,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; @@ -121,6 +123,7 @@ public class TaskResource extends AbstractLeaderAwareResource { private final SingularityValidator validator; private final DisasterManager disasterManager; private final RequestHelper requestHelper; + private final MimetypesFileTypeMap fileTypeMap; @Inject public TaskResource(TaskRequestManager taskRequestManager, TaskManager taskManager, SlaveManager slaveManager, MesosClient mesosClient, SingularityTaskMetadataConfiguration taskMetadataConfiguration, @@ -139,6 +142,7 @@ public TaskResource(TaskRequestManager taskRequestManager, TaskManager taskManag this.requestHelper = requestHelper; this.httpClient = httpClient; this.configuration = configuration; + this.fileTypeMap = new MimetypesFileTypeMap(); } @GET @@ -636,11 +640,26 @@ public Response downloadFileOverProxy( @Parameter(required = true, description = "Mesos slave hostname") @QueryParam("slaveHostname") String slaveHostname, @Parameter(required = true, description = "Full file path to file on Mesos slave to be downloaded") @QueryParam("path") String fileFullPath ) { + return getFile(slaveHostname, fileFullPath, true); + } + + @GET + @Path("/open/") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + @Operation(summary = "Open a file from a Mesos Slave through Singularity") + public Response openFileOverProxy( + @Parameter(required = true, description = "Mesos slave hostname") @QueryParam("slaveHostname") String slaveHostname, + @Parameter(required = true, description = "Full file path to file on Mesos slave to be downloaded") @QueryParam("path") String fileFullPath + ) { + return getFile(slaveHostname, fileFullPath, false); + } + + private Response getFile(String slaveHostname, String fileFullPath, boolean download) { String httpPrefix = configuration.getSlaveHttpsPort().isPresent() ? "https" : "http"; int httpPort = configuration.getSlaveHttpsPort().isPresent() ? configuration.getSlaveHttpsPort().get() : configuration.getSlaveHttpPort(); String url = String.format("%s://%s:%s/files/download.json", - httpPrefix, slaveHostname, httpPort); + httpPrefix, slaveHostname, httpPort); try { PerRequestConfig unlimitedTimeout = new PerRequestConfig(); @@ -657,8 +676,17 @@ public Response downloadFileOverProxy( java.nio.file.Path filePath = Paths.get(fileFullPath).getFileName(); String fileName = filePath != null ? filePath.toString() : fileFullPath; - final String headerValue = String.format("attachment; filename=\"%s\"", fileName); - return Response.ok(streamingOutputNingHandler).header("Content-Disposition", headerValue).build(); + ResponseBuilder responseBuilder = Response.ok(streamingOutputNingHandler); + + if (download) { + final String headerValue = String.format("attachment; filename=\"%s\"", fileName); + responseBuilder.header("Content-Disposition", headerValue); + } else { + // Guess type based on extension since we don't have the file locally to check content + final String maybeContentType = fileTypeMap.getContentType(fileFullPath); + responseBuilder.header("Content-Type", maybeContentType); + } + return responseBuilder.build(); } catch (Exception e) { if (e.getCause().getClass() == ConnectException.class) { throw new SlaveNotFoundException(e); @@ -666,7 +694,6 @@ public Response downloadFileOverProxy( throw new RuntimeException(e); } } - } private static class NingOutputToJaxRsStreamingOutputWrapper implements AsyncHandler, StreamingOutput { From dd2793cd03aaea1a4828d5ba0d575b78f432d2cd Mon Sep 17 00:00:00 2001 From: Stephen Salinas Date: Wed, 26 Sep 2018 09:19:26 -0400 Subject: [PATCH 2/4] fix produces annotation --- .../java/com/hubspot/singularity/resources/TaskResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java b/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java index a2e9aa84e8..16c3c219bc 100644 --- a/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java +++ b/SingularityService/src/main/java/com/hubspot/singularity/resources/TaskResource.java @@ -645,7 +645,7 @@ public Response downloadFileOverProxy( @GET @Path("/open/") - @Produces(MediaType.APPLICATION_OCTET_STREAM) + @Produces("*/*") @Operation(summary = "Open a file from a Mesos Slave through Singularity") public Response openFileOverProxy( @Parameter(required = true, description = "Mesos slave hostname") @QueryParam("slaveHostname") String slaveHostname, From c3dbf4effe707e01cdcde77046d1018223e2862d Mon Sep 17 00:00:00 2001 From: Stephen Salinas Date: Wed, 26 Sep 2018 09:27:18 -0400 Subject: [PATCH 3/4] missing deps --- SingularityService/pom.xml | 5 +++++ pom.xml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/SingularityService/pom.xml b/SingularityService/pom.xml index 156d434a04..e693155701 100644 --- a/SingularityService/pom.xml +++ b/SingularityService/pom.xml @@ -415,6 +415,11 @@ runtime + + javax.activation + activation + + org.slf4j diff --git a/pom.xml b/pom.xml index b82bfaedb9..c1f347cbc6 100644 --- a/pom.xml +++ b/pom.xml @@ -377,6 +377,12 @@ 1.18 + + javax.activation + activation + 1.1 + + com.jayway.awaitility awaitility From 0240e419f753b791d364edf9a08734f12d675292 Mon Sep 17 00:00:00 2001 From: Stephen Salinas Date: Wed, 26 Sep 2018 10:07:31 -0400 Subject: [PATCH 4/4] Support opening certain file types from the ui file browser --- .../app/components/taskDetail/TaskDetail.jsx | 4 +++ .../components/taskDetail/TaskFileBrowser.jsx | 29 ++++++++++++++----- SingularityUI/app/utils.es6 | 2 ++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/SingularityUI/app/components/taskDetail/TaskDetail.jsx b/SingularityUI/app/components/taskDetail/TaskDetail.jsx index fad4994ece..dc0283e48d 100644 --- a/SingularityUI/app/components/taskDetail/TaskDetail.jsx +++ b/SingularityUI/app/components/taskDetail/TaskDetail.jsx @@ -180,6 +180,10 @@ class TaskDetail extends Component { file.fullPath = `${files.fullPathToRoot}/${files.currentDirectory}/${file.name}`; file.downloadLink = `${config.apiRoot}/tasks/download?slaveHostname=${files.slaveHostname}&path=${file.fullPath}`; + const extensionMatcher = /(?:\.([^.]+))?$/; + if (!file.isDirectory && Utils.OPENABLE_EXTENSIONS.includes(extensionMatcher.exec(file.name)[1])) { + file.openLink = `${config.apiRoot}/tasks/open?slaveHostname=${files.slaveHostname}&path=${file.fullPath}`; + } file.isRecentlyModified = Date.now() / 1000 - file.mtime <= RECENTLY_MODIFIED_SECONDS; if (!file.isDirectory) { diff --git a/SingularityUI/app/components/taskDetail/TaskFileBrowser.jsx b/SingularityUI/app/components/taskDetail/TaskFileBrowser.jsx index 6b4c617595..80b0fc66ef 100644 --- a/SingularityUI/app/components/taskDetail/TaskFileBrowser.jsx +++ b/SingularityUI/app/components/taskDetail/TaskFileBrowser.jsx @@ -129,13 +129,28 @@ function TaskFileBrowser (props) { id="actions-column" key="actions-column" className="actions-column" - cellData={(file) => !file.isDirectory && ( - Download {file.name}}> - - - - - )} + cellData={(file) => { + const download = !file.isDirectory && ( + Download {file.name}}> + + + + + ); + const open = !file.isDirectory && file.openLink && ( + Open {file.name}}> + + + + + ) + return ( +
+ {open} + {download} +
+ ); + }} /> diff --git a/SingularityUI/app/utils.es6 b/SingularityUI/app/utils.es6 index d41c97b093..bfc9627420 100644 --- a/SingularityUI/app/utils.es6 +++ b/SingularityUI/app/utils.es6 @@ -16,6 +16,8 @@ const Utils = { DEFAULT_SLAVES_COLUMNS: {'id': true, 'state': true, 'since': true, 'rack': true, 'host': true, 'uptime': true, 'actionUser': true, 'message': true, 'expiring': true}, + OPENABLE_EXTENSIONS: ['svg', 'txt', 'jpg', 'jpeg', 'gif', 'png', 'pdf', 'html'], + isIn(needle, haystack) { return !_.isEmpty(haystack) && haystack.indexOf(needle) >= 0; },