Skip to content

Commit 8e4e20b

Browse files
authored
[grid] Support file downloads on the node (#11277)
* [grid] Support file downloads on the node Fixes: #9218 On the node, set the directory to where the browser will download files to via: a. The CLI argument `--downloads-dir` (or) b. via the Toml syntax of ``` [node] downloads-dir = "/path/to/dir/goes/here" ``` The GET end-points that will support file download would be: * `/session/:sessionId:/se/file?filename=` * Fixing review comments * Renamed the CLI arg to downloads-path * Download is available only via GET /session/{sessionId}/se/file * Files downloaded would be available ONLY as a zip file. * Additional review comment fixes
1 parent e239245 commit 8e4e20b

File tree

11 files changed

+184
-2
lines changed

11 files changed

+184
-2
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.node;
19+
20+
import java.io.UncheckedIOException;
21+
import org.openqa.selenium.internal.Require;
22+
import org.openqa.selenium.remote.SessionId;
23+
import org.openqa.selenium.remote.http.HttpHandler;
24+
import org.openqa.selenium.remote.http.HttpRequest;
25+
import org.openqa.selenium.remote.http.HttpResponse;
26+
27+
class DownloadFile implements HttpHandler {
28+
29+
private final Node node;
30+
private final SessionId id;
31+
32+
DownloadFile(Node node, SessionId id) {
33+
this.node = Require.nonNull("Node", node);
34+
this.id = Require.nonNull("Session id", id);
35+
}
36+
37+
@Override
38+
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
39+
return node.downloadFile(req, id);
40+
}
41+
}

java/src/org/openqa/selenium/grid/node/Node.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ protected Node(Tracer tracer, NodeId id, URI uri, Secret registrationSecret) {
150150
post("/session/{sessionId}/se/file")
151151
.to(params -> new UploadFile(this, sessionIdFrom(params)))
152152
.with(spanDecorator("node.upload_file")),
153+
get("/session/{sessionId}/se/file")
154+
.to(params -> new DownloadFile(this, sessionIdFrom(params)))
155+
.with(spanDecorator("node.download_file")),
153156
get("/se/grid/node/owner/{sessionId}")
154157
.to(params -> new IsSessionOwner(this, sessionIdFrom(params)))
155158
.with(spanDecorator("node.is_session_owner").andThen(requiresSecret)),
@@ -216,6 +219,8 @@ public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOExcepti
216219

217220
public abstract HttpResponse uploadFile(HttpRequest req, SessionId id);
218221

222+
public abstract HttpResponse downloadFile(HttpRequest req, SessionId id);
223+
219224
public abstract void stop(SessionId id) throws NoSuchSessionException;
220225

221226
public abstract boolean isSessionOwner(SessionId id);

java/src/org/openqa/selenium/grid/node/config/NodeFlags.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ public class NodeFlags implements HasRoles {
221221
example = DEFAULT_NODE_IMPLEMENTATION)
222222
private String nodeImplementation = DEFAULT_NODE_IMPLEMENTATION;
223223

224+
@Parameter(
225+
names = {"--downloads-path"},
226+
description = "The default location wherein all browser triggered file downloads would be "
227+
+ "available to be retrieved from. This is usually the directory that you configure in "
228+
+ "your browser as the default location for storing downloaded files.")
229+
@ConfigValue(section = NODE_SECTION, name = "downloads-path", example = "")
230+
private String downloadsPath = "";
231+
224232
@Override
225233
public Set<Role> getRoles() {
226234
return Collections.singleton(NODE_ROLE);

java/src/org/openqa/selenium/grid/node/config/NodeOptions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ public Optional<URI> getPublicGridUri() {
148148
}
149149
}
150150

151+
public Optional<String> getDownloadsPath() {
152+
return config.get(NODE_SECTION, "downloads-path");
153+
}
154+
151155
public Node getNode() {
152156
return config.getClass(NODE_SECTION, "implementation", Node.class, DEFAULT_NODE_IMPLEMENTATION);
153157
}

java/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
307307
return null;
308308
}
309309

