Skip to content

Commit

Permalink
Cleanup (#55)
Browse files Browse the repository at this point in the history
* cleanup polymorph F[_]

* rename package ray.fs2.ftp to fs2.ftp

* update doc

* cleanup test

* add javadoc
  • Loading branch information
regis-leray authored May 30, 2020
1 parent 9efc372 commit 63e300d
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 109 deletions.
48 changes: 34 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ libraryDependencies += "com.github.regis-leray" %% "fs2-ftp" % "<version>"

```scala
import cats.effect.IO
import ray.fs2.ftp.FtpClient._
import ray.fs2.ftp.FtpSettings._
import fs2.ftp.UnsecureFtp._
import fs2.ftp.FtpSettings._

// FTP
val settings = UnsecureFtpSettings("127.0.0.1", 21, FtpCredentials("foo", "bar"))
// FTP-SSL
val settings = UnsecureFtpSettings.secure("127.0.0.1", 21, FtpCredentials("foo", "bar"))
val settings = UnsecureFtpSettings.ssl("127.0.0.1", 21, FtpCredentials("foo", "bar"))

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

#### Password authentication
```scala
import ray.fs2.ftp.FtpClient._
import ray.fs2.ftp.FtpSettings._
import fs2.ftp.SecureFtp._
import fs2.ftp.FtpSettings._
import cats.effect.IO

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

#### private key authentication
```scala
import ray.fs2.ftp.FtpClient._
import ray.fs2.ftp.FtpSettings._
import fs2.ftp.SecureFtp._
import fs2.ftp.FtpSettings._
import java.nio.file.Paths._
import cats.effect.IO

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

## Required ContextShift

All function required an implicit ContextShit[IO].

Since all (s)ftp command are IO bound task , it will be executed on specific blocking executionContext
More information here https://typelevel.org/cats-effect/datatypes/contextshift.html


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

Here how to provide an ContextShift

* you can use the default one provided by `IOApp`
```scala
import cats.effect.{ExitCode, IO}
import fs2.ftp._
import fs2.ftp.FtpSettings._

object MyApp extends cats.effect.IOApp {
//by default an implicit ContextShit is available as an implicit variable
//by default an implicit ContextShift[IO] is available as an implicit variable
//F[_] Effect will be set as cats.effect.IO

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

//print all files/directories
def run(args: List[String]): IO[ExitCode] ={
connect(settings).use(_.ls("/mypath")
.evalTap(r => IO(println(r)))
.compile.drain)
.redeem(_ => ExitCode.Error, _ => ExitCode.Success)
}
}
```

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



## Support any commands ?
The underlying client is safely exposed and you have access to all possible ftp commands

```scala
import ray.fs2.ftp.FtpClient._
import ray.fs2.ftp.FtpSettings._
import cats.effect.IO
import fs2.ftp.SecureFtp._
import fs2.ftp.FtpSettings._

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

Expand All @@ -110,6 +123,13 @@ connect[IO](settings).use(
)
```

## Support any effect (IO, Monix, ZIO)

Since the library is following the paradigm polymorph `F[_]` (aka tagless final) we can create provide any
effect implementation as long your favourite library provide the type classes needed define by `cats-effect`

The library is by default bringing the dependency `cats-effect`

## How to release

1. How to create a key to signed artifact
Expand Down
14 changes: 14 additions & 0 deletions src/main/scala-2.11/fs2/ftp/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package fs2

import cats.effect.{ConcurrentEffect, ContextShift, Resource}
import cats.syntax.EitherSyntax
import fs2.ftp.{FtpClient, FtpSettings, SecureFtp, UnsecureFtp}
import fs2.ftp.FtpSettings.{SecureFtpSettings, UnsecureFtpSettings}

package object ftp extends EitherSyntax{
def connect[F[_]: ContextShift: ConcurrentEffect, A](settings: FtpSettings[A]): Resource[F, FtpClient[F, A]] =
settings match {
case s: UnsecureFtpSettings => UnsecureFtp.connect(s)
case s: SecureFtpSettings => SecureFtp.connect(s)
}
}
5 changes: 0 additions & 5 deletions src/main/scala-2.11/ray/fs2/ftp/package.scala

This file was deleted.

75 changes: 75 additions & 0 deletions src/main/scala/fs2/ftp/FtpClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package fs2.ftp

import fs2.Stream

/**
* Base trait of FtpClient which expose only safe methods
* `F[_]` represents the effect type will be use
* `A` underlying ftp client instance type
*/
trait FtpClient[F[_], +A] {

/**
* Retreive information of a specific ftp resource like a file or directory
* If the resource is not found we are returning an `Option.None`
*/
def stat(path: String): F[Option[FtpResource]]

/**
* Read a file from a specific location path
* If the resource is not found the operation will fail and fs2.Stream will Emit an IOException
* in the error channel use `recover/recoverWith` to catch it
*/
def readFile(path: String, chunkSize: Int = 2048): fs2.Stream[F, Byte]

/**
* Delete resource from a path
* If the resource is not found the operation will fail and Emit an IOException in the error channel
* use `recover/recoverWith` to catch it
*/
def rm(path: String): F[Unit]

/**
* Delete a directory from a path
*
* If the resource is not found the operation will fail and Emit an IOException in the error channel
* use `recover/recoverWith` to catch it
*/
def rmdir(path: String): F[Unit]

/**
* Create a directory from a path
*
* If the resource already exist the operation will fail and emit an IOException in the error channel
* use `recover/recoverWith` to catch it
*/
def mkdir(path: String): F[Unit]

/**
* List all directories and files of a specific directory, it don't support nested directories
* see `lsDescendant`
*
* If the directory dont exist it emits nothing,
*/
def ls(path: String): Stream[F, FtpResource]

/**
* List only files by traversing nested directories
* If the directory dont exist it emits nothing,
*/
def lsDescendant(path: String): Stream[F, FtpResource]

/**
* Upload data to a specific location path
* If operation failed it will emit an IOException in the error channel
* use `recover/recoverWith` to catch it
*/
def upload(path: String, source: fs2.Stream[F, Byte]): F[Unit]

/**
* Execute safely any operation supported by the underlying ftp client `A`
* If operation failed it will emit an IOException in the error channel
* use `recover/recoverWith` to catch it
*/
def execute[T](f: A => T): F[T]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ray.fs2.ftp
package fs2.ftp

import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermission._
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ray.fs2.ftp
package fs2.ftp

import java.net.Proxy
import java.nio.file.Path
Expand Down Expand Up @@ -91,7 +91,7 @@ object FtpSettings {
binary: Boolean,
passiveMode: Boolean,
proxy: Option[Proxy],
secure: Boolean,
ssl: Boolean,
timeOut: Int,
connectTimeOut: Int
) extends FtpSettings[JFTPClient]
Expand All @@ -101,7 +101,7 @@ object FtpSettings {
def apply(host: String, port: Int, creds: FtpCredentials): UnsecureFtpSettings =
new UnsecureFtpSettings(host, port, creds, true, true, None, false, 0, 0)

def secure(host: String, port: Int, creds: FtpCredentials): UnsecureFtpSettings =
def ssl(host: String, port: Int, creds: FtpCredentials): UnsecureFtpSettings =
new UnsecureFtpSettings(host, port, creds, true, true, None, true, 0, 0)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
package ray.fs2.ftp
package fs2.ftp

import java.io._

import cats.effect.{ Blocker, ConcurrentEffect, ContextShift, Resource }
import cats.implicits._
import cats.effect.{ Blocker, ConcurrentEffect, ContextShift, Resource, Sync }
import cats.syntax.applicativeError._
import cats.syntax.flatMap._
import fs2.Stream
import fs2.Stream._
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.sftp.{ OpenMode, Response, SFTPException, SFTPClient => JSFTPClient }
import net.schmizz.sshj.transport.verification.PromiscuousVerifier
import net.schmizz.sshj.userauth.password.PasswordUtils
import ray.fs2.ftp.FtpSettings.{ KeyFileSftpIdentity, RawKeySftpIdentity, SecureFtpSettings, SftpIdentity }
import fs2.ftp.FtpSettings.{ KeyFileSftpIdentity, RawKeySftpIdentity, SecureFtpSettings, SftpIdentity }

import scala.jdk.CollectionConverters._

final private class SFtp[F[_]](unsafeClient: JSFTPClient, blocker: Blocker)(
implicit CE: ConcurrentEffect[F],
CS: ContextShift[F]
) extends FtpClient[F, JSFTPClient] {
final private class SecureFtp[F[_]: ConcurrentEffect: ContextShift](unsafeClient: SecureFtp.Client, blocker: Blocker)
extends FtpClient[F, JSFTPClient] {

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

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

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

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

object SFtp {
object SecureFtp {

def connect[F[_]](
type Client = JSFTPClient

def connect[F[_]: ContextShift: ConcurrentEffect](
settings: SecureFtpSettings
)(implicit CS: ContextShift[F], CE: ConcurrentEffect[F]): Resource[F, FtpClient[F, JSFTPClient]] =
): Resource[F, FtpClient[F, SecureFtp.Client]] =
for {
ssh <- Resource.liftF(CE.delay(new SSHClient(settings.sshConfig)))
ssh <- Resource.liftF(Sync[F].delay(new SSHClient(settings.sshConfig)))

blocker <- Blocker[F]
r <- Resource.make[F, FtpClient[F, JSFTPClient]](CE.delay {
r <- Resource.make[F, FtpClient[F, JSFTPClient]](Sync[F].delay {
import settings._

if (!strictHostKeyChecking)
Expand All @@ -119,9 +120,12 @@ object SFtp {
setIdentity(_, credentials.username)(ssh)
)

new SFtp(ssh.newSFTPClient(), blocker)
new SecureFtp(ssh.newSFTPClient(), blocker)
})(client =>
client.execute(_.close()).attempt.flatMap(_ => if (ssh.isConnected) CE.delay(ssh.disconnect()) else CE.unit)
client
.execute(_.close())
.attempt
.flatMap(_ => if (ssh.isConnected) Sync[F].delay(ssh.disconnect()) else Sync[F].unit)
)
} yield r

Expand Down
Loading

0 comments on commit 63e300d

Please sign in to comment.