Skip to content

Commit fb02c4d

Browse files
committed
[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:/file?filename=` * `/session/:sessionId:/se/file?filename=`
1 parent a70d3bc commit fb02c4d

File tree

9 files changed

+171
-4
lines changed

9 files changed

+171
-4
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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ 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}/file")
154+
.to(params -> new DownloadFile(this, sessionIdFrom(params)))
155+
.with(spanDecorator("node.download_file")),
156+
get("/session/{sessionId}/se/file")
157+
.to(params -> new DownloadFile(this, sessionIdFrom(params)))
158+
.with(spanDecorator("node.download_file")),
153159
get("/se/grid/node/owner/{sessionId}")
154160
.to(params -> new IsSessionOwner(this, sessionIdFrom(params)))
155161
.with(spanDecorator("node.is_session_owner").andThen(requiresSecret)),
@@ -216,6 +222,8 @@ public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOExcepti
216222

217223
public abstract HttpResponse uploadFile(HttpRequest req, SessionId id);
218224

225+
public abstract HttpResponse downloadFile(HttpRequest req, SessionId id);
226+
219227
public abstract void stop(SessionId id) throws NoSuchSessionException;
220228

221229
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-dir"},
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-dir", example = "")
230+
private String downloadsDir = "";
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> getDownloadsDirectory() {
152+
return config.get(NODE_SECTION, "downloads-dir");
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: 59 additions & 4 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 downloadsDir;
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 downloadsDir) {
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.downloadsDir = Optional.ofNullable(downloadsDir).orElse("");
165168

166169
this.healthCheck = healthCheck == null ?
167170
() -> {
@@ -176,8 +179,10 @@ private LocalNode(
176179
.ticker(ticker)
177180
.removalListener((RemovalListener<SessionId, TemporaryFilesystem>) notification -> {
178181
TemporaryFilesystem tempFS = notification.getValue();
179-
tempFS.deleteTemporaryFiles();
180-
tempFS.deleteBaseDir();
182+
if (tempFS != null) {
183+
tempFS.deleteTemporaryFiles();
184+
tempFS.deleteBaseDir();
185+
}
181186
})
182187
.build();
183188

@@ -471,6 +476,49 @@ public HttpResponse executeWebDriverCommand(HttpRequest req) {
471476
return toReturn;
472477
}
473478

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

@@ -701,6 +749,7 @@ public static class Builder {
701749
private Duration sessionTimeout = Duration.ofSeconds(NodeOptions.DEFAULT_SESSION_TIMEOUT);
702750
private HealthCheck healthCheck;
703751
private Duration heartbeatPeriod = Duration.ofSeconds(NodeOptions.DEFAULT_HEARTBEAT_PERIOD);
752+
private String downloadsDir = "";
704753

705754
private Builder(
706755
Tracer tracer,
@@ -755,6 +804,11 @@ public Builder heartbeatPeriod(Duration heartbeatPeriod) {
755804
return this;
756805
}
757806

807+
public Builder downloadsDirectory(String dir) {
808+
this.downloadsDir = dir;
809+
return this;
810+
}
811+
758812
public LocalNode build() {
759813
return new LocalNode(
760814
tracer,
@@ -770,7 +824,8 @@ public LocalNode build() {
770824
sessionTimeout,
771825
heartbeatPeriod,
772826
factories.build(),
773-
registrationSecret);
827+
registrationSecret,
828+
downloadsDir);
774829
}
775830

776831
public Advanced advanced() {

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/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("uploadFile");
390+
}
391+
387392
@Override
388393
public Session getSession(SessionId id) throws NoSuchSessionException {
389394
if (running == null || !running.getId().equals(id)) {

java/test/org/openqa/selenium/grid/node/NodeTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import com.google.common.collect.ImmutableMap;
3131
import com.google.common.collect.ImmutableSet;
3232

33+
import java.nio.file.Path;
34+
import java.nio.file.Paths;
3335
import org.junit.jupiter.api.BeforeEach;
3436
import org.junit.jupiter.api.Test;
3537
import org.openqa.selenium.Capabilities;
@@ -115,6 +117,7 @@ public void setUp() throws URISyntaxException {
115117
caps = new ImmutableCapabilities("browserName", "cheese");
116118

117119
uri = new URI("http://localhost:1234");
120+
File downloadsDir = new File(System.getProperty("java.io.tmpdir"));
118121

119122
class Handler extends Session implements HttpHandler {
120123
private Handler(Capabilities capabilities) {
@@ -131,6 +134,7 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
131134
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
132135
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
133136
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
137+
.downloadsDirectory(downloadsDir.getAbsolutePath())
134138
.maximumConcurrentSessions(2)
135139
.build();
136140

@@ -484,6 +488,27 @@ void canUploadAFile() throws IOException {
484488
assertThat(baseDir).doesNotExist();
485489
}
486490

491+
@Test
492+
void canDownloadAFile() throws IOException {
493+
Either<WebDriverException, CreateSessionResponse> response =
494+
node.newSession(createSessionRequest(caps));
495+
assertThatEither(response).isRight();
496+
Session session = response.right().getSession();
497+
String hello = "Hello, world!";
498+
499+
HttpRequest req = new HttpRequest(GET, String.format("/session/%s/file", session.getId()));
500+
File zip = createTmpFile(hello);
501+
req.addQueryParameter("filename", zip.getName());
502+
HttpResponse rsp = node.execute(req);
503+
node.stop(session.getId());
504+
Map<String, Object> map = new Json().toType(string(rsp), Json.MAP_TYPE);
505+
File baseDir = getTemporaryFilesystemBaseDir(TemporaryFilesystem.getDefaultTmpFS());
506+
Zip.unzip(rsp.getContent().get(), baseDir);
507+
Path path = Paths.get(baseDir.getAbsolutePath(), map.get("filename").toString());
508+
String text = String.join("", Files.readAllLines(path));
509+
assertThat(text).isEqualTo(hello);
510+
}
511+
487512
@Test
488513
void shouldNotCreateSessionIfDraining() {
489514
node.drain();
@@ -573,6 +598,17 @@ void shouldAllowsWebDriverCommandsForOngoingSessionIfDraining() throws Interrupt
573598
assertThat(latch.getCount()).isEqualTo(1);
574599
}
575600

601+
private File createFile(String content, File directory) {
602+
try {
603+
File f = new File(directory.getAbsolutePath(), UUID.randomUUID().toString());
604+
f.deleteOnExit();
605+
Files.write(directory.toPath(), content.getBytes(StandardCharsets.UTF_8));
606+
return f;
607+
} catch (IOException e) {
608+
throw new RuntimeException(e);
609+
}
610+
611+
}
576612
private File createTmpFile(String content) {
577613
try {
578614
File f = File.createTempFile("webdriver", "tmp");

0 commit comments

Comments
 (0)