310+
@Override
311+
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
312+
return null;
313+
}
314+
310315
@Override
311316
public void stop(SessionId id) throws NoSuchSessionException {
312317
LOG.info("Stop has been called: " + id);

java/src/org/openqa/selenium/grid/node/local/LocalNode.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ public class LocalNode extends Node {
122122
private final int maxSessionCount;
123123
private final int configuredSessionCount;
124124
private final boolean cdpEnabled;
125+
private final String downloadsPath;
125126

126127
private final boolean bidiEnabled;
127128
private final AtomicBoolean drainAfterSessions = new AtomicBoolean();
@@ -145,7 +146,8 @@ private LocalNode(
145146
Duration sessionTimeout,
146147
Duration heartbeatPeriod,
147148
List<SessionSlot> factories,
148-
Secret registrationSecret) {
149+
Secret registrationSecret,
150+
String downloadsPath) {
149151
super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret);
150152

151153
this.bus = Require.nonNull("Event bus", bus);
@@ -162,6 +164,7 @@ private LocalNode(
162164
this.sessionCount.set(drainAfterSessionCount);
163165
this.cdpEnabled = cdpEnabled;
164166
this.bidiEnabled = bidiEnabled;
167+
this.downloadsPath = Optional.ofNullable(downloadsPath).orElse("");
165168

166169
this.healthCheck = healthCheck == null ?
167170
() -> {
@@ -471,6 +474,50 @@ public HttpResponse executeWebDriverCommand(HttpRequest req) {
471474
return toReturn;
472475
}
473476

477+
@Override
478+
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
479+
// When the session is running in a Docker container, the download file command
480+
// needs to be forwarded to the container as well.
481+
SessionSlot slot = currentSessions.getIfPresent(id);
482+
if (slot != null && slot.getSession() instanceof DockerSession) {
483+
return executeWebDriverCommand(req);
484+
}
485+
if (this.downloadsPath.isEmpty()) {
486+
String msg = "Please specify the path wherein the files downloaded using the browser "
487+
+ "would be available via the command line arg [--downloads-path] and restart the node";
488+
throw new WebDriverException(msg);
489+
}
490+
File dir = new File(this.downloadsPath);
491+
if (!dir.exists()) {
492+
throw new WebDriverException(
493+
String.format("Cannot locate downloads directory %s.", downloadsPath));
494+
}
495+
if (!dir.isDirectory()) {
496+
throw new WebDriverException(String.format("Invalid directory: %s.", downloadsPath));
497+
}
498+
String filename = req.getQueryParameter("filename");
499+
try {
500+
File[] allFiles = Optional.ofNullable(
501+
dir.listFiles((dir1, name) -> name.equals(filename))
502+
).orElse(new File[]{});
503+
if (allFiles.length == 0) {
504+
throw new WebDriverException(
505+
String.format("Cannot find file [%s] in directory %s.", filename, downloadsPath));
506+
}
507+
if (allFiles.length != 1) {
508+
throw new WebDriverException(
509+
String.format("Expected there to be only 1 file. There were: %s.", allFiles.length));
510+
}
511+
String content = Zip.zip(allFiles[0]);
512+
ImmutableMap<String, Object> result = ImmutableMap.of(
513+
"filename", filename,
514+
"contents", content);
515+
return new HttpResponse().setContent(asJson(result));
516+
} catch (IOException e) {
517+
throw new UncheckedIOException(e);
518+
}
519+
}
520+
474521
@Override
475522
public HttpResponse uploadFile(HttpRequest req, SessionId id) {
476523

@@ -701,6 +748,7 @@ public static class Builder {
701748
private Duration sessionTimeout = Duration.ofSeconds(NodeOptions.DEFAULT_SESSION_TIMEOUT);
702749
private HealthCheck healthCheck;
703750
private Duration heartbeatPeriod = Duration.ofSeconds(NodeOptions.DEFAULT_HEARTBEAT_PERIOD);
751+
private String downloadsPath = "";
704752

705753
private Builder(
706754
Tracer tracer,
@@ -755,6 +803,11 @@ public Builder heartbeatPeriod(Duration heartbeatPeriod) {
755803
return this;
756804
}
757805

806+
public Builder downloadsPath(String path) {
807+
this.downloadsPath = path;
808+
return this;
809+
}
810+
758811
public LocalNode build() {
759812
return new LocalNode(
760813
tracer,
@@ -770,7 +823,8 @@ public LocalNode build() {
770823
sessionTimeout,
771824
heartbeatPeriod,
772825
factories.build(),
773-
registrationSecret);
826+
registrationSecret,
827+
downloadsPath);
774828
}
775829

776830
public Advanced advanced() {

java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public static Node create(Config config) {
7676
List<DriverService.Builder<?, ?>> builders = new ArrayList<>();
7777
ServiceLoader.load(DriverService.Builder.class).forEach(builders::add);
7878

79+
nodeOptions.getDownloadsPath().ifPresent(builder::downloadsPath);
80+
7981
nodeOptions
8082
.getSessionFactories(
8183
caps -> createSessionFactory(tracer, clientFactory, sessionTimeout, builders, caps))

java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
189189
return client.execute(req);
190190
}
191191

192+
@Override
193+
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
194+
return client.execute(req);
195+
}
196+
192197
@Override
193198
public void stop(SessionId id) throws NoSuchSessionException {
194199
Require.nonNull("Session ID", id);

java/src/org/openqa/selenium/remote/http/Contents.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
import com.google.common.io.ByteStreams;
2323
import com.google.common.io.FileBackedOutputStream;
2424

25+
import java.io.File;
26+
import java.io.FileInputStream;
27+
import java.nio.file.Files;
28+
import java.util.Base64;
29+
import java.util.zip.ZipOutputStream;
2530
import org.openqa.selenium.internal.Require;
2631
import org.openqa.selenium.json.Json;
2732
import org.openqa.selenium.json.JsonInput;
@@ -138,6 +143,18 @@ public static Supplier<InputStream> memoize(Supplier<InputStream> delegate) {
138143
return new MemoizedSupplier(delegate);
139144
}
140145

146+
public static String string(File input) throws IOException {
147+
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
148+
InputStream isr = Files.newInputStream(input.toPath())) {
149+
int len;
150+
byte[] buffer = new byte[4096];
151+
while ((len = isr.read(buffer)) != -1) {
152+
bos.write(buffer, 0, len);
153+
}
154+
return Base64.getEncoder().encodeToString(bos.toByteArray());
155+
}
156+
}
157+
141158
private static final class MemoizedSupplier implements Supplier<InputStream> {
142159

143160
private volatile boolean initialized;

java/test/org/openqa/selenium/grid/distributor/AddingNodesTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
384384
throw new UnsupportedOperationException("uploadFile");
385385
}
386386

387+
@Override
388+
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
389+
throw new UnsupportedOperationException("downloadFile");
390+
}
391+
387392
@Override
388393
public Session getSession(SessionId id) throws NoSuchSessionException {
389394
if (running == null || !running.getId().equals(id)) {

0 commit comments

Comments
 (0)