Skip to content
Merged
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
41 changes: 41 additions & 0 deletions java/src/org/openqa/selenium/grid/node/DownloadFile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.grid.node;

import java.io.UncheckedIOException;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.http.HttpHandler;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;

class DownloadFile implements HttpHandler {

private final Node node;
private final SessionId id;

DownloadFile(Node node, SessionId id) {
this.node = Require.nonNull("Node", node);
this.id = Require.nonNull("Session id", id);
}

@Override
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
return node.downloadFile(req, id);
}
}
5 changes: 5 additions & 0 deletions java/src/org/openqa/selenium/grid/node/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ protected Node(Tracer tracer, NodeId id, URI uri, Secret registrationSecret) {
post("/session/{sessionId}/se/file")
.to(params -> new UploadFile(this, sessionIdFrom(params)))
.with(spanDecorator("node.upload_file")),
get("/session/{sessionId}/se/file")
.to(params -> new DownloadFile(this, sessionIdFrom(params)))
.with(spanDecorator("node.download_file")),
get("/se/grid/node/owner/{sessionId}")
.to(params -> new IsSessionOwner(this, sessionIdFrom(params)))
.with(spanDecorator("node.is_session_owner").andThen(requiresSecret)),
Expand Down Expand Up @@ -216,6 +219,8 @@ public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOExcepti

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

public abstract HttpResponse downloadFile(HttpRequest req, SessionId id);

public abstract void stop(SessionId id) throws NoSuchSessionException;

public abstract boolean isSessionOwner(SessionId id);
Expand Down
8 changes: 8 additions & 0 deletions java/src/org/openqa/selenium/grid/node/config/NodeFlags.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ public class NodeFlags implements HasRoles {
example = DEFAULT_NODE_IMPLEMENTATION)
private String nodeImplementation = DEFAULT_NODE_IMPLEMENTATION;

@Parameter(
names = {"--downloads-path"},
description = "The default location wherein all browser triggered file downloads would be "
+ "available to be retrieved from. This is usually the directory that you configure in "
+ "your browser as the default location for storing downloaded files.")
@ConfigValue(section = NODE_SECTION, name = "downloads-path", example = "")
private String downloadsPath = "";

@Override
public Set<Role> getRoles() {
return Collections.singleton(NODE_ROLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ public Optional<URI> getPublicGridUri() {
}
}

public Optional<String> getDownloadsPath() {
return config.get(NODE_SECTION, "downloads-path");
}

public Node getNode() {
return config.getClass(NODE_SECTION, "implementation", Node.class, DEFAULT_NODE_IMPLEMENTATION);
}
Expand Down
5 changes: 5 additions & 0 deletions java/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
return null;
}

@Override
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
return null;
}

