diff --git a/zio-ftp/src/main/scala/zio/ftp/FtpAccessors.scala b/zio-ftp/src/main/scala/zio/ftp/FtpAccessors.scala index 3d1b9736..1d360bbe 100644 --- a/zio-ftp/src/main/scala/zio/ftp/FtpAccessors.scala +++ b/zio-ftp/src/main/scala/zio/ftp/FtpAccessors.scala @@ -75,4 +75,12 @@ trait FtpAccessors[+A] { */ def upload[R](path: String, source: ZStream[R, Throwable, Byte]): ZIO[R with Scope, IOException, Unit] + /** + * Renames a file/directory. If the operation failed, an error will be emitted + * + * @param oldPath absolute path of the file/directory to rename + * @param newPath absolute path of the file/directory destination. + */ + def rename(oldPath: String, newPath: String): ZIO[Any, IOException, Unit] + } diff --git a/zio-ftp/src/main/scala/zio/ftp/SecureFtp.scala b/zio-ftp/src/main/scala/zio/ftp/SecureFtp.scala index d07ae83f..9d4ee873 100644 --- a/zio-ftp/src/main/scala/zio/ftp/SecureFtp.scala +++ b/zio-ftp/src/main/scala/zio/ftp/SecureFtp.scala @@ -78,6 +78,9 @@ final private class SecureFtp(unsafeClient: Client) extends FtpAccessors[Client] .flatMap(ZStream.fromIterable(_)) .map(FtpResource.fromResource) + def rename(oldPath: String, newPath: String): ZIO[Any, IOException, Unit] = + execute(_.rename(oldPath, newPath)) + def lsDescendant(path: String): ZStream[Any, IOException, FtpResource] = ZStream .fromZIO( diff --git a/zio-ftp/src/main/scala/zio/ftp/TestFtp.scala b/zio-ftp/src/main/scala/zio/ftp/TestFtp.scala index 4aac8ca3..1d4785cf 100644 --- a/zio-ftp/src/main/scala/zio/ftp/TestFtp.scala +++ b/zio-ftp/src/main/scala/zio/ftp/TestFtp.scala @@ -111,5 +111,10 @@ object TestFtp { .refineToOrDie[IOException] .catchAll(err => ZIO.fail(new IOException(s"Path is invalid. Cannot upload data to : $path", err))) } + + override def rename(oldPath: String, newPath: String): ZIO[Any, IOException, Unit] = + Files + .move(root / ZPath(oldPath).elements.mkString("/"), root / ZPath(newPath).elements.mkString("/")) + .catchAll(err => ZIO.fail(new IOException(s"Path is invalid. Cannot rename $oldPath to $newPath", err))) } } diff --git a/zio-ftp/src/main/scala/zio/ftp/UnsecureFtp.scala b/zio-ftp/src/main/scala/zio/ftp/UnsecureFtp.scala index bbdf2068..38d7524a 100644 --- a/zio-ftp/src/main/scala/zio/ftp/UnsecureFtp.scala +++ b/zio-ftp/src/main/scala/zio/ftp/UnsecureFtp.scala @@ -88,6 +88,11 @@ final private class UnsecureFtp(unsafeClient: Client) extends FtpAccessors[Clien .unit ) + def rename(oldPath: String, newPath: String): ZIO[Any, IOException, Unit] = + execute(_.rename(oldPath, newPath)) + .filterOrFail(identity)(InvalidPathError(s"Path is invalid. Cannot rename $oldPath to $newPath")) + .unit + override def execute[T](f: Client => T): ZIO[Any, IOException, T] = attemptBlockingIO(f(unsafeClient)) } diff --git a/zio-ftp/src/main/scala/zio/ftp/package.scala b/zio-ftp/src/main/scala/zio/ftp/package.scala index 0e258ce8..8b682c00 100644 --- a/zio-ftp/src/main/scala/zio/ftp/package.scala +++ b/zio-ftp/src/main/scala/zio/ftp/package.scala @@ -63,6 +63,9 @@ package object ftp { def readFile(path: String, chunkSize: Int = 2048): ZStream[Ftp, IOException, Byte] = ZStream.serviceWithStream(_.readFile(path, chunkSize)) + + def rename(oldPath: String, newPath: String): ZIO[Ftp, Exception, Unit] = + ZIO.serviceWithZIO(_.rename(oldPath, newPath)) } object SFtp { @@ -99,6 +102,9 @@ package object ftp { def readFile(path: String, chunkSize: Int = 2048): ZStream[SFtp, IOException, Byte] = ZStream.serviceWithStream(_.readFile(path, chunkSize)) + + def rename(oldPath: String, newPath: String): ZIO[SFtp, Exception, Unit] = + ZIO.serviceWithZIO(_.rename(oldPath, newPath)) } object StubFtp { @@ -135,6 +141,9 @@ package object ftp { def readFile(path: String, chunkSize: Int = 2048): ZStream[StubFtp, IOException, Byte] = ZStream.serviceWithStream(_.readFile(path, chunkSize)) + + def rename(oldPath: String, newPath: String): ZIO[StubFtp, Exception, Unit] = + ZIO.serviceWithZIO(_.rename(oldPath, newPath)) } def unsecure(settings: UnsecureFtpSettings): ZLayer[Scope, ConnectionError, Ftp] = diff --git a/zio-ftp/src/test/scala/zio/ftp/SecureFtpSpec.scala b/zio-ftp/src/test/scala/zio/ftp/SecureFtpSpec.scala index 30049dc1..4493e208 100644 --- a/zio-ftp/src/test/scala/zio/ftp/SecureFtpSpec.scala +++ b/zio-ftp/src/test/scala/zio/ftp/SecureFtpSpec.scala @@ -1,9 +1,6 @@ package zio.ftp import zio.ZIO.{ acquireRelease, attempt, attemptBlockingIO } - -import java.net.{ InetSocketAddress, Proxy } -import java.nio.file.{ Files, Paths } import zio._ import zio.ftp.SFtp._ import zio.stream.ZPipeline.utf8Decode @@ -11,6 +8,8 @@ import zio.stream.ZStream import zio.test.Assertion._ import zio.test._ +import java.net.{ InetSocketAddress, Proxy } +import java.nio.file.{ Files, Paths } import scala.io.Source object Load @@ -197,6 +196,24 @@ object SecureFtpSpec extends ZIOSpecDefault { for { version <- execute(_.version()) } yield assert(version.toString)(isNonEmptyString) + }, + test("rename valid path") { + val oldPath = home.resolve("dir1/to-rename.txt") + val newPath = home.resolve("dir1/to-rename-destination.txt") + Files.createFile(oldPath) + + ( + for { + success <- rename("/dir1/to-rename.txt", "/dir1/to-rename-destination.txt").as(true) + oldFileExists <- attempt(Files.exists(oldPath)) + newFileExists <- attempt(Files.exists(newPath)) + } yield assertTrue(success && !oldFileExists && newFileExists) + ) <* attempt(Files.delete(newPath)) + }, + test("rename fail when invalid path") { + for { + invalid <- rename("/dont-exist", "dont-exist-destination").flip.map(_.getMessage) + } yield assertTrue(invalid == "No such file") } ).provideCustomLayerShared(sftp) } diff --git a/zio-ftp/src/test/scala/zio/ftp/StubFtpSpec.scala b/zio-ftp/src/test/scala/zio/ftp/StubFtpSpec.scala index 41844570..9e42b205 100644 --- a/zio-ftp/src/test/scala/zio/ftp/StubFtpSpec.scala +++ b/zio-ftp/src/test/scala/zio/ftp/StubFtpSpec.scala @@ -137,6 +137,23 @@ object StubFtpSpec extends ZIOSpecDefault { failure <- upload("/dont-exist/hello-world.txt", data).flip.map(_.getMessage) } yield assertTrue(failure == "Path is invalid. Cannot upload data to : /dont-exist/hello-world.txt") + }, + test("rename a file") { + val oldPath = home / "to-rename.txt" + val newPath = home / "to-rename-destination.txt" + + (for { + _ <- Files.createFile(oldPath) + success <- rename("/to-rename.txt", "/to-rename-destination.txt").as(true) + + oldFileExists <- Files.exists(oldPath) + newFileExists <- Files.exists(newPath) + } yield assertTrue(success && !oldFileExists && newFileExists)) <* Files.delete(newPath) + }, + test("rename a file fails when old path doesn't exist") { + for { + failure <- rename("/dont-exist.txt", "/dont-exist-destination.txt").flip.map(_.getMessage) + } yield assertTrue(failure == "Path is invalid. Cannot rename /dont-exist.txt to /dont-exist-destination.txt") } ).provideCustomLayerShared(stubFtp) } diff --git a/zio-ftp/src/test/scala/zio/ftp/UnsecureFtpSpec.scala b/zio-ftp/src/test/scala/zio/ftp/UnsecureFtpSpec.scala index 1df6835d..6c86707f 100644 --- a/zio-ftp/src/test/scala/zio/ftp/UnsecureFtpSpec.scala +++ b/zio-ftp/src/test/scala/zio/ftp/UnsecureFtpSpec.scala @@ -181,6 +181,23 @@ object FtpSuite { failure <- upload("/dont-exist/hello-world.txt", data).flip.map(_.getMessage) } yield assertTrue(failure == "Path is invalid. Cannot upload data to : /dont-exist/hello-world.txt") }, + test("rename valid path") { + val oldPath = home / "to-rename.txt" + val newPath = home / "to-rename-destination.txt" + + (for { + _ <- Files.createFile(oldPath) + success <- rename("/to-rename.txt", "/to-rename-destination.txt").as(true) + + oldFileExists <- Files.exists(oldPath) + newFileExists <- Files.exists(newPath) + } yield assertTrue(success && !oldFileExists && newFileExists)) <* Files.delete(newPath) + }, + test("rename fail when invalid path") { + for { + invalid <- rename("/dont-exist", "/dont-exist-destination").flip.map(_.getMessage) + } yield assertTrue(invalid == "Path is invalid. Cannot rename /dont-exist to /dont-exist-destination") + }, test("call noOp underlying client") { for { noOp <- execute(_.sendNoOp())