diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/ExecutableSearch.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/ExecutableSearch.scala new file mode 100644 index 0000000000..f2f39c5149 --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/ExecutableSearch.scala @@ -0,0 +1,52 @@ +package izumi.fundamentals.platform.files + +import izumi.fundamentals.platform.os.{IzOs, OsType} + +import java.nio.file.{Path, Paths} + +trait ExecutableSearch { + def haveExecutables(names: String*): Boolean = { + names.forall(which(_).nonEmpty) + } + + def which(name: String, morePaths: Seq[String] = Seq.empty): Option[Path] = { + find(binaryNameCandidates(name), IzOs.path ++ morePaths) + } + + def whichAll(name: String, morePaths: Seq[String] = Seq.empty): Iterable[Path] = { + findAll(binaryNameCandidates(name), IzOs.path ++ morePaths) + } + + private def find(candidates: Seq[String], paths: Seq[String]): Option[Path] = { + paths.view + .flatMap { + p => + candidates.map(ext => Paths.get(p).resolve(ext)) + } + .find { + p => + p.toFile.exists() + } + } + + private def findAll(candidates: Seq[String], paths: Seq[String]): Iterable[Path] = { + paths.view + .flatMap { + p => + candidates.map(ext => Paths.get(p).resolve(ext)) + } + .filter { + p => + p.toFile.exists() + } + } + + private def binaryNameCandidates(name: String): Seq[String] = { + IzOs.osType match { + case OsType.Windows => + Seq("exe", "com", "bat").map(ext => s"$name.$ext") + case _ => + Seq(name) + } + } +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileAttributes.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileAttributes.scala new file mode 100644 index 0000000000..e114dfe291 --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileAttributes.scala @@ -0,0 +1,29 @@ +package izumi.fundamentals.platform.files + +import izumi.fundamentals.platform.time.IzTime + +import java.io.File +import java.time.LocalDateTime + +trait FileAttributes { this: FileSearch => + /** Unsafe, will recursively iterate the whole directory + */ + def getLastModifiedRecursively(directory: File): Option[LocalDateTime] = { + import IzTime.* + + if (!directory.exists()) { + return None + } + + if (directory.isDirectory) { + val dmt = directory.lastModified().asEpochMillisLocal + + val fmt = walk(directory).map(_.toFile.lastModified().asEpochMillisLocal).toSeq + + Some((dmt +: fmt).max) + } else { + Some(directory.lastModified().asEpochMillisLocal) + } + } + +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileReads.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileReads.scala new file mode 100644 index 0000000000..3f9f0ed510 --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileReads.scala @@ -0,0 +1,16 @@ +package izumi.fundamentals.platform.files + +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Path + +trait FileReads { + def readString(path: Path): String = { + import java.nio.file.Files + new String(Files.readAllBytes(path), StandardCharsets.UTF_8) + } + + def readString(file: File): String = { + readString(file.toPath) + } +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileSearch.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileSearch.scala new file mode 100644 index 0000000000..417abedff3 --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FileSearch.scala @@ -0,0 +1,15 @@ +package izumi.fundamentals.platform.files + +import java.io.File +import java.nio.file.{Files, Path} +import scala.jdk.CollectionConverters.* + +trait FileSearch { + def walk(directory: Path): Iterator[Path] = { + Files.walk(directory).iterator().asScala + } + + def walk(directory: File): Iterator[Path] = { + walk(directory.toPath) + } +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FsRefresh.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FsRefresh.scala new file mode 100644 index 0000000000..bcbc8cb8e7 --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/FsRefresh.scala @@ -0,0 +1,26 @@ +package izumi.fundamentals.platform.files + +import izumi.fundamentals.platform.language.Quirks + +import java.nio.file.{Files, Path} + +trait FsRefresh { this: RecursiveFileRemovals => + def recreateDirs(paths: Path*): Unit = { + paths.foreach(recreateDir) + } + + def recreateDir(path: Path): Unit = { + val asFile = path.toFile + + if (asFile.exists()) { + erase(path) + } + + Quirks.discard(asFile.mkdirs()) + } + + def refreshSymlink(symlink: Path, target: Path): Unit = { + Quirks.discard(symlink.toFile.delete()) + Quirks.discard(Files.createSymbolicLink(symlink, target.toFile.getCanonicalFile.toPath)) + } +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/Homedir.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/Homedir.scala new file mode 100644 index 0000000000..260ed5b8f4 --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/Homedir.scala @@ -0,0 +1,13 @@ +package izumi.fundamentals.platform.files + +import java.nio.file.{Path, Paths} + +trait Homedir { + def home(): Path = { + Paths.get(System.getProperty("user.home")) + } + + def homedir(): String = { + home().toFile.getCanonicalPath + } +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzFiles.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzFiles.scala index 3ae8073554..ca2488afce 100644 --- a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzFiles.scala +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzFiles.scala @@ -1,144 +1,19 @@ package izumi.fundamentals.platform.files -import java.io.{File, IOException} import java.net.URI -import java.nio.charset.StandardCharsets -import java.nio.file._ -import java.nio.file.attribute.BasicFileAttributes -import java.time.LocalDateTime -import java.util.stream.Collectors - -import izumi.fundamentals.platform.language.Quirks -import izumi.fundamentals.platform.os.{IzOs, OsType} -import izumi.fundamentals.platform.time.IzTime - -import scala.jdk.CollectionConverters._ +import java.nio.file.* +import scala.annotation.nowarn +import scala.jdk.CollectionConverters.* import scala.util.Try -object IzFiles { - def homedir(): String = { - Paths.get(System.getProperty("user.home")).toFile.getCanonicalPath - } - - def getFs(uri: URI): Try[FileSystem] = synchronized { +@nowarn("msg=Unused import") +object IzFiles extends RecursiveFileRemovals with FileSearch with FsRefresh with FileReads with ExecutableSearch with Homedir with FileAttributes { + def getFs(uri: URI, loader: ClassLoader): Try[FileSystem] = { + // this is like DCL, there might be races but we have no tool to prevent them + // so first we try to get a filesystem, if it's not there we try to create it, there might be a race so it can fail, so we try to get again Try(FileSystems.getFileSystem(uri)) - .recover { - case _ => - FileSystems.newFileSystem(uri, Map.empty[String, Any].asJava) - } - } - - def getLastModified(directory: File): Option[LocalDateTime] = { - import IzTime._ - - if (!directory.exists()) { - return None - } - - if (directory.isDirectory) { - val dmt = directory.lastModified().asEpochMillisLocal - - val fmt = walk(directory).map(_.toFile.lastModified().asEpochMillisLocal) - - Some((dmt +: fmt).max) - } else { - Some(directory.lastModified().asEpochMillisLocal) - } - } - - def walk(directory: File): Seq[Path] = { - Files.walk(directory.toPath).collect(Collectors.toList()).asScala.toSeq - } - - def recreateDirs(paths: Path*): Unit = { - paths.foreach(recreateDir) - } - - def readString(path: Path): String = { - import java.nio.file.Files - new String(Files.readAllBytes(path), StandardCharsets.UTF_8) - } - - def readString(file: File): String = { - readString(file.toPath) - } - - def recreateDir(path: Path): Unit = { - val asFile = path.toFile - - if (asFile.exists()) { - removeDir(path) - } - - Quirks.discard(asFile.mkdirs()) - } - - def removeDir(root: Path): Unit = { - val _ = Files.walkFileTree( - root, - new SimpleFileVisitor[Path] { - override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { - Files.delete(file) - FileVisitResult.CONTINUE - } - - override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { - Files.delete(dir) - FileVisitResult.CONTINUE - } - - }, - ) - } - - def refreshSymlink(symlink: Path, target: Path): Unit = { - Quirks.discard(symlink.toFile.delete()) - Quirks.discard(Files.createSymbolicLink(symlink, target.toFile.getCanonicalFile.toPath)) - } - - def haveExecutables(names: String*): Boolean = { - names.forall(which(_).nonEmpty) - } - - def which(name: String, morePaths: Seq[String] = Seq.empty): Option[Path] = { - find(binaryNameCandidates(name), IzOs.path ++ morePaths) - } - - def whichAll(name: String, morePaths: Seq[String] = Seq.empty): Iterable[Path] = { - findAll(binaryNameCandidates(name), IzOs.path ++ morePaths) - } - - private def binaryNameCandidates(name: String): Seq[String] = { - IzOs.osType match { - case OsType.Windows => - Seq("exe", "com", "bat").map(ext => s"$name.$ext") - case _ => - Seq(name) - } - } - - def find(candidates: Seq[String], paths: Seq[String]): Option[Path] = { - paths.view - .flatMap { - p => - candidates.map(ext => Paths.get(p).resolve(ext)) - } - .find { - p => - p.toFile.exists() - } - } - - def findAll(candidates: Seq[String], paths: Seq[String]): Iterable[Path] = { - paths.view - .flatMap { - p => - candidates.map(ext => Paths.get(p).resolve(ext)) - } - .filter { - p => - p.toFile.exists() - } + .orElse(Try(FileSystems.newFileSystem(uri, Map.empty[String, Any].asJava, loader))) + .orElse(Try(FileSystems.getFileSystem(uri))) } } diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzZip.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzZip.scala index 76fc616d4e..7b5c235698 100644 --- a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzZip.scala +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/IzZip.scala @@ -35,14 +35,14 @@ object IzZip { } // zip filesystem isn't thread safe - def findInZips(zips: Seq[File], predicate: Path => Boolean): Iterable[(Path, String)] = synchronized { + def findInZips(zips: Seq[File], predicate: Path => Boolean): Iterable[(Path, String)] = { zips .filter(f => f.exists() && f.isFile && (f.getName.endsWith(".jar") || f.getName.endsWith(".zip"))) .flatMap { f => val uri = f.toURI val jarUri = URI.create(s"jar:${uri.toString}") - val fs = IzFiles.getFs(jarUri).get + val fs = IzFiles.getFs(jarUri, Thread.currentThread().getContextClassLoader).get try { enumerate(predicate, fs) diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/RecursiveFileRemovals.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/RecursiveFileRemovals.scala new file mode 100644 index 0000000000..2e7dfdca9b --- /dev/null +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/files/RecursiveFileRemovals.scala @@ -0,0 +1,34 @@ +package izumi.fundamentals.platform.files + +import java.io.{File, IOException} +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} + +trait RecursiveFileRemovals { + def erase(root: Path): Unit = { + val _ = Files.walkFileTree( + root, + new SimpleFileVisitor[Path] { + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.delete(file) + FileVisitResult.CONTINUE + } + + override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { + Files.delete(dir) + FileVisitResult.CONTINUE + } + + }, + ) + } + + def erase(root: File): Unit = { + erase(root.toPath) + } + + @deprecated("use IzFiles.erase") + def removeDir(root: Path): Unit = { + erase(root) + } +} diff --git a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/resources/IzResources.scala b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/resources/IzResources.scala index c9ce15c727..8ac6a2055f 100644 --- a/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/resources/IzResources.scala +++ b/fundamentals/fundamentals-platform/.jvm/src/main/scala/izumi/fundamentals/platform/resources/IzResources.scala @@ -33,14 +33,12 @@ final class IzResources(private val classLoader: ClassLoader) extends AnyVal { Some(LoadablePathReference(Paths.get(u.toURI), null)) } catch { case _: FileSystemNotFoundException => - IzFiles.getFs(u.toURI) match { + IzFiles.getFs(u.toURI, classLoader) match { case Failure(_) => Some(UnloadablePathReference(u.toURI)) - // throw exception + case Success(fs) => - fs.synchronized { - Some(LoadablePathReference(fs.provider().getPath(u.toURI), fs)) - } + Some(LoadablePathReference(fs.provider().getPath(u.toURI), fs)) } } diff --git a/fundamentals/fundamentals-platform/.jvm/src/test/scala/izumi/fundamentals/platform/IzFilesTest.scala b/fundamentals/fundamentals-platform/.jvm/src/test/scala/izumi/fundamentals/platform/IzFilesTest.scala index 989104a768..fa62e66578 100644 --- a/fundamentals/fundamentals-platform/.jvm/src/test/scala/izumi/fundamentals/platform/IzFilesTest.scala +++ b/fundamentals/fundamentals-platform/.jvm/src/test/scala/izumi/fundamentals/platform/IzFilesTest.scala @@ -1,12 +1,12 @@ package izumi.fundamentals.platform -import java.util.concurrent.TimeUnit import izumi.fundamentals.platform.files.IzFiles import org.scalatest.wordspec.AnyWordSpec +import java.nio.file.{Files, Paths} +import java.util.concurrent.TimeUnit import scala.concurrent.Await import scala.concurrent.duration.Duration -import scala.util.Try class IzFilesTest extends AnyWordSpec { @@ -14,20 +14,40 @@ class IzFilesTest extends AnyWordSpec { "resolve path entries on nix-like systems" in { assert(IzFiles.which("bash").nonEmpty) } + + "destroy directories and files" in { + val dir0 = Files.createTempDirectory("test") + assert(dir0.toFile.exists()) + + IzFiles.erase(dir0.toFile) + assert(!dir0.toFile.exists()) + + val file0 = Files.createTempFile("test", "test") + assert(file0.toFile.exists()) + IzFiles.erase(file0.toFile) + assert(!file0.toFile.exists()) + } + + "resolve home directory" in { + Paths.get(IzFiles.homedir()).toFile.isDirectory + } + +// "support last modified" in { +// assert(IzFiles.getLastModifiedRecursively(IzFiles.home().toFile).nonEmpty) +// } + } import izumi.fundamentals.platform.resources.IzResources + import scala.concurrent.Future "Resource tools" should { "support concurrent queries" in { - // unfortunately this test is unreliable, the filesystem logic might occasionally fail during initialization + // the filesystem logic might occasionally fail during initialization + // we need to file a JDK bug if we can reliably reproduce this import scala.concurrent.ExecutionContext.Implicits.global - while (Try(IzResources.getPath("library.properties")).isFailure) { - Thread.sleep(100) - } - val seq = (0 to 200).map { _ => Future(IzResources.getPath("library.properties"))