@Override
public void stop(SessionId id) throws NoSuchSessionException {
LOG.info("Stop has been called: " + id);
Expand Down
58 changes: 56 additions & 2 deletions java/src/org/openqa/selenium/grid/node/local/LocalNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public class LocalNode extends Node {
private final int maxSessionCount;
private final int configuredSessionCount;
private final boolean cdpEnabled;
private final String downloadsPath;

private final boolean bidiEnabled;
private final AtomicBoolean drainAfterSessions = new AtomicBoolean();
Expand All @@ -145,7 +146,8 @@ private LocalNode(
Duration sessionTimeout,
Duration heartbeatPeriod,
List<SessionSlot> factories,
Secret registrationSecret) {
Secret registrationSecret,
String downloadsPath) {
super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret);

this.bus = Require.nonNull("Event bus", bus);
Expand All @@ -162,6 +164,7 @@ private LocalNode(
this.sessionCount.set(drainAfterSessionCount);
this.cdpEnabled = cdpEnabled;
this.bidiEnabled = bidiEnabled;
this.downloadsPath = Optional.ofNullable(downloadsPath).orElse("");

this.healthCheck = healthCheck == null ?
() -> {
Expand Down Expand Up @@ -471,6 +474,50 @@ public HttpResponse executeWebDriverCommand(HttpRequest req) {
return toReturn;
}

@Override
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
// When the session is running in a Docker container, the download file command
// needs to be forwarded to the container as well.
SessionSlot slot = currentSessions.getIfPresent(id);
if (slot != null && slot.getSession() instanceof DockerSession) {
return executeWebDriverCommand(req);
}
if (this.downloadsPath.isEmpty()) {
String msg = "Please specify the path wherein the files downloaded using the browser "
+ "would be available via the command line arg [--downloads-path] and restart the node";
throw new WebDriverException(msg);
}
File dir = new File(this.downloadsPath);
if (!dir.exists()) {
throw new WebDriverException(
String.format("Cannot locate downloads directory %s.", downloadsPath));
}
if (!dir.isDirectory()) {
throw new WebDriverException(String.format("Invalid directory: %s.", downloadsPath));
}
String filename = req.getQueryParameter("filename");
try {
File[] allFiles = Optional.ofNullable(
dir.listFiles((dir1, name) -> name.equals(filename))
).orElse(new File[]{});
if (allFiles.length == 0) {
throw new WebDriverException(
String.format("Cannot find file [%s] in directory %s.", filename, downloadsPath));
}
if (allFiles.length != 1) {
throw new WebDriverException(
String.format("Expected there to be only 1 file. There were: %s.", allFiles.length));
}
String content = Zip.zip(allFiles[0]);
ImmutableMap<String, Object> result = ImmutableMap.of(
"filename", filename,
"contents", content);
return new HttpResponse().setContent(asJson(result));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public HttpResponse uploadFile(HttpRequest req, SessionId id) {

Expand Down Expand Up @@ -701,6 +748,7 @@ public static class Builder {
private Duration sessionTimeout = Duration.ofSeconds(NodeOptions.DEFAULT_SESSION_TIMEOUT);
private HealthCheck healthCheck;
private Duration heartbeatPeriod = Duration.ofSeconds(NodeOptions.DEFAULT_HEARTBEAT_PERIOD);
private String downloadsPath = "";

private Builder(
Tracer tracer,
Expand Down Expand Up @@ -755,6 +803,11 @@ public Builder heartbeatPeriod(Duration heartbeatPeriod) {
return this;
}

public Builder downloadsPath(String path) {
this.downloadsPath = path;
return this;
}

public LocalNode build() {
return new LocalNode(
tracer,
Expand All @@ -770,7 +823,8 @@ public LocalNode build() {
sessionTimeout,
heartbeatPeriod,
factories.build(),
registrationSecret);
registrationSecret,
downloadsPath);
}

public Advanced advanced() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public static Node create(Config config) {
List<DriverService.Builder<?, ?>> builders = new ArrayList<>();
ServiceLoader.load(DriverService.Builder.class).forEach(builders::add);

nodeOptions.getDownloadsPath().ifPresent(builder::downloadsPath);

nodeOptions
.getSessionFactories(
caps -> createSessionFactory(tracer, clientFactory, sessionTimeout, builders, caps))
Expand Down
5 changes: 5 additions & 0 deletions java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
return client.execute(req);
}

@Override
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
return client.execute(req);
}

@Override
public void stop(SessionId id) throws NoSuchSessionException {
Require.nonNull("Session ID", id);
Expand Down
17 changes: 17 additions & 0 deletions java/src/org/openqa/selenium/remote/http/Contents.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
import com.google.common.io.ByteStreams;
import com.google.common.io.FileBackedOutputStream;

import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.util.Base64;
import java.util.zip.ZipOutputStream;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
Expand Down Expand Up @@ -138,6 +143,18 @@ public static Supplier<InputStream> memoize(Supplier<InputStream> delegate) {
return new MemoizedSupplier(delegate);
}

public static String string(File input) throws IOException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
InputStream isr = Files.newInputStream(input.toPath())) {
int len;
byte[] buffer = new byte[4096];
while ((len = isr.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return Base64.getEncoder().encodeToString(bos.toByteArray());
}
}

private static final class MemoizedSupplier implements Supplier<InputStream> {

private volatile boolean initialized;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
throw new UnsupportedOperationException("uploadFile");
}

@Override
public HttpResponse downloadFile(HttpRequest req, SessionId id) {
throw new UnsupportedOperationException("downloadFile");
}

@Override
public Session getSession(SessionId id) throws NoSuchSessionException {
if (running == null || !running.getId().equals(id)) {
Expand Down
36 changes: 36 additions & 0 deletions java/test/org/openqa/selenium/grid/node/NodeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import java.nio.file.Path;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.Capabilities;
Expand Down Expand Up @@ -115,6 +116,7 @@ public void setUp() throws URISyntaxException {
caps = new ImmutableCapabilities("browserName", "cheese");

uri = new URI("http://localhost:1234");
File downloadsPath = new File(System.getProperty("java.io.tmpdir"));

class Handler extends Session implements HttpHandler {
private Handler(Capabilities capabilities) {
Expand All @@ -131,6 +133,7 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
.add(caps, new TestSessionFactory((id, c) -> new Handler(c)))
.downloadsPath(downloadsPath.getAbsolutePath())
.maximumConcurrentSessions(2)
.build();

Expand Down Expand Up @@ -484,6 +487,28 @@ void canUploadAFile() throws IOException {
assertThat(baseDir).doesNotExist();
}

@Test
void canDownloadAFile() throws IOException {
Either<WebDriverException, CreateSessionResponse> response =
node.newSession(createSessionRequest(caps));
assertThatEither(response).isRight();
Session session = response.right().getSession();
String hello = "Hello, world!";

HttpRequest req = new HttpRequest(GET, String.format("/session/%s/se/file", session.getId()));
File zip = createTmpFile(hello);
req.addQueryParameter("filename", zip.getName());
HttpResponse rsp = node.execute(req);
node.stop(session.getId());
Map<String, Object> map = new Json().toType(string(rsp), Json.MAP_TYPE);
File baseDir = getTemporaryFilesystemBaseDir(TemporaryFilesystem.getDefaultTmpFS());
String encodedContents = map.get("contents").toString();
Zip.unzip(encodedContents, baseDir);
Path path = new File(baseDir.getAbsolutePath() + "/" + map.get("filename")).toPath();
String decodedContents = String.join("", Files.readAllLines(path));
assertThat(decodedContents).isEqualTo(hello);
}

@Test
void shouldNotCreateSessionIfDraining() {
node.drain();
Expand Down Expand Up @@ -573,6 +598,17 @@ void shouldAllowsWebDriverCommandsForOngoingSessionIfDraining() throws Interrupt
assertThat(latch.getCount()).isEqualTo(1);
}

private File createFile(String content, File directory) {
try {
File f = new File(directory.getAbsolutePath(), UUID.randomUUID().toString());
f.deleteOnExit();
Files.write(directory.toPath(), content.getBytes(StandardCharsets.UTF_8));
return f;
} catch (IOException e) {
throw new RuntimeException(e);
}

}
private File createTmpFile(String content) {
try {
File f = File.createTempFile("webdriver", "tmp");
Expand Down