2626import com .google .common .collect .ImmutableList ;
2727import com .google .common .collect .ImmutableMap ;
2828
29+ import java .io .Serializable ;
30+ import java .nio .file .Files ;
31+ import java .nio .file .Path ;
32+ import java .nio .file .Paths ;
2933import java .util .Arrays ;
34+ import java .util .Comparator ;
35+ import java .util .concurrent .ConcurrentHashMap ;
36+ import java .util .stream .Stream ;
3037import org .openqa .selenium .Capabilities ;
3138import org .openqa .selenium .ImmutableCapabilities ;
3239import org .openqa .selenium .MutableCapabilities ;
6370import org .openqa .selenium .io .TemporaryFilesystem ;
6471import org .openqa .selenium .io .Zip ;
6572import org .openqa .selenium .json .Json ;
73+ import org .openqa .selenium .remote .Browser ;
6674import org .openqa .selenium .remote .SessionId ;
6775import org .openqa .selenium .remote .http .HttpMethod ;
6876import org .openqa .selenium .remote .http .HttpRequest ;
@@ -116,6 +124,7 @@ public class LocalNode extends Node {
116124
117125 private static final Json JSON = new Json ();
118126 private static final Logger LOG = Logger .getLogger (LocalNode .class .getName ());
127+
119128 private final EventBus bus ;
120129 private final URI externalUri ;
121130 private final URI gridUri ;
@@ -124,7 +133,7 @@ public class LocalNode extends Node {
124133 private final int maxSessionCount ;
125134 private final int configuredSessionCount ;
126135 private final boolean cdpEnabled ;
127- private final String downloadsPath ;
136+ private final boolean enableManageDownloads ;
128137
129138 private final boolean bidiEnabled ;
130139 private final AtomicBoolean drainAfterSessions = new AtomicBoolean ();
@@ -133,6 +142,10 @@ public class LocalNode extends Node {
133142 private final Cache <SessionId , TemporaryFilesystem > tempFileSystems ;
134143 private final AtomicInteger pendingSessions = new AtomicInteger ();
135144 private final AtomicInteger sessionCount = new AtomicInteger ();
145+ private final Map <SessionId ,UUID > downloadFolderToSessionMapping = new ConcurrentHashMap <>();
146+ private final String baseDir ;
147+
148+ private File defaultDownloadsDir ;
136149
137150 private LocalNode (
138151 Tracer tracer ,
@@ -149,7 +162,8 @@ private LocalNode(
149162 Duration heartbeatPeriod ,
150163 List <SessionSlot > factories ,
151164 Secret registrationSecret ,
152- String downloadsPath ) {
165+ String baseDir ,
166+ boolean enableManageDownloads ) {
153167 super (tracer , new NodeId (UUID .randomUUID ()), uri , registrationSecret );
154168
155169 this .bus = Require .nonNull ("Event bus" , bus );
@@ -166,7 +180,8 @@ private LocalNode(
166180 this .sessionCount .set (drainAfterSessionCount );
167181 this .cdpEnabled = cdpEnabled ;
168182 this .bidiEnabled = bidiEnabled ;
169- this .downloadsPath = Optional .ofNullable (downloadsPath ).orElse ("" );
183+ this .enableManageDownloads = enableManageDownloads ;
184+ this .baseDir = baseDir ;
170185
171186 this .healthCheck = healthCheck == null ?
172187 () -> {
@@ -283,6 +298,11 @@ public int getCurrentSessionCount() {
283298 return Math .toIntExact (currentSessions .size ());
284299 }
285300
301+ @ VisibleForTesting
302+ public String getDownloadsDirForSession (SessionId sessionId ) {
303+ return downloadFolderToSessionMapping .get (sessionId ).toString ();
304+ }
305+
286306 @ ManagedAttribute (name = "MaxSessions" )
287307 public int getMaxSessionCount () {
288308 return maxSessionCount ;
@@ -381,10 +401,19 @@ public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessio
381401 new RetrySessionRequestException ("No slot matched the requested capabilities." ));
382402 }
383403
404+ UUID folderIdForPossibleSession = UUID .randomUUID ();
405+ Capabilities desiredCapabilities = sessionRequest .getDesiredCapabilities ();
406+ Capabilities enriched = enrichWithDownloadsPathInfo (folderIdForPossibleSession , desiredCapabilities );
407+ enriched = desiredCapabilities .merge (enriched );
408+ sessionRequest = new CreateSessionRequest (sessionRequest .getDownstreamDialects (), enriched , sessionRequest .getMetadata ());
409+
384410 Either <WebDriverException , ActiveSession > possibleSession = slotToUse .apply (sessionRequest );
385411
386412 if (possibleSession .isRight ()) {
387413 ActiveSession session = possibleSession .right ();
414+ downloadFolderToSessionMapping .put (session .getId (), folderIdForPossibleSession );
415+ LOG .info ("Downloads pertaining to Session Id [" + session .getId ().toString () + "] will be " +
416+ "saved to " + defaultDownloadsBaseFolder ().getAbsolutePath () + "/" + folderIdForPossibleSession );
388417 currentSessions .put (session .getId (), slotToUse );
389418
390419 checkSessionCount ();
@@ -407,7 +436,7 @@ public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessio
407436 externalUri ,
408437 slotToUse .isSupportingCdp (),
409438 slotToUse .isSupportingBiDi (),
410- sessionRequest . getDesiredCapabilities () );
439+ desiredCapabilities );
411440
412441 String sessionCreatedMessage = "Session created by the Node" ;
413442 LOG .info (String .format ("%s. Id: %s, Caps: %s" , sessionCreatedMessage , sessionId , caps ));
@@ -425,6 +454,57 @@ public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessio
425454 }
426455 }
427456
457+ private Capabilities enrichWithDownloadsPathInfo (UUID id , Capabilities caps ) {
458+ if (!enableManageDownloads || !Browser .honoursSpecifiedDownloadsDir (caps )) {
459+ // Auto enable of downloads is not turned on. So don't bother fiddling around
460+ // with the capabilities.
461+ return caps ;
462+ }
463+
464+ File subDir = new File (defaultDownloadsBaseFolder (), id .toString ());
465+ boolean created = subDir .mkdirs ();
466+ if (created ) {
467+ LOG .fine ("Created folder " + subDir .getAbsolutePath () + " for auto downloads" );
468+ } else {
469+ //Should we error out here because if the downloads folder can't be created, then
470+ //there's a good chance that the downloads may not work too.
471+ LOG .warning ("Could not create folder " + subDir .getAbsolutePath () + " for auto downloads" );
472+ }
473+ if (Browser .CHROME .is (caps )) {
474+ ImmutableMap <String , Serializable > map = ImmutableMap .of (
475+ "download.prompt_for_download" , false ,
476+ "download.default_directory" , subDir .getAbsolutePath ());
477+ appendPrefs (caps , "goog:chromeOptions" , map );
478+ return caps ;
479+ }
480+
481+ if (Browser .EDGE .is (caps )) {
482+ ImmutableMap <String , Serializable > map = ImmutableMap .of (
483+ "download.prompt_for_download" , false ,
484+ "download.default_directory" , subDir .getAbsolutePath ());
485+ appendPrefs (caps , "ms:edgeOptions" , map );
486+ return caps ;
487+ }
488+
489+ if (Browser .FIREFOX .is (caps )) {
490+ ImmutableMap <String , Serializable > map = ImmutableMap .of (
491+ "browser.download.folderList" , 2 ,
492+ "browser.download.dir" , subDir .getAbsolutePath ());
493+ appendPrefs (caps , "moz:firefoxOptions" , map );
494+ }
495+ return caps ;
496+ }
497+
498+ @ SuppressWarnings ("unchecked" )
499+ private Capabilities appendPrefs (Capabilities caps , String optionsKey , Map <String , Serializable > map ) {
500+ Map <String , Object > currentOptions = (Map <String , Object >) Optional .ofNullable (
501+ caps .getCapability (optionsKey ))
502+ .orElse (new HashMap <>());
503+
504+ ((Map <String , Serializable >)currentOptions .computeIfAbsent ("prefs" ,k -> new HashMap <>())).putAll (map );
505+ return caps ;
506+ }
507+
428508 @ Override
429509 public boolean isSessionOwner (SessionId id ) {
430510 Require .nonNull ("Session ID" , id );
@@ -484,18 +564,19 @@ public HttpResponse downloadFile(HttpRequest req, SessionId id) {
484564 if (slot != null && slot .getSession () instanceof DockerSession ) {
485565 return executeWebDriverCommand (req );
486566 }
487- if (this .downloadsPath . isEmpty () ) {
488- String msg = "Please specify the path wherein the files downloaded using the browser "
489- + "would be available via the command line arg [--downloads-path ] and restart the node" ;
567+ if (! this .enableManageDownloads ) {
568+ String msg = "Please enable management of downloads via the command line arg "
569+ + "[--enable-manage-downloads ] and restart the node" ;
490570 throw new WebDriverException (msg );
491571 }
492- File dir = new File (this .downloadsPath );
572+ File dir = new File (defaultDownloadsBaseFolder (),
573+ downloadFolderToSessionMapping .get (id ).toString ());
493574 if (!dir .exists ()) {
494575 throw new WebDriverException (
495- String .format ("Cannot locate downloads directory %s." , downloadsPath ));
576+ String .format ("Cannot locate downloads directory %s." , dir ));
496577 }
497578 if (!dir .isDirectory ()) {
498- throw new WebDriverException (String .format ("Invalid directory: %s." , downloadsPath ));
579+ throw new WebDriverException (String .format ("Invalid directory: %s." , dir ));
499580 }
500581 if (req .getMethod ().equals (HttpMethod .GET )) {
501582 //User wants to list files that can be downloaded
@@ -525,7 +606,7 @@ public HttpResponse downloadFile(HttpRequest req, SessionId id) {
525606 ).orElse (new File []{});
526607 if (allFiles .length == 0 ) {
527608 throw new WebDriverException (
528- String .format ("Cannot find file [%s] in directory %s." , filename , downloadsPath ));
609+ String .format ("Cannot find file [%s] in directory %s." , filename , dir . getAbsolutePath () ));
529610 }
530611 if (allFiles .length != 1 ) {
531612 throw new WebDriverException (
@@ -590,6 +671,26 @@ public void stop(SessionId id) throws NoSuchSessionException {
590671 }
591672
592673 currentSessions .invalidate (id );
674+ deleteDownloadsFolderForSession (id );
675+ }
676+
677+ private void deleteDownloadsFolderForSession (SessionId id ) {
678+ String childDir = downloadFolderToSessionMapping .get (id ).toString ();
679+ Path toDelete = Paths .get (defaultDownloadsBaseFolder ().getAbsolutePath (), childDir );
680+ if (!toDelete .toFile ().exists ()) {
681+ return ;
682+ }
683+ try (Stream <Path > walk = Files .walk (toDelete )) {
684+ walk
685+ .sorted (Comparator .reverseOrder ())
686+ .map (Path ::toFile )
687+ .peek (each -> LOG .info ("Deleting downloads folder for session " + id ))
688+ .forEach (File ::delete );
689+ downloadFolderToSessionMapping .remove (id );
690+ } catch (IOException e ) {
691+ LOG .warning ("Could not delete downloads directory for session " + id + ". Root cause: "
692+ + e .getMessage ());
693+ }
593694 }
594695
595696 private void stopAllSessions () {
@@ -756,6 +857,22 @@ private Map<String, Object> toJson() {
756857 .collect (Collectors .toSet ()));
757858 }
758859
860+ private File defaultDownloadsBaseFolder () {
861+ if (defaultDownloadsDir != null ) {
862+ return defaultDownloadsDir ;
863+ }
864+ String location = baseDir + File .separator + ".cache" +
865+ File .separator + "selenium" + File .separator + "downloads" ;
866+ defaultDownloadsDir = new File (location );
867+ boolean created = defaultDownloadsDir .mkdirs ();
868+ if (created ) {
869+ LOG .info ("All files downloaded by sessions on this node can be found under [" + location + "]." );
870+ } else {
871+ LOG .info (location + " already exists. Using it for managing downloaded files." );
872+ }
873+ return defaultDownloadsDir ;
874+ }
875+
759876 public static class Builder {
760877
761878 private final Tracer tracer ;
@@ -772,7 +889,8 @@ public static class Builder {
772889 private Duration sessionTimeout = Duration .ofSeconds (NodeOptions .DEFAULT_SESSION_TIMEOUT );
773890 private HealthCheck healthCheck ;
774891 private Duration heartbeatPeriod = Duration .ofSeconds (NodeOptions .DEFAULT_HEARTBEAT_PERIOD );
775- private String downloadsPath = "" ;
892+ private boolean enableManageDownloads = false ;
893+ private String baseDirectory ;
776894
777895 private Builder (
778896 Tracer tracer ,
@@ -827,8 +945,13 @@ public Builder heartbeatPeriod(Duration heartbeatPeriod) {
827945 return this ;
828946 }
829947
830- public Builder downloadsPath (String path ) {
831- this .downloadsPath = path ;
948+ public Builder enableManageDownloads (boolean enable ) {
949+ this .enableManageDownloads = enable ;
950+ return this ;
951+ }
952+
953+ public Builder baseDirectory (String directory ) {
954+ this .baseDirectory = directory ;
832955 return this ;
833956 }
834957
@@ -848,7 +971,8 @@ public LocalNode build() {
848971 heartbeatPeriod ,
849972 factories .build (),
850973 registrationSecret ,
851- downloadsPath );
974+ Optional .ofNullable (baseDirectory ).orElse (System .getProperty ("user.home" )),
975+ enableManageDownloads );
852976 }
853977
854978 public Advanced advanced () {
0 commit comments