Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TempPath (<: AutoCloseable) and withTempFile|Dir convenience methods #147

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 17 additions & 7 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
32 changes: 29 additions & 3 deletions os/src/Path.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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] {
Expand Down
80 changes: 72 additions & 8 deletions os/src/TempOps.scala
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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
Expand All @@ -40,7 +52,7 @@ object temp {

if (contents != null) write.over(Path(nioPath), contents)
if (deleteOnExit) nioPath.toFile.deleteOnExit()
Path(nioPath)
new TempPath(nioPath)
}

/**
Expand All @@ -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()))
Expand All @@ -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, // TempFile.close() deletes it, no need to register with JVM
mpollmeier marked this conversation as resolved.
Show resolved Hide resolved
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, // TempFile.close() deletes it, no need to register with JVM
mpollmeier marked this conversation as resolved.
Show resolved Hide resolved
perms = perms
))(fun)

}
77 changes: 77 additions & 0 deletions os/test/src/TempPathTests.scala
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
}