Skip to content

Commit 3539ca4

Browse files
committed
Switch from Either to Try for all "error-enabled" types
1 parent 0e9ffe0 commit 3539ca4

File tree

8 files changed

+164
-195
lines changed

8 files changed

+164
-195
lines changed

src/main/scala/com/decodified/scalassh/Command.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package com.decodified.scalassh
1818

19-
import net.schmizz.sshj.connection.channel.direct.Session
2019
import java.io.{ByteArrayInputStream, File, FileInputStream, InputStream}
20+
import net.schmizz.sshj.connection.channel.direct.Session
2121

2222
final case class Command(command: String, input: CommandInput = CommandInput.NoInput, timeout: Option[Int] = None)
2323

src/main/scala/com/decodified/scalassh/HostConfig.scala

Lines changed: 73 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,30 @@
1616

1717
package com.decodified.scalassh
1818

19-
import net.schmizz.sshj.{Config, DefaultConfig}
20-
21-
import io.Source
2219
import java.io.{File, IOException}
23-
24-
import HostKeyVerifiers._
2520
import java.security.PublicKey
2621

2722
import net.schmizz.sshj.common.SecurityUtils
2823
import net.schmizz.sshj.connection.channel.direct.PTYMode
2924
import net.schmizz.sshj.transport.verification.{HostKeyVerifier, OpenSSHKnownHosts}
25+
import net.schmizz.sshj.{Config, DefaultConfig}
3026

31-
import annotation.tailrec
27+
import scala.annotation.tailrec
28+
import scala.util.{Failure, Success, Try}
3229
import scala.util.control.NonFatal
30+
import scala.io.Source
31+
import HostKeyVerifiers._
3332

34-
trait HostConfigProvider extends (String Validated[HostConfig])
33+
trait HostConfigProvider extends (String Try[HostConfig])
3534

3635
object HostConfigProvider {
3736
implicit def fromLogin(login: SshLogin): HostConfigProvider =
3837
new HostConfigProvider {
39-
def apply(host: String) = Right(HostConfig(login = login, hostName = host))
38+
def apply(host: String) = Success(HostConfig(login = login, hostName = host))
4039
}
4140
implicit def fromHostConfig(config: HostConfig): HostConfigProvider =
4241
new HostConfigProvider {
43-
def apply(host: String) = Right(if (config.hostName.isEmpty) config.copy(hostName = host) else config)
42+
def apply(host: String) = Success(if (config.hostName.isEmpty) config.copy(hostName = host) else config)
4443
}
4544
}
4645

@@ -51,7 +50,7 @@ final case class HostConfig(login: SshLogin,
5150
connectionTimeout: Option[Int] = None,
5251
commandTimeout: Option[Int] = None,
5352
enableCompression: Boolean = false,
54-
hostKeyVerifier: HostKeyVerifier = KnownHosts.right.toOption getOrElse DontVerify,
53+
hostKeyVerifier: HostKeyVerifier = KnownHosts.toOption getOrElse DontVerify,
5554
ptyConfig: Option[PTYConfig] = None,
5655
sshjConfig: Config = HostConfig.DefaultSshjConfig)
5756

@@ -67,28 +66,24 @@ object HostConfig {
6766
}
6867

