Skip to content

Commit 63e300d

Browse files
authored
Cleanup (#55)
* cleanup polymorph F[_] * rename package ray.fs2.ftp to fs2.ftp * update doc * cleanup test * add javadoc
1 parent 9efc372 commit 63e300d

File tree

15 files changed

+210
-109
lines changed

15 files changed

+210
-109
lines changed

README.md

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ libraryDependencies += "com.github.regis-leray" %% "fs2-ftp" % "<version>"
2121

2222
```scala
2323
import cats.effect.IO
24-
import ray.fs2.ftp.FtpClient._
25-
import ray.fs2.ftp.FtpSettings._
24+
import fs2.ftp.UnsecureFtp._
25+
import fs2.ftp.FtpSettings._
2626

2727
// FTP
2828
val settings = UnsecureFtpSettings("127.0.0.1", 21, FtpCredentials("foo", "bar"))
2929
// FTP-SSL
30-
val settings = UnsecureFtpSettings.secure("127.0.0.1", 21, FtpCredentials("foo", "bar"))
30+
val settings = UnsecureFtpSettings.ssl("127.0.0.1", 21, FtpCredentials("foo", "bar"))
3131

3232
connect[IO](settings).use{
3333
_.ls("/").compile.toList
@@ -38,8 +38,8 @@ connect[IO](settings).use{
3838

3939
#### Password authentication
4040
```scala
41-
import ray.fs2.ftp.FtpClient._
42-
import ray.fs2.ftp.FtpSettings._
41+
import fs2.ftp.SecureFtp._
42+
import fs2.ftp.FtpSettings._
4343
import cats.effect.IO
4444

4545
val settings = SecureFtpSettings("127.0.0.1", 22, FtpCredentials("foo", "bar"))
@@ -51,8 +51,8 @@ connect[IO](settings).use(
5151

5252
#### private key authentication
5353
```scala
54-
import ray.fs2.ftp.FtpClient._
55-
import ray.fs2.ftp.FtpSettings._
54+
import fs2.ftp.SecureFtp._
55+
import fs2.ftp.FtpSettings._
5656
import java.nio.file.Paths._
5757
import cats.effect.IO
5858

@@ -69,19 +69,33 @@ connect[IO](settings).use(
6969

7070
## Required ContextShift
7171

72-
All function required an implicit ContextShit[IO].
73-
7472
Since all (s)ftp command are IO bound task , it will be executed on specific blocking executionContext
7573
More information here https://typelevel.org/cats-effect/datatypes/contextshift.html
7674

7775

76+
Create a `FtpClient[F[_], +A]` by using `connect()` it is required to provide an implicit `ContextShift[F]`
7877

7978
Here how to provide an ContextShift
8079

8180
* you can use the default one provided by `IOApp`
8281
```scala
82+
import cats.effect.{ExitCode, IO}
83+
import fs2.ftp._
84+
import fs2.ftp.FtpSettings._
85+
8386
object MyApp extends cats.effect.IOApp {
84-
//by default an implicit ContextShit is available as an implicit variable
87+
//by default an implicit ContextShift[IO] is available as an implicit variable
88+
//F[_] Effect will be set as cats.effect.IO
89+
90+
val settings = SecureFtpSettings("127.0.0.1", 22, FtpCredentials("foo", "bar"))
91+
92+
//print all files/directories
93+
def run(args: List[String]): IO[ExitCode] ={
94+
connect(settings).use(_.ls("/mypath")
95+
.evalTap(r => IO(println(r)))
96+
.compile.drain)
97+
.redeem(_ => ExitCode.Error, _ => ExitCode.Success)
98+
}
8599
}
86100
```
87101

@@ -94,14 +108,13 @@ implicit val blockingIO = ExecutionContext.fromExecutor(Executors.newCachedThrea
94108
implicit val cs: ContextShift[IO] = IO.contextShift(blockingIO)
95109
```
96110

97-
98-
99111
## Support any commands ?
100112
The underlying client is safely exposed and you have access to all possible ftp commands
101113

102114
```scala
103-
import ray.fs2.ftp.FtpClient._
104-
import ray.fs2.ftp.FtpSettings._
115+
import cats.effect.IO
116+
import fs2.ftp.SecureFtp._
117+
import fs2.ftp.FtpSettings._
105118

106119
val settings = SecureFtpSettings("127.0.0.1", 22, FtpCredentials("foo", "bar"))
107120

@@ -110,6 +123,13 @@ connect[IO](settings).use(
110123
)
111124
```
112125

126+
## Support any effect (IO, Monix, ZIO)
127+
128+
Since the library is following the paradigm polymorph `F[_]` (aka tagless final) we can create provide any
129+
effect implementation as long your favourite library provide the type classes needed define by `cats-effect`
130+
131+
The library is by default bringing the dependency `cats-effect`
132+
113133
## How to release
114134

115135
1. How to create a key to signed artifact
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package fs2
2+
3+
import cats.effect.{ConcurrentEffect, ContextShift, Resource}
4+
import cats.syntax.EitherSyntax
5+
import fs2.ftp.{FtpClient, FtpSettings, SecureFtp, UnsecureFtp}
6+
import fs2.ftp.FtpSettings.{SecureFtpSettings, UnsecureFtpSettings}
7+
8+
package object ftp extends EitherSyntax{
9+
def connect[F[_]: ContextShift: ConcurrentEffect, A](settings: FtpSettings[A]): Resource[F, FtpClient[F, A]] =
10+
settings match {
11+
case s: UnsecureFtpSettings => UnsecureFtp.connect(s)
12+
case s: SecureFtpSettings => SecureFtp.connect(s)
13+
}
14+
}

src/main/scala-2.11/ray/fs2/ftp/package.scala

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package fs2.ftp
2+
3+
import fs2.Stream
4+
5+
/**
6+
* Base trait of FtpClient which expose only safe methods
7+
* `F[_]` represents the effect type will be use
8+
* `A` underlying ftp client instance type
9+
*/
10+
trait FtpClient[F[_], +A] {
11+
12+
/**
13+
* Retreive information of a specific ftp resource like a file or directory
14+
* If the resource is not found we are returning an `Option.None`
15+
*/
16+
def stat(path: String): F[Option[FtpResource]]
17+
18+
/**
19+
* Read a file from a specific location path
20+
* If the resource is not found the operation will fail and fs2.Stream will Emit an IOException
21+
* in the error channel use `recover/recoverWith` to catch it
22+
*/
23+
def readFile(path: String, chunkSize: Int = 2048): fs2.Stream[F, Byte]
24+
25+
/**
26+
* Delete resource from a path
27+
* If the resource is not found the operation will fail and Emit an IOException in the error channel
28+
* use `recover/recoverWith` to catch it
29+
*/
30+
def rm(path: String): F[Unit]
31+
32+
/**
33+
* Delete a directory from a path
34+
*
35+
* If the resource is not found the operation will fail and Emit an IOException in the error channel
36+
* use `recover/recoverWith` to catch it
37+
*/
38+
def rmdir(path: String): F[Unit]
39+
40+
/**
41+
* Create a directory from a path
42+
*
43+
* If the resource already exist the operation will fail and emit an IOException in the error channel
44+
* use `recover/recoverWith` to catch it
45+
*/
46+
def mkdir(path: String): F[Unit]
47+
48+
/**
49+
* List all directories and files of a specific directory, it don't support nested directories
50+
* see `lsDescendant`
51+
*
52+
* If the directory dont exist it emits nothing,
53+
*/
54+
def ls(path: String): Stream[F, FtpResource]
55+
56+
/**
57+
* List only files by traversing nested directories
58+
* If the directory dont exist it emits nothing,
59+
*/
60+
def lsDescendant(path: String): Stream[F, FtpResource]
61+
62+
/**
63+
* Upload data to a specific location path
64+
* If operation failed it will emit an IOException in the error channel
65+
* use `recover/recoverWith` to catch it
66+
*/
67+
def upload(path: String, source: fs2.Stream[F, Byte]): F[Unit]
68+
69+
/**
70+
* Execute safely any operation supported by the underlying ftp client `A`
71+
* If operation failed it will emit an IOException in the error channel
72+
* use `recover/recoverWith` to catch it
73+
*/
74+
def execute[T](f: A => T): F[T]
75+
}

src/main/scala/ray/fs2/ftp/FtpResource.scala renamed to src/main/scala/fs2/ftp/FtpResource.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package ray.fs2.ftp
1+
package fs2.ftp
22

33
import java.nio.file.attribute.PosixFilePermission
44
import java.nio.file.attribute.PosixFilePermission._

src/main/scala/ray/fs2/ftp/FtpSettings.scala renamed to src/main/scala/fs2/ftp/FtpSettings.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package ray.fs2.ftp
1+
package fs2.ftp
22

33
import java.net.Proxy
44
import java.nio.file.Path
@@ -91,7 +91,7 @@ object FtpSettings {
9191
binary: Boolean,
9292
passiveMode: Boolean,
9393
proxy: Option[Proxy],
94-
secure: Boolean,
94+
ssl: Boolean,
9595
timeOut: Int,
9696
connectTimeOut: Int
9797
) extends FtpSettings[JFTPClient]
@@ -101,7 +101,7 @@ object FtpSettings {
101101
def apply(host: String, port: Int, creds: FtpCredentials): UnsecureFtpSettings =
102102
new UnsecureFtpSettings(host, port, creds, true, true, None, false, 0, 0)
103103

104-
def secure(host: String, port: Int, creds: FtpCredentials): UnsecureFtpSettings =
104+
def ssl(host: String, port: Int, creds: FtpCredentials): UnsecureFtpSettings =
105105
new UnsecureFtpSettings(host, port, creds, true, true, None, true, 0, 0)
106106
}
107107

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
package ray.fs2.ftp
1+
package fs2.ftp
22

33
import java.io._
44

5-
import cats.effect.{ Blocker, ConcurrentEffect, ContextShift, Resource }
6-
import cats.implicits._
5+
import cats.effect.{ Blocker, ConcurrentEffect, ContextShift, Resource, Sync }
6+
import cats.syntax.applicativeError._
7+
import cats.syntax.flatMap._
78
import fs2.Stream
89
import fs2.Stream._
910
import net.schmizz.sshj.SSHClient
1011
import net.schmizz.sshj.sftp.{ OpenMode, Response, SFTPException, SFTPClient => JSFTPClient }
1112
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
1213
import net.schmizz.sshj.userauth.password.PasswordUtils
13-
import ray.fs2.ftp.FtpSettings.{ KeyFileSftpIdentity, RawKeySftpIdentity, SecureFtpSettings, SftpIdentity }
14+
import fs2.ftp.FtpSettings.{ KeyFileSftpIdentity, RawKeySftpIdentity, SecureFtpSettings, SftpIdentity }
1415

1516
import scala.jdk.CollectionConverters._
1617

17-
final private class SFtp[F[_]](unsafeClient: JSFTPClient, blocker: Blocker)(
18-
implicit CE: ConcurrentEffect[F],
19-
CS: ContextShift[F]
20-
) extends FtpClient[F, JSFTPClient] {
18+
final private class SecureFtp[F[_]: ConcurrentEffect: ContextShift](unsafeClient: SecureFtp.Client, blocker: Blocker)
19+
extends FtpClient[F, JSFTPClient] {
2120

2221
def ls(path: String): fs2.Stream[F, FtpResource] =
2322
fs2.Stream
@@ -57,7 +56,7 @@ final private class SFtp[F[_]](unsafeClient: JSFTPClient, blocker: Blocker)(
5756
}
5857
}
5958

60-
input <- fs2.io.readInputStream(CE.pure(is), chunkSize, blocker)
59+
input <- fs2.io.readInputStream(Sync[F].pure(is), chunkSize, blocker)
6160
} yield input
6261

6362
def rm(path: String): F[Unit] =
@@ -85,23 +84,25 @@ final private class SFtp[F[_]](unsafeClient: JSFTPClient, blocker: Blocker)(
8584
super.close()
8685
}
8786
}
88-
_ <- source.through(fs2.io.writeOutputStream(CE.pure(os), blocker))
87+
_ <- source.through(fs2.io.writeOutputStream(Sync[F].pure(os), blocker))
8988
} yield ()).compile.drain
9089

