diff --git a/Readme.adoc b/Readme.adoc index 4c04df23..869cac74 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -1157,6 +1157,43 @@ os.followLink(wd / "misc" / "folder-symlink") ==> Some(wd / "folder1") os.followLink(wd / "misc" / "broken-symlink") ==> None ---- +==== `os.temp.withFile` + +[source,scala] +---- +os.temp.withFile[A](fun: Path => A): A +---- + +Creates a temporary file, passes it to the given function and removes it immediately after the function completed, even if the function threw an exception. + +This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp file gets deleted even if JVM never stopps gracefully (e.g. because it's intended to run continually, or the process get's killed from the outside). + +[source,scala] +---- +os.temp.withFile { file => + os.write(file, "some content") +} +---- + +==== `os.temp.withDir` + +[source,scala] +---- +os.temp.withDir[A](fun: Path => A): A +---- + +Creates a temporary directory, passes it to the given function and removes it immediately after the function completed, even if the function threw an exception. + +This doesn't rely on the JVM's `deleteOnExit` handler, therefor the temp dir gets deleted even if JVM never stopps gracefully (e.g. because it's intended to run continually, or the process get's killed from the outside). + +[source,scala] +---- +os.temp.withDir { file => + val file = dir / "somefile" + os.write(file, "some content") +} +---- + ==== `os.temp` [source,scala] diff --git a/build.sc b/build.sc index bb723fa6..7d578847 100644 --- a/build.sc +++ b/build.sc @@ -35,6 +35,7 @@ object Deps { val acyclic = ivy"com.lihaoyi:::acyclic:0.3.6" val jna = ivy"net.java.dev.jna:jna:5.13.0" val geny = ivy"com.lihaoyi::geny::1.0.0" + val scalaCollectionCompat = ivy"org.scala-lang.modules::scala-collection-compat::2.9.0" val sourcecode = ivy"com.lihaoyi::sourcecode::0.3.0" val utest = ivy"com.lihaoyi::utest::0.8.1" def scalaLibrary(version: String) = ivy"org.scala-lang:scala-library:${version}" @@ -143,10 +144,15 @@ trait OsLibModule extends CrossScalaModule with PublishModule with AcyclicModule ) def platformSegment: String override def millSourcePath = super.millSourcePath / oslib.up - override def sources = T.sources( - millSourcePath / "src", - millSourcePath / s"src-$platformSegment" - ) + override def sources = T.sources { + Seq( + PathRef(millSourcePath / "src"), + PathRef(millSourcePath / s"src-$platformSegment") + ) ++ + ZincWorkerUtil.versionRanges(crossScalaVersion, scalaVersions).map(vr => + PathRef(millSourcePath / s"src-${vr}")) + + } } trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps { @@ -168,9 +174,13 @@ trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps { trait OsModule extends OsLibModule { override def artifactName = "os-lib" - override def ivyDeps = Agg( - Deps.geny - ) + override def ivyDeps = T { + val scalaV = scalaVersion() + if (scalaV.startsWith("2.11") || scalaV.startsWith("2.12")) { + // include collection compat, mostly for a backported scala.util.Using + Agg(Deps.geny, Deps.scalaCollectionCompat) + } else Agg(Deps.geny) + } } trait WatchModule extends OsLibModule { diff --git a/os/src/Path.scala b/os/src/Path.scala index 9de9d2a4..22b4783c 100644 --- a/os/src/Path.scala +++ b/os/src/Path.scala @@ -1,7 +1,8 @@ package os import java.net.URI -import java.nio.file.Paths +import java.nio.file.{LinkOption, Paths} +import java.nio.file.Files import collection.JavaConverters._ import scala.language.implicitConversions @@ -450,7 +451,7 @@ trait ReadablePath { class Path private[os] (val wrapped: java.nio.file.Path) extends FilePath with ReadablePath with BasePathImpl { def toSource: SeekableSource = - new SeekableSource.ChannelSource(java.nio.file.Files.newByteChannel(wrapped)) + new SeekableSource.ChannelSource(Files.newByteChannel(wrapped)) require(wrapped.isAbsolute, s"$wrapped is not an absolute path") def segments: Iterator[String] = wrapped.iterator().asScala.map(_.toString) @@ -497,7 +498,32 @@ class Path private[os] (val wrapped: java.nio.file.Path) def resolveFrom(base: os.Path) = this - def getInputStream = java.nio.file.Files.newInputStream(wrapped) + def getInputStream = Files.newInputStream(wrapped) +} + +class TempPath private[os] (wrapped: java.nio.file.Path) + extends Path(wrapped) with AutoCloseable { + + override def close(): Unit = deleteRecursively(wrapped) + + /** Wouldn't it be nice if we could just call `os.remove.all(this)`? + * For some reason, Scala 2 throws a rather obscure `[error] Unwanted cyclic dependency` + */ + private def deleteRecursively(ioPath: java.nio.file.Path): Unit = { + if (Files.isDirectory(ioPath)) { + // while we support Scala 2.11 we need (something like) this: + Files.list(ioPath).forEach( + new java.util.function.Consumer[java.nio.file.Path] { + override def accept(path: java.nio.file.Path): Unit = + deleteRecursively(path) + } + ) + + // this works for Scala 2.12+ + // Files.list(ioPath).forEach(deleteRecursively) + } + Files.deleteIfExists(ioPath) + } } sealed trait PathConvertible[T] { diff --git a/os/src/TempOps.scala b/os/src/TempOps.scala index 900fe6b7..43284792 100644 --- a/os/src/TempOps.scala +++ b/os/src/TempOps.scala @@ -1,11 +1,23 @@ package os -import java.nio.file.attribute.{FileAttribute, PosixFilePermission, PosixFilePermissions} +import java.nio.file.attribute.{FileAttribute, PosixFilePermissions} +import scala.util.Using /** - * Alias for `java.nio.file.Files.createTempFile` and - * `java.io.File.deleteOnExit`. Pass in `deleteOnExit = false` if you want - * the temp file to stick around. + * Create temporary files and directories. [[withFile]] and [[withDir]] + * are convenience methods that handle the most common case. They delete the temp + * file/dir immediately after the given function completed - even if the given + * function threw an exception. + * + * {{{ + * os.temp.withFile { file => + * os.write(file, "some content") + * } + * }}} + * + * [[os.temp()]] and [[os.temp.dir()]] are aliases for + * `java.nio.file.Files.createTemp[File|Directory]` and `java.io.File.deleteOnExit`. + * Pass in `deleteOnExit = false` if you want the temp file to stick around. */ object temp { @@ -27,7 +39,7 @@ object temp { suffix: String = null, deleteOnExit: Boolean = true, perms: PermSet = null - ): Path = { + ): TempPath = { import collection.JavaConverters._ val permArray: Array[FileAttribute[_]] = if (perms == null) Array.empty @@ -40,7 +52,7 @@ object temp { if (contents != null) write.over(Path(nioPath), contents) if (deleteOnExit) nioPath.toFile.deleteOnExit() - Path(nioPath) + new TempPath(nioPath) } /** @@ -56,7 +68,7 @@ object temp { prefix: String = null, deleteOnExit: Boolean = true, perms: PermSet = null - ): Path = { + ): TempPath = { val permArray: Array[FileAttribute[_]] = if (perms == null) Array.empty else Array(PosixFilePermissions.asFileAttribute(perms.toSet())) @@ -67,7 +79,59 @@ object temp { } if (deleteOnExit) nioPath.toFile.deleteOnExit() - Path(nioPath) + new TempPath(nioPath) } + /** + * Convenience method that creates a temporary file and automatically deletes it + * after the given function completed - even if the function throws an exception. + * + * {{{ + * os.temp.withFile { file => + * os.write(file, "some content") + * } + * }}} + */ + def withFile[A]( + fun: Path => A, + contents: Source = null, + dir: Path = null, + prefix: String = null, + suffix: String = null, + perms: PermSet = null + ): A = { + Using.resource(os.temp( + contents = contents, + dir = dir, + prefix = prefix, + suffix = suffix, + deleteOnExit = false, // TempPath.close() deletes it, no need to register with JVM + perms = perms + ))(fun) + } + + /** + * Convenience method that creates a temporary directory and automatically deletes it + * after the given function completed - even if the function throws an exception. + * + * {{{ + * os.temp.withDir { file => + * val file = dir / "somefile" + * os.write(file, "some content") + * } + * }}} + */ + def withDir[A]( + fun: Path => A, + dir: Path = null, + prefix: String = null, + perms: PermSet = null + ): A = + Using.resource(os.temp.dir( + dir = dir, + prefix = prefix, + deleteOnExit = false, // TempPath.close() deletes it, no need to register with JVM + perms = perms + ))(fun) + } diff --git a/os/test/src/TempPathTests.scala b/os/test/src/TempPathTests.scala new file mode 100644 index 00000000..1424d7a2 --- /dev/null +++ b/os/test/src/TempPathTests.scala @@ -0,0 +1,77 @@ +package test.os + +import os._ +import scala.util.Using +import utest.{assert => _, _} + +object TempPathTests extends TestSuite{ + val tests = Tests { + + test("convenience methods") { + test("temp.withFile") { + var tempFilePath: String = null + os.temp.withFile { file => + tempFilePath = file.toString + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempFilePath)), + s"temp file did not get auto-deleted after `Using` block: $tempFilePath") + } + test("temp.withDir") { + var tempDirPath: String = null + var tempFilePath: String = null + os.temp.withDir { dir => + val file = dir / "somefile" + tempDirPath = dir.toString + tempFilePath = file.toString + os.write(file, "some content") + assert(os.exists(dir)) + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempDirPath)), + s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") + } + } + + test("delete after `Using` block") { + test("single file") { + var tempFilePath: String = null + Using(os.temp()) { file => + tempFilePath = file.toString + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempFilePath)), + s"temp file did not get auto-deleted after `Using` block: $tempFilePath") + } + test("directory") { + var tempDirPath: String = null + var tempFilePath: String = null + Using(os.temp.dir()) { dir => + val file = dir / "somefile" + tempDirPath = dir.toString + tempFilePath = file.toString + os.write(file, "some content") + assert(os.exists(dir)) + assert(os.exists(file)) + } + assert(!os.exists(os.Path(tempDirPath)), + s"temp dir did not get auto-deleted after `Using` block: $tempDirPath") + } + + test("multiple files") { + var tempFilePaths: Seq[String] = Nil + Using.Manager { use => + val file1 = use(os.temp()) + val file2 = use(os.temp()) + val files = Seq(file1, file2) + tempFilePaths = files.map(_.toString) + files.foreach(file => assert(os.exists(file))) + } + tempFilePaths.foreach { file => + assert(!os.exists(os.Path(file)), + s"temp file did not get auto-deleted after `Using` block: $file") + } + } + } + } +}