6968
sealed abstract class FromStringsHostConfigProvider extends HostConfigProvider {
70-
protected def rawLines(host: String): Validated[(String, TraversableOnce[String])]
69+
protected def rawLines(host: String): Try[(String, TraversableOnce[String])]
7170

72-
def apply(host: String): Validated[HostConfig] =
73-
rawLines(host).right.flatMap {
71+
def apply(host: String): Try[HostConfig] =
72+
rawLines(host).flatMap {
7473
case (source, lines)
7574
for {
76-
settings splitToMap(lines, source).right
77-
login login(settings, source).right
78-
port optIntSetting("port", settings, source).right
79-
connectTimeout optIntSetting("connect-timeout", settings, source).right
80-
connectionTimeout optIntSetting("connection-timeout", settings, source).right
81-
commandTimeout optIntSetting("command-timeout", settings, source).right
82-
enableCompression optBoolSetting("enable-compression", settings, source).right
83-
verifier setting("fingerprint", settings, source).right
84-
.map(forFingerprint)
85-
.left
86-
.flatMap(_ KnownHosts)
87-
.right
75+
settings splitToMap(lines, source)
76+
login login(settings, source)
77+
port optIntSetting("port", settings, source)
78+
connectTimeout optIntSetting("connect-timeout", settings, source)
79+
connectionTimeout optIntSetting("connection-timeout", settings, source)
80+
commandTimeout optIntSetting("command-timeout", settings, source)
81+
enableCompression optBoolSetting("enable-compression", settings, source)
82+
verifier setting("fingerprint", settings, source).transform(x Success(forFingerprint(x)), _ KnownHosts)
8883
} yield {
8984
HostConfig(
9085
login,
91-
hostName = setting("host-name", settings, source).right.toOption getOrElse host,
86+
hostName = setting("host-name", settings, source).toOption getOrElse host,
9287
port = port getOrElse 22,
9388
connectTimeout = connectTimeout,
9489
connectionTimeout = connectionTimeout,
@@ -99,28 +94,28 @@ sealed abstract class FromStringsHostConfigProvider extends HostConfigProvider {
9994
}
10095
}
10196

102-
private def login(settings: Map[String, String], source: String): Validated[SshLogin] =
103-
setting("login-type", settings, source).right.flatMap {
97+
private def login(settings: Map[String, String], source: String): Try[SshLogin] =
98+
setting("login-type", settings, source).flatMap {
10499
case "password" passwordLogin(settings, source)
105100
case "keyfile" keyfileLogin(settings, source)
106101
case "agent" agentLogin(settings, source)
107102
case x
108-
Left(
109-
"Illegal login-type setting '%s' in host config '%s': expecting either 'password' or 'keyfile'"
110-
.format(x, source))
103+
Failure(
104+
SSH.Error(s"Illegal login-type setting '$x' in host config '$source': " +
105+
"expecting either 'password', 'keyfile' or 'agent'"))
111106
}
112107

113-
private def passwordLogin(settings: Map[String, String], source: String): Validated[PasswordLogin] =
108+
private def passwordLogin(settings: Map[String, String], source: String): Try[PasswordLogin] =
114109
for {
115-
user setting("username", settings, source).right
116-
pass setting("password", settings, source).right
110+
user setting("username", settings, source)
111+
pass setting("password", settings, source)
117112
} yield PasswordLogin(user, pass)
118113

119-
private def keyfileLogin(settings: Map[String, String], source: String): Validated[PublicKeyLogin] =
120-
setting("username", settings, source).right.map { user
114+
private def keyfileLogin(settings: Map[String, String], source: String): Try[PublicKeyLogin] =
115+
for (user setting("username", settings, source)) yield {
121116
import PublicKeyLogin._
122-
val keyfile = setting("keyfile", settings, source).right.toOption
123-
val passphrase = setting("passphrase", settings, source).right.toOption
117+
val keyfile = setting("keyfile", settings, source).toOption
118+
val passphrase = setting("passphrase", settings, source).toOption
124119
PublicKeyLogin(
125120
user,
126121
passphrase.map(SimplePasswordProducer),
@@ -134,43 +129,35 @@ sealed abstract class FromStringsHostConfigProvider extends HostConfigProvider {
134129
)
135130
}
136131

137-
private def agentLogin(settings: Map[String, String], source: String): Validated[AgentLogin] = {
138-
val user = setting("username", settings, source).right.toOption
139-
val host = setting("host", settings, source).right.toOption
140-
Right(AgentLogin(user getOrElse System.getProperty("user.home"), host getOrElse "localhost"))
132+
private def agentLogin(settings: Map[String, String], source: String): Try[AgentLogin] = {
133+
val user = setting("username", settings, source).toOption
134+
val host = setting("host", settings, source).toOption
135+
Success(AgentLogin(user getOrElse System.getProperty("user.home"), host getOrElse "localhost"))
141136
}
142137

143-
private def setting(key: String, settings: Map[String, String], source: String): Validated[String] =
138+
private def setting(key: String, settings: Map[String, String], source: String): Try[String] =
144139
settings.get(key) match {
145-
case Some(user) Right(user)
146-
case None Left(s"Host config '$source' is missing required setting '$key'")
140+
case Some(user) Success(user)
141+
case None Failure(SSH.Error(s"Host config '$source' is missing required setting '$key'"))
147142
}
148143

149-
private def optIntSetting(key: String, settings: Map[String, String], source: String): Validated[Option[Int]] = {
150-
setting(key, settings, source) match {
151-
case Right(value)
152-
try Right(Some(value.toInt))
153-
catch {
154-
case _: NumberFormatException
155-
Left(s"Value '$value' for setting '$key' in host config '$source' is not a legal integer")
156-
}
157-
case Left(_) Right(None)
158-
}
159-
}
144+
private def optIntSetting(key: String, settings: Map[String, String], source: String): Try[Option[Int]] =
145+
setting(key, settings, source).transform(x Success(Some(x.toInt)), _ Success(None))
160146

161-
private def optBoolSetting(key: String, settings: Map[String, String], source: String): Validated[Option[Boolean]] =
147+
private def optBoolSetting(key: String, settings: Map[String, String], source: String): Try[Option[Boolean]] =
162148
setting(key, settings, source) match {
163-
case Right("yes" | "YES" | "true" | "TRUE") Right(Some(true))
164-
case Right(value) Left(s"Value '$value' for setting '$key' in host config '$source' is not a legal integer")
165-
case Left(_) Right(None)
149+
case Success("yes" | "YES" | "true" | "TRUE") Success(Some(true))
150+
case Failure(_) Success(None)
151+
case Success(value)
152+
Failure(SSH.Error(s"Value '$value' for setting '$key' in host config '$source' is not a legal boolean"))
166153
}
167154

168155
private def splitToMap(lines: TraversableOnce[String], source: String) =
169-
lines.foldLeft(Right(Map.empty): Validated[Map[String, String]]) {
170-
case (Right(map), line) if line.nonEmpty && line.charAt(0) != '#'
156+
lines.foldLeft(Success(Map.empty): Try[Map[String, String]]) {
157+
case (Success(map), line) if line.nonEmpty && line.charAt(0) != '#'
171158
line.indexOf('=') match {
172-
case -1 Left(s"Host config '$source' contains illegal line:\n$line")
173-
case ix Right(map.updated(line.substring(0, ix).trim, line.substring(ix + 1).trim))
159+
case -1 Failure(SSH.Error(s"Host config '$source' contains illegal line:\n$line"))
160+
case ix Success(map.updated(line.substring(0, ix).trim, line.substring(ix + 1).trim))
174161
}
175162
case (result, _) result
176163
}
@@ -181,16 +168,16 @@ object HostFileConfig {
181168
def apply(): HostConfigProvider = apply(DefaultHostFileDir)
182169
def apply(hostFilesDir: String): HostConfigProvider =
183170
new FromStringsHostConfigProvider {
184-
protected def rawLines(host: String) = {
171+
protected def rawLines(host: String): Try[(String, TraversableOnce[String])] = {
185172
val locations = searchLocations(host).map(name new File(hostFilesDir + File.separator + name))
186173
locations.find(_.exists) match {
187174
case Some(file)
188-
try Right(file.getAbsolutePath Source.fromFile(file, "utf8").getLines())
189-
catch { case e: IOException Left(s"Could not read host file '$file' due to $e") }
175+
try Success(file.getAbsolutePath Source.fromFile(file, "utf8").getLines())
176+
catch { case e: IOException Failure(SSH.Error(s"Could not read host file '$file' due to $e")) }
190177
case None
191-
Left(
192-
s"Host files '${locations.mkString("', '")}' not found, " +
193-
"either provide one or use a concrete HostConfig, PasswordLogin, PublicKeyLogin or AgentLogin")
178+
Failure(
179+
SSH.Error(s"Host files '${locations.mkString("', '")}' not found, " +
180+
"either provide one or use a concrete HostConfig, PasswordLogin, PublicKeyLogin or AgentLogin"))
194181
}
195182
}
196183
}
@@ -212,7 +199,7 @@ object HostResourceConfig {
212199
def apply(): HostConfigProvider = apply("")
213200
def apply(resourceBase: String): HostConfigProvider =
214201
new FromStringsHostConfigProvider {
215-
protected def rawLines(host: String): Validated[(String, TraversableOnce[String])] = {
202+
protected def rawLines(host: String): Try[(String, TraversableOnce[String])] = {
216203
val locations = HostFileConfig.searchLocations(host).map(resourceBase + _)
217204
locations
218205
.map { location
@@ -225,11 +212,11 @@ object HostResourceConfig {
225212
}
226213
}
227214
.find(_._2.nonEmpty) match {
228-
case Some(result) Right(result)
215+
case Some(result) Success(result)
229216
case None
230-
Left(
231-
s"Host resources '${locations.mkString("', '")}' not found, " +
232-
s"either provide one or use a concrete HostConfig, PasswordLogin, PublicKeyLogin or AgentLogin")
217+
Failure(
218+
SSH.Error(s"Host resources '${locations.mkString("', '")}' not found, " +
219+
s"either provide one or use a concrete HostConfig, PasswordLogin, PublicKeyLogin or AgentLogin"))
233220
}
234221
}
235222
}
@@ -241,19 +228,21 @@ object HostKeyVerifiers {
241228
def verify(hostname: String, port: Int, key: PublicKey) = true
242229
}
243230

244-
lazy val KnownHosts: Validated[HostKeyVerifier] = {
231+
lazy val KnownHosts: Try[HostKeyVerifier] = {
245232
val sshDir = System.getProperty("user.home") + File.separator + ".ssh" + File.separator
246-
for {
247-
error1 fromKnownHostsFile(new File(sshDir + "known_hosts")).left
248-
error2 fromKnownHostsFile(new File(sshDir + "known_hosts2")).left
249-
} yield error1 + " and " + error2
233+
fromKnownHostsFile(new File(sshDir + "known_hosts")).recoverWith {
234+
case NonFatal(e1)
235+
fromKnownHostsFile(new File(sshDir + "known_hosts2")).recoverWith {
236+
case NonFatal(e2) Failure(SSH.Error(e1 + " and " + e2))
237+
}
238+
}
250239
}
251240

252-
def fromKnownHostsFile(knownHostsFile: File): Validated[HostKeyVerifier] =
241+
def fromKnownHostsFile(knownHostsFile: File): Try[HostKeyVerifier] =
253242
if (knownHostsFile.exists()) {
254-
try Right(new OpenSSHKnownHosts(knownHostsFile))
255-
catch { case NonFatal(e) Left(s"Could not read $knownHostsFile due to $e") }
256-
} else Left(knownHostsFile.toString + " not found")
243+
try Success(new OpenSSHKnownHosts(knownHostsFile))
244+
catch { case NonFatal(e) Failure(SSH.Error(s"Could not read $knownHostsFile", e)) }
245+
} else Failure(SSH.Error(knownHostsFile.toString + " not found"))
257246

258247
def forFingerprint(fingerprint: String): HostKeyVerifier =
259248
fingerprint match {

src/main/scala/com/decodified/scalassh/SSH.scala

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,26 @@
1616

1717
package com.decodified.scalassh
1818

19-
import scala.util.control.NonFatal
19+
import scala.util.control.{NoStackTrace, NonFatal}
20+
import scala.util.{Failure, Success, Try}
2021

2122
object SSH {
2223

2324
def apply[T](host: String, configProvider: HostConfigProvider = HostFileConfig())(
24-
body: SshClient Result[T]): Validated[T] =
25-
SshClient(host, configProvider).right.flatMap { client
26-
val result = {
27-
try body(client).result
28-
catch { case NonFatal(e) Left(e.toString) }
29-
}
30-
client.close()
31-
result
25+
body: SshClient Result[T]): Try[T] =
26+
SshClient(host, configProvider).flatMap { client
27+
try body(client).result
28+
catch { case NonFatal(e) Failure(e) } finally client.close()
3229
}
3330

34-
final case class Result[T](result: Validated[T])
31+
final case class Result[T](result: Try[T])
3532

3633
object Result extends LowerPriorityImplicits {
37-
implicit def fromValidated[T](value: Validated[T]) = Result(value)
34+
implicit def fromTry[T](value: Try[T]) = Result(value)
3835
}
3936
private[SSH] abstract class LowerPriorityImplicits {
40-
implicit def fromAny[T](value: T) = Result(Right(value))
37+
implicit def fromAny[T](value: T) = Result(Success(value))
4138
}
39+
40+
final case class Error(msg: String, cause: Throwable = null) extends RuntimeException(msg, cause) with NoStackTrace
4241
}

src/main/scala/com/decodified/scalassh/ScpTransferable.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,33 @@ import net.schmizz.sshj.sftp.SFTPClient
2121
import net.schmizz.sshj.xfer.scp.SCPFileTransfer
2222
import net.schmizz.sshj.xfer.TransferListener
2323
import net.schmizz.sshj.xfer.LoggingTransferListener
24+
import scala.util.Try
2425

2526
abstract class ScpTransferable {
2627
self: SshClient
2728

28-
def sftp[T](fun: SFTPClient T): Validated[T] =
29-
authenticatedClient.right.flatMap { client
29+
def sftp[T](fun: SFTPClient T): Try[T] =
30+
authenticatedClient.flatMap { client
3031
protect("SFTP client failed") {
3132
val ftpClient = client.newSFTPClient()
3233
try fun(ftpClient)
3334
finally ftpClient.close()
3435
}
3536
}
3637

37-
def fileTransfer[T](fun: SCPFileTransfer T)(implicit l: TransferListener = defaultListener): Validated[T] =
38-
authenticatedClient.right.flatMap { client
38+
def fileTransfer[T](fun: SCPFileTransfer T)(implicit l: TransferListener = defaultListener): Try[T] =
39+
authenticatedClient.flatMap { client
3940
protect("SCP file transfer failed") {
4041
val transfer = client.newSCPFileTransfer()
4142
transfer.setTransferListener(l)
4243
fun(transfer)
4344
}
4445
}
4546

46-
def upload(localPath: String, remotePath: String)(implicit l: TransferListener = defaultListener): Validated[Unit] =
47+
def upload(localPath: String, remotePath: String)(implicit l: TransferListener = defaultListener): Try[Unit] =
4748
fileTransfer(_.upload(localPath, remotePath))(l)
4849

49-
def download(remotePath: String, localPath: String)(implicit l: TransferListener = defaultListener): Validated[Unit] =
50+
def download(remotePath: String, localPath: String)(implicit l: TransferListener = defaultListener): Try[Unit] =
5051
fileTransfer(_.download(remotePath, localPath))(l)
5152

5253
private def defaultListener = new LoggingTransferListener(LoggerFactory.DEFAULT)

0 commit comments

Comments
 (0)