9190
def execute[T](f: JSFTPClient => T): F[T] =
9291
blocker.delay[F, T](f(unsafeClient))
9392
}
9493

95-
object SFtp {
94+
object SecureFtp {
9695

97-
def connect[F[_]](
96+
type Client = JSFTPClient
97+
98+
def connect[F[_]: ContextShift: ConcurrentEffect](
9899
settings: SecureFtpSettings
99-
)(implicit CS: ContextShift[F], CE: ConcurrentEffect[F]): Resource[F, FtpClient[F, JSFTPClient]] =
100+
): Resource[F, FtpClient[F, SecureFtp.Client]] =
100101
for {
101-
ssh <- Resource.liftF(CE.delay(new SSHClient(settings.sshConfig)))
102+
ssh <- Resource.liftF(Sync[F].delay(new SSHClient(settings.sshConfig)))
102103

103104
blocker <- Blocker[F]
104-
r <- Resource.make[F, FtpClient[F, JSFTPClient]](CE.delay {
105+
r <- Resource.make[F, FtpClient[F, JSFTPClient]](Sync[F].delay {
105106
import settings._
106107

107108
if (!strictHostKeyChecking)
@@ -119,9 +120,12 @@ object SFtp {
119120
setIdentity(_, credentials.username)(ssh)
120121
)
121122

122-
new SFtp(ssh.newSFTPClient(), blocker)
123+
new SecureFtp(ssh.newSFTPClient(), blocker)
123124
})(client =>
124-
client.execute(_.close()).attempt.flatMap(_ => if (ssh.isConnected) CE.delay(ssh.disconnect()) else CE.unit)
125+
client
126+
.execute(_.close())
127+
.attempt
128+
.flatMap(_ => if (ssh.isConnected) Sync[F].delay(ssh.disconnect()) else Sync[F].unit)
125129
)
126130
} yield r
127131

0 commit comments

Comments
 (0)