diff --git a/io/js/src/main/scala/fs2/io/file/FilesPlatform.scala b/io/js/src/main/scala/fs2/io/file/FilesPlatform.scala index 325b654543..2bd0b980da 100644 --- a/io/js/src/main/scala/fs2/io/file/FilesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/file/FilesPlatform.scala @@ -217,27 +217,24 @@ private[fs2] trait FilesCompanionPlatform { getPosixFileAttributes(path, followLinks).map(_.permissions) override def isDirectory(path: Path, followLinks: Boolean): F[Boolean] = - stat(path, followLinks).map(_.isDirectory()) + stat(path, followLinks).map(_.isDirectory()).recover { case _: NoSuchFileException => false } override def isExecutable(path: Path): F[Boolean] = access(path, fsMod.constants.X_OK) - private val HiddenPattern = raw"/(^|\/)\.[^\/\.]/g".r override def isHidden(path: Path): F[Boolean] = F.pure { - path.toString match { - case HiddenPattern() => true - case _ => false - } + val fileName = path.fileName.toString + fileName.length >= 2 && fileName(0) == '.' && fileName(1) != '.' } override def isReadable(path: Path): F[Boolean] = access(path, fsMod.constants.R_OK) override def isRegularFile(path: Path, followLinks: Boolean): F[Boolean] = - stat(path, followLinks).map(_.isFile()) + stat(path, followLinks).map(_.isFile()).recover { case _: NoSuchFileException => false } override def isSymbolicLink(path: Path): F[Boolean] = - stat(path).map(_.isSymbolicLink()) + stat(path).map(_.isSymbolicLink()).recover { case _: NoSuchFileException => false } override def isWritable(path: Path): F[Boolean] = access(path, fsMod.constants.W_OK) diff --git a/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala b/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala index a9c027fdca..22a1537e8f 100644 --- a/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala +++ b/io/shared/src/test/scala/fs2/io/file/FilesSuite.scala @@ -23,7 +23,7 @@ package fs2 package io package file -import cats.effect.IO +import cats.effect.{IO, Resource} import cats.kernel.Order import cats.syntax.all._ @@ -541,6 +541,13 @@ class FilesSuite extends Fs2Suite with BaseFileSuite { .use(Files[IO].isDirectory(_)) .assertEquals(true) } + + test("returns false if the path does not exist") { + tempDirectory + .map(_.resolve("non-existent-file")) + .use(Files[IO].isDirectory(_)) + .assertEquals(false) + } } group("isFile") { @@ -555,5 +562,149 @@ class FilesSuite extends Fs2Suite with BaseFileSuite { .use(Files[IO].isRegularFile(_)) .assertEquals(false) } + + test("returns false if the path does not exist") { + tempDirectory + .map(_.resolve("non-existent-file")) + .use(Files[IO].isRegularFile(_)) + .assertEquals(false) + } + } + + group("isReadable") { + test("returns true if the path is for a readable file") { + tempFile + .use(Files[IO].isReadable(_)) + .assertEquals(true) + } + + test("returns true if the path is for a directory") { + tempDirectory + .use(Files[IO].isReadable(_)) + .assertEquals(true) + } + + test("returns false if the path does not exist") { + tempDirectory + .map(_.resolve("non-existent-file")) + .use(Files[IO].isReadable(_)) + .assertEquals(false) + } + } + + group("isWritable") { + test("returns true if the path is for a readable file") { + tempFile + .use(Files[IO].isWritable(_)) + .assertEquals(true) + } + + test("returns true if the path is for a directory") { + tempDirectory + .use(Files[IO].isWritable(_)) + .assertEquals(true) + } + + test("returns false if the path does not exist") { + tempDirectory + .map(_.resolve("non-existent-file")) + .use(Files[IO].isWritable(_)) + .assertEquals(false) + } + } + + group("isHidden") { + + test("returns false if the path is for a non-hidden file") { + tempFile + .use(Files[IO].isHidden(_)) + .assertEquals(false) + } + + test("returns false if the path is for a non-hidden directory") { + tempDirectory + .use(Files[IO].isHidden(_)) + .assertEquals(false) + } + + test("returns true if the path is for a hidden file") { + val hiddenFile = for { + dir <- tempDirectory + hiddenFilePath = dir.resolve(".should-be-hidden") + _ <- Resource.make(Files[IO].createFile(hiddenFilePath)) { _ => + Files[IO].deleteIfExists(hiddenFilePath).void + } + } yield hiddenFilePath + hiddenFile + .use(Files[IO].isHidden(_)) + .assertEquals(true) + } + + test("returns false if the path is for a non-hidden file in a hidden directory") { + val fileInHiddenDir = for { + dir <- tempDirectory + hiddenDirPath = dir.resolve(".should-be-hidden") + _ <- Resource.make(Files[IO].createDirectory(hiddenDirPath)) { _ => + Files[IO].deleteIfExists(hiddenDirPath).void + } + filePath = hiddenDirPath.resolve("not-hidden") + _ <- Resource.make(Files[IO].createFile(filePath)) { _ => + Files[IO].deleteIfExists(filePath).void + } + } yield filePath + fileInHiddenDir + .use(Files[IO].isHidden(_)) + .assertEquals(false) + } + + test("returns true if the path is for a hidden directory") { + val hiddenDir = for { + dir <- tempDirectory + hiddenDirPath = dir.resolve(".should-be-hidden") + _ <- Resource.make(Files[IO].createDirectory(hiddenDirPath)) { _ => + Files[IO].deleteIfExists(hiddenDirPath).void + } + } yield hiddenDirPath + hiddenDir + .use(Files[IO].isHidden(_)) + .assertEquals(true) + } + + test("returns false if the path does not exist") { + tempDirectory + .map(_.resolve("non-existent-file")) + .use(Files[IO].isWritable(_)) + .assertEquals(false) + } + } + + group("isSymbolicLink") { + + test("returns true if the path is for a symbolic link") { + val symLink = for { + dir <- tempDirectory + file <- tempFile + symLinkPath = dir.resolve("temp-sym-link") + _ <- Resource.make(Files[IO].createSymbolicLink(symLinkPath, file)) { _ => + Files[IO].deleteIfExists(symLinkPath).void + } + } yield symLinkPath + symLink + .use(Files[IO].isSymbolicLink(_)) + .assertEquals(true) + } + + test("returns false if the path is for a directory") { + tempDirectory + .use(Files[IO].isSymbolicLink(_)) + .assertEquals(false) + } + + test("returns false if the path does not exist") { + tempDirectory + .map(_.resolve("non-existent-file")) + .use(Files[IO].isSymbolicLink(_)) + .assertEquals(false) + } } }