diff --git a/core/src/main/scala/io/chrisdavenport/shellfish/FilesOs.scala b/core/src/main/scala/io/chrisdavenport/shellfish/FilesOs.scala index 1666735..2d29c1c 100644 --- a/core/src/main/scala/io/chrisdavenport/shellfish/FilesOs.scala +++ b/core/src/main/scala/io/chrisdavenport/shellfish/FilesOs.scala @@ -22,7 +22,7 @@ package io.chrisdavenport.shellfish import cats.syntax.all.* -import cats.effect.{IO, Resource} +import cats.effect.IO import fs2.{Stream, Chunk} import fs2.io.file.* @@ -62,7 +62,7 @@ object FilesOs { * @return * The file loaded in memory as a String */ - def readWithCharset(path: Path, charset: Charset): IO[String] = + def read(path: Path, charset: Charset): IO[String] = files .readAll(path) .through(fs2.text.decodeWithCharset(charset)) @@ -106,16 +106,6 @@ object FilesOs { def readAs[A: Codec](path: Path): IO[A] = readBytes(path).map(bytes => Codec[A].decodeValue(bytes.bits).require) - /** - * The function reads the contents of the file at the path and returns it as a - * Stream of Bytes, useful when working with large files. - * @param path - * The path to read from - * @return - * A Stream of Bytes - */ - def readStream(path: Path): Stream[IO, Byte] = files.readAll(path) - // Write operations: /** @@ -148,7 +138,7 @@ object FilesOs { * @param charset * The charset to use to encode the file */ - def writeWithCharset( + def write( path: Path, contents: String, charset: Charset @@ -243,7 +233,7 @@ object FilesOs { * @param charset * The charset to use to encode the contents */ - def appendWithCharset( + def append( path: Path, contents: String, charset: Charset @@ -626,7 +616,7 @@ object FilesOs { def lineSeparator: String = files.lineSeparator /** Gets the contents of the specified directory. */ - def list(path: Path): Stream[IO, Path] = files.list(path) + def list(path: Path): IO[List[Path]] = files.list(path).compile.toList /** * Moves the source to the target, failing if source does not exist or the @@ -644,22 +634,6 @@ object FilesOs { def move(source: Path, target: Path, flags: CopyFlags): IO[Unit] = files.move(source, target, flags) - /** - * Creates a `FileHandle` for the file at the supplied `Path`. The supplied - * flags indicate the mode used when opening the file (e.g. read, write, - * append) as well as the ability to specify additional options (e.g. - * automatic deletion at process exit). - */ - def open(path: Path, flags: Flags): Resource[IO, FileHandle[IO]] = - files.open(path, flags) - - /** - * Returns a `ReadCursor` for the specified path, using the supplied flags - * when opening the file. - */ - def readCursor(path: Path, flags: Flags): Resource[IO, ReadCursor[IO]] = - files.readCursor(path, flags) - /** * Returns the real path i.e. the actual location of `path`. The precise * definition of this method is implementation dependent but in general it @@ -705,14 +679,16 @@ object FilesOs { def size(path: Path): IO[Long] = files.size(path) /** - * Creates a temporary file and deletes it upon finalization of the returned - * resource. + * Creates a temporary file and deletes it at the end of the use of it. */ - def tempFile: Resource[IO, Path] = files.tempFile(None, "", ".tmp", None) + def withTempFile[A](use: Path => IO[A]): IO[A] = + files.tempFile.use(use) /** - * Creates a temporary file and deletes it upon finalization of the returned - * resource. + * Creates a temporary file and deletes it at the end of the use of it. + * + * @tparam A + * the type of the result computation * * @param dir * the directory which the temporary file will be created in. Pass in None @@ -724,24 +700,27 @@ object FilesOs { * @param permissions * permissions to set on the created file * @return - * a resource containing the path of the temporary file + * The result of the computation after using the temporary file */ - def tempFile( + def withTempFile[A]( dir: Option[Path], prefix: String, suffix: String, permissions: Permissions - ): Resource[IO, Path] = files.tempFile(dir, prefix, suffix, permissions.some) + )(use: Path => IO[A]): IO[A] = + files.tempFile(dir, prefix, suffix, permissions.some).use(use) /** - * Creates a temporary directory and deletes it upon finalization of the - * returned resource. + * Creates a temporary directory and deletes it at the end of the use of it. */ - def tempDirectory: Resource[IO, Path] = files.tempDirectory(None, "", None) + def withTempDirectory[A](use: Path => IO[A]): IO[A] = + files.tempDirectory.use(use) /** - * Creates a temporary directory and deletes it upon finalization of the - * returned resource. + * Creates a temporary directory and deletes it at the end of the use of it. + * + * @tparam A + * the type of the result computation * * @param dir * the directory which the temporary directory will be created in. Pass in @@ -751,13 +730,14 @@ object FilesOs { * @param permissions * permissions to set on the created file * @return - * a resource containing the path of the temporary directory + * the result of the computation after using the temporary directory */ - def tempDirectory( + def withTempDirectory[A]( dir: Option[Path], prefix: String, permissions: Permissions - ): Resource[IO, Path] = files.tempDirectory(dir, prefix, permissions.some) + )(use: Path => IO[A]): IO[A] = + files.tempDirectory(dir, prefix, permissions.some).use(use) /** User's home directory */ def userHome: IO[Path] = files.userHome diff --git a/core/src/main/scala/io/chrisdavenport/shellfish/syntax/path/package.scala b/core/src/main/scala/io/chrisdavenport/shellfish/syntax/path/package.scala index 312f4dc..4bc3a30 100644 --- a/core/src/main/scala/io/chrisdavenport/shellfish/syntax/path/package.scala +++ b/core/src/main/scala/io/chrisdavenport/shellfish/syntax/path/package.scala @@ -22,10 +22,8 @@ package io.chrisdavenport.shellfish package syntax -import cats.syntax.all.* -import cats.effect.{IO, Resource} +import cats.effect.IO -import fs2.Stream import fs2.io.file.* import scodec.bits.ByteVector @@ -62,8 +60,8 @@ package object path { * @return * The file loaded in memory as a String */ - def readWithCharset(charset: Charset): IO[String] = - FilesOs.readWithCharset(path, charset) + def read(charset: Charset): IO[String] = + FilesOs.read(path, charset) /** * Reads the contents of the file at the path and returns it as a @@ -100,16 +98,6 @@ package object path { */ def readAs[A: Codec]: IO[A] = FilesOs.readAs(path) - /** - * The function reads the contents of the file at the path and returns it as - * a Stream of Bytes, useful when working with large files. - * @param path - * The path to read from - * @return - * A Stream of Bytes - */ - def readStream: fs2.Stream[IO, Byte] = FilesOs.readStream(path) - // Write operations: /** @@ -136,8 +124,8 @@ package object path { * @param charset * The charset to use to encode the file */ - def writeWithCharset(contents: String, charset: Charset): IO[Unit] = - FilesOs.writeWithCharset(path, contents, charset) + def write(contents: String, charset: Charset): IO[Unit] = + FilesOs.write(path, contents, charset) /** * This function overwrites the contents of the file at the path with the @@ -205,11 +193,11 @@ package object path { * @param charset * The charset to use to encode the contents */ - def appendWithCharset( + def append( contents: String, charset: Charset ): IO[Unit] = - FilesOs.appendWithCharset(path, contents, charset) + FilesOs.append(path, contents, charset) /** * Similar to `write`, but appends to the file instead of overwriting it. @@ -485,7 +473,7 @@ package object path { def isSameFile(path2: Path): IO[Boolean] = FilesOs.isSameFile(path, path2) /** Gets the contents of the specified directory. */ - def list: Stream[IO, Path] = FilesOs.list(path) + def list: IO[List[Path]] = FilesOs.list(path) /** * Moves the source to the target, failing if source does not exist or the @@ -502,17 +490,6 @@ package object path { def move(target: Path, flags: CopyFlags): IO[Unit] = FilesOs.move(path, target, flags) - /** Creates a `FileHandle` for the file at the supplied `Path`. */ - def open(flags: Flags): Resource[IO, FileHandle[IO]] = - FilesOs.open(path, flags) - - /** - * Returns a `ReadCursor` for the specified path, using the supplied flags - * when opening the file. - */ - def readCursor(flags: Flags): Resource[IO, ReadCursor[IO]] = - FilesOs.readCursor(path, flags) - // Real Path /** Returns the real path i.e. the actual location of `path`. */ def realPath: IO[Path] = FilesOs.realPath(path) @@ -617,14 +594,16 @@ package object path { def lineSeparator: String = FilesOs.lineSeparator /** - * Creates a temporary file and deletes it upon finalization of the returned - * resource. + * Creates a temporary file and deletes it at the end of the use of it. */ - def tempFile: Resource[IO, Path] = files.tempFile(None, "", ".tmp", None) + def withTempFile[A](use: Path => IO[A]): IO[A] = + FilesOs.withTempFile(use) /** - * Creates a temporary file and deletes it upon finalization of the returned - * resource. + * Creates a temporary file and deletes it at the end of the use of it. + * + * @tparam A + * the type of the result computation * * @param dir * the directory which the temporary file will be created in. Pass in None @@ -636,24 +615,27 @@ package object path { * @param permissions * permissions to set on the created file * @return - * a resource containing the path of the temporary file + * The result of the computation after using the temporary file */ - def tempFile( + def withTempFile[A]( dir: Option[Path], prefix: String, suffix: String, permissions: Permissions - ): Resource[IO, Path] = files.tempFile(dir, prefix, suffix, permissions.some) + )(use: Path => IO[A]): IO[A] = + FilesOs.withTempFile(dir, prefix, suffix, permissions)(use) /** - * Creates a temporary directory and deletes it upon finalization of the - * returned resource. + * Creates a temporary directory and deletes it at the end of the use of it. */ - def tempDirectory: Resource[IO, Path] = files.tempDirectory(None, "", None) + def withTempDirectory[A](use: Path => IO[A]): IO[A] = + FilesOs.withTempDirectory(use) /** - * Creates a temporary directory and deletes it upon finalization of the - * returned resource. + * Creates a temporary directory and deletes it at the end of the use of it. + * + * @tparam A + * the type of the result computation * * @param dir * the directory which the temporary directory will be created in. Pass in @@ -663,13 +645,14 @@ package object path { * @param permissions * permissions to set on the created file * @return - * a resource containing the path of the temporary directory + * the result of the computation after using the temporary directory */ - def tempDirectory( + def withTempDirectory[A]( dir: Option[Path], prefix: String, permissions: Permissions - ): Resource[IO, Path] = files.tempDirectory(dir, prefix, permissions.some) + )(use: Path => IO[A]): IO[A] = + FilesOs.withTempDirectory(dir, prefix, permissions)(use) /** User's home directory */ def userHome: IO[Path] = files.userHome diff --git a/core/src/test/scala/io/chrisdavenport/shellfish/FileOsSpec.scala b/core/src/test/scala/io/chrisdavenport/shellfish/FileOsSpec.scala index fc5a468..9ab30ca 100644 --- a/core/src/test/scala/io/chrisdavenport/shellfish/FileOsSpec.scala +++ b/core/src/test/scala/io/chrisdavenport/shellfish/FileOsSpec.scala @@ -26,9 +26,9 @@ import weaver.scalacheck.Checkers import org.scalacheck.Gen -import cats.effect.Resource +import cats.effect.IO -import fs2.io.file.{CopyFlag, CopyFlags, Path, PosixPermissions} +import fs2.io.file.* import scodec.bits.ByteVector import scodec.Codec @@ -40,7 +40,7 @@ import syntax.path.* object FileOsSpec extends SimpleIOSuite with Checkers { test("The API should delete a file") { - tempFile.use { path => + withTempFile { path => for { _ <- path.write("") deleted <- path.deleteIfExists @@ -50,16 +50,18 @@ object FileOsSpec extends SimpleIOSuite with Checkers { test("Copying a file should have the same content as the original") { forall(Gen.asciiStr) { contents => - Resource.both(tempFile, tempFile).use { case (t1, t2) => - for { - _ <- t1.write(contents) - _ <- t1.copy(t2, CopyFlags(CopyFlag.ReplaceExisting)) - s1 <- t1.read - s2 <- t2.read - _ <- t1.deleteIfExists - _ <- t2.deleteIfExists - - } yield expect.same(s1, s2) + withTempFile { t1 => + withTempFile { t2 => + for { + _ <- t1.write(contents) + _ <- t1.copy(t2, CopyFlags(CopyFlag.ReplaceExisting)) + s1 <- t1.read + s2 <- t2.read + _ <- t1.deleteIfExists + _ <- t2.deleteIfExists + + } yield expect.same(s1, s2) + } } } } @@ -72,7 +74,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { Gen.size.flatMap(size => Gen.listOfN(size, Gen.asciiStr)) forall(contentGenerator) { contentsList => - tempFile.use { path => + withTempFile { path => for { _ <- path.writeLines(contentsList) sizeBefore <- path.readLines.map(_.size) @@ -85,7 +87,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { test("Append should behave the same as adding at the end of the string") { forall(Gen.asciiStr) { contents => - tempFile.use { path => + withTempFile { path => for { _ <- path.write(contents) sizeBefore <- path.read.map(_.length) @@ -100,13 +102,15 @@ object FileOsSpec extends SimpleIOSuite with Checkers { "`append` should behave the same as `appendLine` when prepending a newline to the contents" ) { forall(Gen.asciiStr) { contents => - Resource.both(tempFile, tempFile).use { case (t1, t2) => - for { - _ <- t1.append(s"\n$contents") - _ <- t2.appendLine(contents) - file1 <- t1.read - file2 <- t2.read - } yield expect.same(file1, file2) + withTempFile { t1 => + withTempFile { t2 => + for { + _ <- t1.append(s"\n$contents") + _ <- t2.appendLine(contents) + file1 <- t1.read + file2 <- t2.read + } yield expect.same(file1, file2) + } } } } @@ -117,7 +121,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { implicit val codec: Codec[String] = scodec.codecs.ascii forall(Gen.asciiStr) { contents => - tempFile.use { path => + withTempFile { path => for { _ <- path.writeAs(contents) sizeBefore <- path.read.map(_.length) @@ -131,7 +135,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { test("We should write bytes and read bytes") { forall(Gen.identifier) { name => - tempFile.use { path => + withTempFile { path => val bytes = ByteVector(name.getBytes) for { @@ -159,7 +163,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { } yield (path, contents) forall(multipleGenerators) { case (path, contents) => - tempDirectory.use { dir => + withTempDirectory { dir => val firstPath = dir / "moving_file.data" val movePath = dir / path / "moved_file.data" @@ -183,11 +187,11 @@ object FileOsSpec extends SimpleIOSuite with Checkers { Gen.size.flatMap(size => Gen.listOfN(size, Gen.asciiStr)) forall(contentGenerator) { contentsList => - tempFile.use { path => + withTempFile { path => for { _ <- path.writeLines(contentsList) size <- path.size - streamSize <- path.readStream.compile.count + streamSize <- Files[IO].readAll(path).compile.count } yield expect(size == streamSize) } } @@ -202,7 +206,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { } yield names.foldLeft(Path(""))(_ / _) forall(pathsGenerator) { paths => - tempDirectory.use { dir => + withTempDirectory { dir => val tempDir = dir / paths for { @@ -217,7 +221,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { test("We should be able to get and modify the last modified time of a file") { forall(Gen.asciiStr) { contents => - tempFile.use { path => + withTempFile { path => for { _ <- path.write(contents) before <- path.getLastModifiedTime @@ -247,16 +251,18 @@ object FileOsSpec extends SimpleIOSuite with Checkers { } yield names.foldLeft(Path(""))(_ / _) forall(pathsGenerator) { path => - Resource.both(tempFile, tempDirectory).use { case (file, dir) => - val link = dir / path / "link" - for { - _ <- (dir / path).createDirectories - _ <- link.createSymbolicLink(file) - exists <- link.exists(followLinks = true) - notFollowed <- link.exists(followLinks = false) - _ <- link.deleteRecursively(followLinks = true) - notExists <- link.exists(followLinks = true) - } yield expect(exists) and not(expect(notExists && notFollowed)) + withTempFile { file => + withTempDirectory { dir => + val link = dir / path / "link" + for { + _ <- (dir / path).createDirectories + _ <- link.createSymbolicLink(file) + exists <- link.exists(followLinks = true) + notFollowed <- link.exists(followLinks = false) + _ <- link.deleteRecursively(followLinks = true) + notExists <- link.exists(followLinks = true) + } yield expect(exists) and not(expect(notExists && notFollowed)) + } } } } @@ -267,7 +273,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { // Warning: Platform dependent test; this may fail on some operating systems test("The permissions POSIX API should approve or prevent reading") { - tempFile.use { path => + withTempFile { path => for { _ <- path.setPosixPermissions( PosixPermissions.fromString("r--r--r--").get @@ -284,7 +290,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { // Warning: Platform dependent test; this may fail on some operating systems test("The permissions POSIX API should approve or prevent writing") { - tempFile.use { path => + withTempFile { path => for { _ <- path.setPosixPermissions( PosixPermissions.fromString("-w--w--w-").get @@ -301,7 +307,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { // Warning: Platform dependent test; this may fail on some operating systems test("The permissions POSIX API should approve or prevent executing") { - tempFile.use { path => + withTempFile { path => for { _ <- path.setPosixPermissions( PosixPermissions.fromString("--x--x--x").get @@ -328,7 +334,7 @@ object FileOsSpec extends SimpleIOSuite with Checkers { cats.Show.show(_.toString) forall(permissionsGenerator) { permissions => - tempFile.use { path => + withTempFile { path => for { _ <- path.setPosixPermissions(permissions) perms <- path.getPosixPermissions diff --git a/core/src/test/scala/io/chrisdavenport/shellfish/SyntaxSpec.scala b/core/src/test/scala/io/chrisdavenport/shellfish/SyntaxSpec.scala index 5f64435..cb804cb 100644 --- a/core/src/test/scala/io/chrisdavenport/shellfish/SyntaxSpec.scala +++ b/core/src/test/scala/io/chrisdavenport/shellfish/SyntaxSpec.scala @@ -34,7 +34,7 @@ object SyntaxSpec extends SimpleIOSuite with Checkers { "Extension methods for read, write and append should work the same as the normal ones" ) { forall(Gen.asciiStr) { contents => - tempFile.use { path => + withTempFile { path => for { _ <- path.write(contents) _ <- path.append("Hi from the test!") @@ -50,7 +50,7 @@ object SyntaxSpec extends SimpleIOSuite with Checkers { test( "Extension methods for creating and deleting a file should work the same as the normal ones" ) { - tempDirectory.use { dir => + withTempDirectory { dir => val path = dir / "sample.txt" for { _ <- path.createFile @@ -67,7 +67,7 @@ object SyntaxSpec extends SimpleIOSuite with Checkers { "Extension methods for copying a file should work the same as the normal ones" ) { forall(Gen.asciiStr) { contents => - tempDirectory.use { dir => + withTempDirectory { dir => val original = dir / "sample.txt" val copy1 = dir / "sample-copy.txt" val copy2 = dir / "sample-copy-jo2.txt" @@ -87,7 +87,7 @@ object SyntaxSpec extends SimpleIOSuite with Checkers { ) { forall(Gen.asciiStr) { contents => - tempDirectory.use { dir => + withTempDirectory { dir => val original = dir / "sample.txt" val moved = dir / "sample-moved.txt" for {