![JAR search and dependency download from the Maven repository](/logo.png)
grizzled.file.util.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grizzled-scala_2.11 Show documentation
Show all versions of grizzled-scala_2.11 Show documentation
A general-purpose Scala utility library
The newest version!
package grizzled.file
import scala.annotation.tailrec
import scala.language.higherKinds
import grizzled.io.Implicits.RichInputStream
import grizzled.sys.os
import grizzled.sys.OperatingSystem._
import java.io.{File, IOException}
import java.nio.file.{Files, Path, Paths}
import java.security.{SecureRandom => Random}
import scala.util.{Failure, Success, Try}
class FileDoesNotExistException(message: String) extends Exception
/** Useful file-related utility functions.
*/
object util {
val fileSeparator: String = File.separator
val fileSeparatorChar: Char = fileSeparator(0)
private lazy val random = new Random()
import grizzled.ScalaCompat._
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/** Get the directory name of a pathname.
*
* @param path path (absolute or relative)
* @param fileSep the file separator to use. Defaults to the value of
* the "file.separator" property.
*
* @return the directory portion
*/
def dirname(path: String, fileSep: String = fileSeparator): String = {
val components = splitPath(path, fileSep)
components match {
case Nil =>
""
case List("") =>
""
case simple :: Nil if ! (simple startsWith fileSep) =>
"."
case _ =>
val len = components.length
val result = components.take(len - 1) mkString fileSep
if (result.length == 0)
fileSep
else
result
}
}
/** Get the basename (file name only) part of a path.
*
* @param path the path (absolute or relative)
* @param fileSep the file separator to use. Defaults to the value of
* the "file.separator" property.
*
* @return the file name portion
*/
def basename(path: String, fileSep: String = fileSeparator): String = {
val components = splitPath(path, fileSep)
components match {
case Nil =>
""
case List("") =>
""
case simple :: Nil if ! (simple startsWith fileSep) =>
path
case _ =>
components.drop(components.length - 1) mkString fileSep
}
}
/** Split a path into directory (dirname) and file (basename) components.
* Analogous to Python's `os.path.pathsplit()` function.
*
* @param path the path to split
* @param fileSep the file separator to use. Defaults to the value of
* the "file.separator" property.
*
* @return a (dirname, basename) tuple of strings
*/
def dirnameBasename(path: String,
fileSep: String = fileSeparator): (String, String) = {
if (Option(path).isEmpty || path.isEmpty)
("", "")
else if ((path == ".") || (path == ".."))
(path, "")
else if (! (path contains fileSep))
(".", path)
else if (path == fileSep)
(fileSep, "")
else {
// Use a character to split, so it's not interpreted as a regular
// expression (which causes problems with a Windows-style "\".
// NOTE: We deliberately don't use splitPath() here.
val components = (path split fileSep(0)).toList
components match {
case Nil =>
("", "")
case head :: Nil =>
(head, "")
case head :: tail =>
val listTuple = components splitAt (components.length - 1)
val s: String = listTuple._1 mkString fileSep
val prefix =
if ((s.length == 0) && (path startsWith fileSep))
fileSep
else
s
(prefix, listTuple._2 mkString fileSep)
}
}
}
private lazy val ExtRegexp = """^(.*)(\.[^.]+)$""".r
/** Split a pathname into the directory name, basename, and extension
* pieces.
*
* @param pathname the pathname
* @param fileSep the file separator to use. Defaults to the value of
* the "file.separator" property.
*
* @return a 3-tuple of (dirname, basename, extension)
*/
def dirnameBasenameExtension(pathname: String,
fileSep: String = fileSeparator):
(String, String, String) = {
val (path1, extension) = pathname match {
case ExtRegexp(pathNoExt, ext) => (pathNoExt, ext)
case _ => (pathname, "")
}
val (dirname, basename) = dirnameBasename(path1, fileSep)
(dirname, basename, extension)
}
/** Return the current working directory, as an absolute path.
*
* @return the current working directory
*/
def pwd: String = new File(".").getCanonicalPath
/**
* Calculate the relative path between two files.
*
* @param from the starting file
* @param to the file to be converted to a relative path
*
* @return the (String) relative path
*/
def relativePath(from: File, to: File): String = {
if (from.getAbsolutePath == to.getAbsolutePath)
basename(from.getPath)
else {
val fromPath = toPathArray(from)
val toPath = toPathArray(to)
val commonLength = commonPrefix(fromPath, toPath)
val relativeTo = toPath.drop(commonLength)
if (fromPath.length == commonLength)
// It's right under the from path.
relativeTo mkString fileSeparator
else {
val commonParentsTotal = fromPath.length - commonLength - 1
require(commonParentsTotal >= 0)
val up = (".." + fileSeparator) * commonParentsTotal
relativeTo.mkString(up, fileSeparator, "")
}
}
}
/** Return a list of paths matching a pathname pattern. The pattern may
* contain simple shell-style wildcards. See `fnmatch()`. This function
* is essentially a direct port of the Python `glob.glob()` function.
*
* Restrictions:
*
* - There's currently no way to escape a wildcard character. That is,
* if you need to match a '*' character or a '?' character exactly,
* you can't do that with this library. (You can't do it with the
* `glob` library in Python 2 or Python 3, either.)
*
* @param path The path to expand.
*
* @return a list of possibly expanded file names
*/
def glob(path: String): List[String] = {
def glob1(dirname: String, pattern: String): List[String] = {
val dir = if (dirname.length == 0) pwd else dirname
val names = Option(new File(dir).list).map(_.toList)
.getOrElse(List.empty[String])
if (names.isEmpty)
Nil
else {
val names2 =
if (path(0) != '.')
names.filter(_(0) != '.')
else
names
names.filter(n => fnmatch(n, pattern))
}
}
def glob0(dirname: String, basename: String): List[String] = {
if (basename.length == 0) {
if (new File(dirname).isDirectory)
List(basename)
else
Nil
}
else {
val path = dirname + fileSeparator + basename
if (new File(path).exists())
List(basename)
else
Nil
}
}
val wildcards = """[\*\?\[]""".r
if ((wildcards findFirstIn path).isEmpty) {
if (new File(path).exists)
List(path)
else
List.empty[String]
}
else {
val (dirname, basename) = dirnameBasename(path)
if (dirname.length == 0)
for (name <- glob1(pwd, basename)) yield name
else {
val dirs = if ((wildcards findFirstIn dirname).nonEmpty)
glob(dirname)
else
List(dirname)
val globber = if ((wildcards findFirstIn basename).nonEmpty)
glob1 _
else
glob0 _
for (d <- dirs; name <- globber(d, basename))
yield d + fileSeparator + name
}
}
}
/** An extended ''glob'' function that supports all the wildcards of
* the `glob()` function, in addition to:
* - a leading `~`, signifying the user's home directory
* - a special `**` wildcard that recursively matches any directory.
* (Think "ant".)
*
* ''~user'' is not supported, however.
*
* NOTE: Any non-Windows operating system is treated as Posix.
*
* @param pattern the wildcard pattern
*
* @return list of matches, or an empty list for none
*/
def eglob(pattern: String): List[String] = {
def doGlob(pieces: List[String], directory: String): List[String] = {
import scala.collection.mutable.ArrayBuffer
val result = new ArrayBuffer[String]()
pieces match {
case Nil => pieces
case head :: tail =>
val last = tail.isEmpty
if (head == "**") {
val remainingPieces = if (last) Nil else pieces.drop(1)
for ((root, dirs, files) <- walk(directory, topdown = true)) {
if (last) {
// At the end of a pattern, "**" just recursively
// matches directories.
result += root
}
else {
// Recurse downward, trying to match the rest of
// the pattern.
result ++= doGlob(remainingPieces, root)
}
}
}
else {
// Regular glob pattern.
val path = directory + fileSeparator + head
val matches = glob(path)
if (matches.nonEmpty) {
if (last)
// Save the matches, and stop.
result ++= matches
else {
// Must continue recursing.
val remainingPieces = pieces.drop(1)
for (m <- matches if new File(m).isDirectory) {
val subResult = doGlob(remainingPieces, m)
for (partialPath <- subResult)
result += partialPath
}
}
}
}
}
result.toList
}
// Main eglob() logic
// Account for leading "~"
val adjustedPattern =
if (pattern.length == 0)
"."
else if (pattern.startsWith("~"))
normalizePath(joinPath(System.getProperty("user.home"), pattern drop 1))
else
pattern
// Determine leading directory, which is different per OS (because
// of Windows' stupid drive letters).
val (relativePattern, directory) = eglobPatternSplitter(adjustedPattern)
// Do the actual globbing.
val pieces = splitPath(relativePattern)
val matches = doGlob(pieces, directory)
matches map normalizePath
}
/** Similar to Python's `fnmatch()` function, this function determines
* whether a string matches a wildcard pattern. Patterns are Unix-style
* shell-style wildcards:
*
* - `*` matches everything
* - `?` matches any single character
* - `[set]` matches any character in ''set''
* - `[!set]` matches any character not in ''set''
*
* An initial period in `filename` is not special. Matches are
* case-sensitive on Posix operating systems, case-insensitive elsewhere.
*
* @param name the name to match
* @param pattern the wildcard pattern
*/
def fnmatch(name: String, pattern: String): Boolean = {
// Convert to regular expression pattern.
val caseConv: String => String =
if (os == Posix)
{s => s}
else
{s => s.toLowerCase}
val regex = caseConv("^" + pattern.replace("\\", "\\\\")
.replace(".", "\\.")
.replace("*", ".*")
.replace("[!", "[^")
.replace("?", ".") + "$").r
regex.findFirstIn(caseConv(name)).nonEmpty
}
/** Directory tree generator, adapted from Python's `os.walk()`
* function.
*
* Note that `java.nio.Files.walk()` and `java.nio.Files.walkTree()`
* provide similar functionality, though with a different interface.
*
* For each directory in the directory tree rooted at top (including top
* itself, but excluding '.' and '..'), yields a 3-tuple
*
* {{{
* (dirpath, dirnames, filenames)
* }}}
*
* ''dirpath'' is a string, the path to the directory. ''dirnames'' is a
* list of the names of the subdirectories in ''dirpath'' (excluding '.'
* and '..'). ''filenames'' is a list of the names of the non-directory
* files in ''dirpath''. Note that the names in the lists are just names,
* with no path components. To get a full path (which begins with top) to a
* file or directory in ''dirpath'', use `dirpath + java.io.fileSeparator +
* name`, or use `joinPath()`.
*
* If ''topdown'' is `true`, the triple for a directory is generated before
* the triples for any of its subdirectories (directories are generated top
* down). If `topdown` is `false`, the triple for a directory is generated
* after the triples for all of its subdirectories (directories are generated
* bottom up).
*
* '''WARNING!''' This method does ''not'' grok symbolic links!
*
* The JDK's [[https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#walk-java.nio.file.Path-int-java.nio.file.FileVisitOption...- java.nio.file.Files.walk()]]
* function provides a similar capability in JDK 8. Prior to JDK 8, you can
* also use [[https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#walkFileTree(java.nio.file.Path,%20java.util.Set,%20int,%20java.nio.file.FileVisitor) java.nio.file.Files.walkFileTree()]]
*
* @param top name of starting directory
* @param topdown `true` to do a top-down traversal, `false`
* otherwise.
*
* @return List of triplets, as described above.
*/
def walk(top: String, topdown: Boolean = true):
List[(String, List[String], List[String])] = {
// This needs to be made more efficient, with some kind of generator.
import scala.collection.mutable.ArrayBuffer
val dirs = new ArrayBuffer[String]()
val nondirs = new ArrayBuffer[String]()
val result = new ArrayBuffer[(String, List[String], List[String])]()
val fTop = new File(top)
val names = Option(fTop.list).getOrElse(Array.empty[String])
if (names.nonEmpty) {
for (name <- names) {
val f = new File(top + fileSeparator + name)
if (f.isDirectory)
dirs += name
else
nondirs += name
}
if (topdown)
result += ((top, dirs.toList, nondirs.toList))
for (name <- dirs)
result ++= walk(top + fileSeparator + name, topdown)
if (! topdown)
result += ((top, dirs.toList, nondirs.toList))
}
result.toList
}
/** List a directory recursively, returning `File` objects for each file
* (and subdirectory) found. This method does lazy evaluation, instead
* of calculating everything up-front, as `walk()` does.
*
* The JDK's `java.io.Files.walk()` and `java.io.Files.walkTree()`
* functions provide a similar capability, starting in JDK 8.
*
* @param file The `File` object, presumed to represent a directory.
* @param topdown If `true` (the default), the stream will be generated
* top down. If `false`, it'll be generated bottom-up.
*
* @return a stream of `File` objects.
*/
def listRecursively(file: File, topdown: Boolean = true): LazyList[File] = {
def go(list: List[File]): LazyList[File] = {
// See http://www.nurkiewicz.com/2013/05/lazy-sequences-in-scala-and-clojure.html
list match {
case Nil => LazyList.empty[File]
case f :: tail =>
val list = if (f.isDirectory) f.listFiles.toList else Nil
if (topdown)
f #:: go(list ++ tail)
else
go(list ++ tail) :+ f
}
}
if (file.isDirectory)
go(file.listFiles.toList)
else
LazyList.empty[File]
}
/** Split a path into its constituent components. If the path is
* absolute, the first piece will have a file separator in the
* beginning. Examples:
*
*
*
* Input
* Output
*
*
* ""
* List("")
*
*
* "/"
* List("/")
*
*
* "foo"
* List("foo")
*
*
* "foo/bar"
* List("foo", "bar")
*
*
* "."
* List(".")
*
*
* "../foo"
* List("..", "foo")
*
*
* "./foo"
* List(".", "foo")
*
*
* "/foo/bar/baz"
* List("/foo", "bar", "baz")
*
*
* "foo/bar/baz"
* List("foo", "bar", "baz")
*
*
* "/foo"
* List("/foo")
*
*
*
* @param path the path
* @param fileSep the file separator to use. Defaults to the value of
* the "file.separator" property.
*
* @return the component pieces.
*/
def splitPath(path: String, fileSep: String = fileSeparator): List[String] = {
// Split with the path separator character, rather than the path
// separator string. Using the string causes Scala to interpret it
// as a regular expression, which causes problems when the separator
// is a backslash (as on Windows). We could escape the backslash,
// but it's just as easy to split on the character, not the string,
// Null guard.
val nonNullPath = Option(path).getOrElse("")
// Special case for Windows. (Stupid drive letters.)
val (prefix, usePath) =
if (fileSep == "\\")
splitDrivePath(nonNullPath)
else
("", nonNullPath)
// If there are leading file separator characters, split() will
// produce extra empty array elements. Prevent that.
val subpath = usePath.foldLeft("") {
(c1, c2) => // Note: c1 and c2 are strings, not characters
if (c1 == fileSep)
c2.toString
else
s"$c1${c2.toString}"
}
val absolute = path.startsWith(fileSep) || (prefix != "")
// Split on the character, not the full string. Splitting on a string
// uses regular expression semantics, which will fail if this is
// Windows (and the file separator is "\"). Windows is a pain in the
// ass.
val pieces = (subpath split fileSep(0)).toList
if (absolute) {
pieces match {
case Nil => List[String](prefix + fileSep)
case head :: tail => (prefix + fileSep + head) :: tail
}
}
else
pieces
}
/** Join components of a path together, using the specified file separator.
* Note that `java.nio.file.Paths.get()` can be used to join paths, as
* well.
*
* @param fileSep the file separator to use
* @param pieces path pieces
*
* @return a composite path
*/
def joinPath(fileSep: String, pieces: Seq[String]): String =
pieces mkString fileSep
/** Join components of a path together, using the current file separator.
* Note that `java.nio.file.Paths.get()` can be used to join paths, as
* well.
*
* @param pieces path pieces
*
* @return a composite path
*/
def joinPath(pieces: String*): String =
joinPath(fileSeparator, pieces.toList)
/** Join components of a path together, using the current file separator.
* Note that `java.nio.file.Paths.get()` can be used to join paths, as
* well.
*
* @param pieces path pieces
*
* @return a composite path
*/
def joinPath(pieces: File*): File =
new File(joinPath(fileSeparator, pieces.toList.map(_.getName)))
/** Join components of a path together, using the current file separator;
* then, normalize the result.
*
* @param pieces path pieces
*
* @return a composite, normalized path
*
* @see [[joinPath(pieces:String*):String*]]
* @see [[normalizePath]]
*/
def joinAndNormalizePath(pieces: String*): String = {
normalizePath(joinPath(pieces: _*))
}
/** Join components of a path together, using the current file separator;
* then, normalize the result.
*
* @param pieces path pieces
*
* @return a composite, normalized path
*
* @see [[joinPath(pieces:String*):String*]]
* @see [[normalizePath]]
*/
def joinAndNormalizePath(pieces: File*): File = {
new File(normalizePath(joinPath(pieces: _*).getPath))
}
/** Determine the temporary directory to use.
*
* @return the temporary directory
*/
def temporaryDirectory: File = {
import grizzled.sys.OperatingSystem._
def guess: String = {
grizzled.sys.os match {
case Posix => "/tmp"
case Mac => "/tmp"
case (Windows | WindowsCE | OS2 | NetWare) => """C:\TEMP"""
case _ => "/tmp"
}
}
val sysProp = System getProperty "java.io.tmpdir"
val tempDirName = Option(sysProp).getOrElse(guess)
new File(tempDirName)
}
/** Create a temporary directory. This is now just a simple front-end
* to `java.nio.file.Files.createTempDirectory()`, and it's deprecated.
* Use `java.nio.file.Files.createTempDirectory()` directly.
*
* @param prefix Prefix for directory name
* @param maxTries Maximum number of times to try creating the
* directory before giving up. '''Ignored now.'''
*
* @return A `Success` of the directory, or a `Failure` with any error
* that occurs.
*/
@deprecated("Use java.nio.file.Files.createTempDirectory()", "4.10.0")
def makeTemporaryDirectory(prefix: String, maxTries: Int = 3): Try[File] = {
Try {
Files.createTempDirectory(prefix).toFile
}
}
/** Allow execution of a block of code within the context of a temporary
* directory. The temporary directory is cleaned up after the operation
* completes. This version throws exceptions. Use
* [[tryWithTemporaryDirectory]] if you'd prefer a `Try`.
*
* @param prefix file name prefix to use
* @param action action to perform
*
* @return whatever the action returns. Errors are thrown as `IOException`.
*/
@SuppressWarnings(Array("org.wartremover.warts.Throw"))
def withTemporaryDirectory[T](prefix: String)(action: File => T): T = {
import grizzled.file.Implicits._
Try { Files.createTempDirectory(prefix) } match {
case Failure(ex) => throw ex
case Success(dir) =>
try {
action(dir.toFile)
}
finally {
dir.toFile.deleteRecursively()
}
}
}
/** Similar to [[withTemporaryDirectory]], this function creates a temporary
* directory, runs a block of code, and recursively removes the temporary
* directory when the code block returns. Unlike [[withTemporaryDirectory]],
* `tryWithTemporaryDirectory`:
*
* - returns a `Try`, instead of throwing or propagating exceptions
* - passes a `java.nio.files.Path` to the code block, instead of a
* `java.io.File`
*
* @param prefix the desired prefix to use when generating the directory
* name
* @param action the code to run
* @tparam T the type of the code's return value
*
* @return A `Success` containing the result of the code block, or a
* `Failure` if an exception is thrown.
*/
def tryWithTemporaryDirectory[T](prefix: String)(action: Path => T): Try[T] = {
import grizzled.file.Implicits._
Try { Files.createTempDirectory(prefix) }.flatMap { dir: Path =>
val res = Try { action(dir) }
dir.toFile.deleteRecursively()
res
}
}
/** Copy multiple files to a target directory. Also see the version of this
* method that takes only one file.
*
* @param files An `Iterable` of file names to be copied
* @param targetDir Path name to target directory
* @param createTarget `true` to create the target directory,
* `false` to throw an exception if the
* directory doesn't already exist.
*
* @return `Success(true)` if the copy worked. `Failure(exception)` on
* error.
*/
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
def copy(files: Iterable[String],
targetDir: String,
createTarget: Boolean = true): Try[Boolean] = {
val target = new File(targetDir)
if ((! target.exists()) && createTarget && (! target.mkdirs())) {
Failure(new IOException(
s"""Unable to create target directory "$targetDir"."""
))
}
else if (target.exists() && (! target.isDirectory)) {
Failure(new IOException(
s"""Cannot copy files to non-directory "$targetDir"."""
))
}
else if (! target.exists()) {
Failure(new FileDoesNotExistException(
s"""Target directory "$targetDir" does not exist."""
))
}
else {
val results = for (file <- files) yield {
// The .get() forces a failure if a specific copy fails.
copyFile(file, targetDir + fileSeparator + basename(file))
}
results.toSeq.filter(_.isFailure) match {
case Seq(head, _*) => Try { head.get }.map { _ => false }
case s if s.isEmpty => Success(true)
}
}
}
/** Copy a file to a directory. The JDK's
* [[https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...) java.nio.file.Files.copy()]]
* function provides a similar capability.
*
* @param file Path name of the file to copy
* @param targetDir Path name to target directory
* @param createTarget `true` to create the target directory,
* `false` to throw an exception if the
* directory doesn't already exist.
*
* @return `Success(true)` if the copy worked. `Failure(exception)` on
* error.
*/
def copy(file: String,
targetDir: String,
createTarget: Boolean): Try[Boolean] = {
copy(List[String](file), targetDir, createTarget)
}
/** Copy a file to a directory. If the target directory does not exist,
* it is created. The JDK's
* [[https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...) java.nio.file.Files.copy()]]
* function provides a similar capability.
*
* @param file Path name of the file to copy
* @param targetDir Path name to target directory
*
* @return `Success(true)` if the copy worked. `Failure(exception)` on
* error.
*/
def copy(file: String, targetDir: String): Try[Boolean] = {
copy(file, targetDir, createTarget = true)
}
/** Copy a source file to a target file, using binary copying. The source
* file must be a file. The target path can be a file or a directory; if
* it is a directory, the target file will have the same base name as
* as the source file.
*
* The JDK's
* [[https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#copy(java.nio.file.Path,%20java.nio.file.Path,%20java.nio.file.CopyOption...) java.nio.file.Files.copy()]]
* function provides a similar capability.
*
* @param sourcePath path to the source file
* @param targetPath path to the target file or directory
*
* @return A `Success` with the full path of the target file, or
* `Failure(exception)`
*/
def copyFile(sourcePath: String, targetPath: String): Try[String] = {
copyFile(new File(sourcePath), new File(targetPath)).map {_.getPath}
}
/** Copy a source file to a target file, using binary copying. The source
* file must be a file. The target path can be a file or a directory; if
* it is a directory, the target file will have the same base name as
* as the source file.
*
* The JDK's `java.nio.file.Files.copy()` function provides a similar
* capability.
*
* @param source path to the source file
* @param target path to the target file or directory
*
* @return A `Success` containing the full path of the target file,
* or `Failure(exception)`
*/
def copyFile(source: File, target: File): Try[File] = {
import java.io.{BufferedInputStream, BufferedOutputStream,
FileInputStream, FileOutputStream}
import grizzled.util.withResource
val targetFile =
if (target.isDirectory)
new File(joinPath(target.getPath, basename(source.getName)))
else
target
Try {
import grizzled.util.CanReleaseResource.Implicits.CanReleaseCloseable
withResource(new BufferedInputStream(new FileInputStream(source))) { in =>
withResource(new BufferedOutputStream(new FileOutputStream(targetFile))) { out =>
in.copyTo(out)
targetFile
}
}
}
}
/** Recursively copy a source directory and its contents to a target
* directory. Creates the target directory if it does not exist.
*
* @param sourceDir the source directory
* @param targetDir the target directory
*
* @return `Success(true)` if the copy worked. `Failure(exception)` on
* error.
*/
def copyTree(sourceDir: String, targetDir: String): Try[Boolean] =
copyTree(new File(sourceDir), new File(targetDir))
/** Recursively copy a source directory and its contents to a target
* directory. Creates the target directory if it does not exist.
*
* @param sourceDir the source directory
* @param targetDir the target directory
*
* @return `Success(true)` if the copy worked. `Failure(exception)` on
* error.
*/
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def copyTree(sourceDir: File, targetDir: File): Try[Boolean] = {
if (! sourceDir.exists()) {
Failure(new FileDoesNotExistException(sourceDir.getPath))
}
else if (! sourceDir.isDirectory) {
Failure(new IOException(
s"""Source directory "${sourceDir.getPath}" is not a directory."""
))
}
else {
val files = sourceDir
.list
.map { f: String =>
(new File(sourceDir, f), new File(targetDir, f))
}
targetDir.mkdirs
// Not sure why Wart Remover is complaining about inferred Any here,
// since everything is explicit.
files.foreach { case (src: File, target: File) =>
if (src.isDirectory)
copyTree(src, target)
else
copyFile(src, target)
}
Success(true)
}
}
/** Recursively copy a source directory and its contents to a target
* directory. Creates the target directory if it does not exist.
*
* @param sourceDir the source directory
* @param targetDir the target directory
*
* @return `Success(true)` if the copy worked. `Failure(exception)` on
* error.
*/
def copyTree(sourceDir: Path, targetDir: Path): Try[Boolean] = {
if (! Files.exists(sourceDir)) {
Failure(new FileDoesNotExistException(sourceDir.toAbsolutePath.toString))
}
else if (! Files.isDirectory(sourceDir)) {
Failure(new IOException(
s"""Source "${sourceDir.toAbsolutePath.toString}" is not a directory."""
))
}
else {
import grizzled.ScalaCompat.CollectionConverters._
Files.createDirectories(targetDir)
val targetAsString = targetDir.toString
Files
.walk(sourceDir)
.iterator
.asScala
.filter { path: Path => ! Files.isDirectory(path) }
.foreach { path: Path =>
// These paths contain the parent directory.
val src = sourceDir.toString
val p = path.toString
val minusParent = if (p startsWith src) p.drop(src.length + 1) else p
val targetFile = Paths.get(targetAsString, minusParent)
val targetParent = dirname(targetFile.toString)
Files.createDirectories(Paths.get(targetParent))
Files.copy(path, targetFile)
}
Success(true)
}
}
/** Recursively remove a directory tree. This function is conceptually
* equivalent to `rm -r` on a Unix system.
*
* @param dir The directory
*/
def deleteTree(dir: String): Unit = deleteTree(new File(dir))
/** Recursively remove a directory tree. This function is conceptually
* equivalent to `rm -r` on a Unix system.
*
* @param dir The directory
*
* @return `Failure(exception)` on error, `Success(total)` on success. `total`
* is the total number of deleted files.
*/
def deleteTree(dir: File): Try[Int] = {
def deleteOne(f: File): Try[Int] = {
if (! f.delete)
Failure(new IOException(s"Can't delete '${f.toString}'"))
else
Success(1)
}
if (! dir.exists)
Success(0)
else if (! dir.isDirectory)
Failure(new IOException(s""""${dir.toString}" is not a directory."""))
else {
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
val treeResults = dir.listFiles.map { f =>
val t = Try {
if (f.isDirectory)
deleteTree(f)
else
deleteOne(f)
}
// Force an abort on first failure.
t.get
}
treeResults.find { t => t.isFailure }.getOrElse {
deleteOne(dir)
}
}
}
/** Recursively remove a directory tree. This function is conceptually
* equivalent to `rm -r` on a Unix system.
*
* @param dir The directory
*
* @return `Failure(exception)` on error, `Success(total)` on success. `total`
* is the total number of deleted files.
*/
def deleteTree(dir: Path): Try[Int] = deleteTree(dir.toFile)
/** Similar to the Unix ''touch'' command, this function:
*
* - updates the access and modification times for any existing files
* in a list of files
* - creates an nonexistent files in the list
*
* If any file in the list is a directory, this method will return an error.
*
* @param files Iterable of files to touch
* @param time Set the last-modified time to this time, or to the current
* time if this parameter is negative.
*
* @return `Failure(exception)` on error. `Success(total)` on success.
*/
def touchMany(files: Iterable[String], time: Long = -1): Try[Int] = {
val useTime = if (time < 0) System.currentTimeMillis else time
Try {
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
val results = files.map { name =>
// Force a failure on error.
touch(name, time).get
1
}
results.sum
}
}
/** Similar to the Unix `touch` command, this function:
*
* - updates the access and modification times for a file
* - creates the file if it does not exist
*
* If the file is a directory, this method will return an error.
*
* @param path The file to touch
* @param time Set the last-modified time to this time, or to the current
* time if this parameter is negative.
*
* @return `Failure(exception)` on error, `Success(true)` on success
*/
def touch(path: String, time: Long = -1): Try[Boolean] = {
val file = new File(path)
if (file.isDirectory)
Failure(new Exception(s"""File "$path" is a directory."""))
else if (! file.exists) {
Try {
file.createNewFile()
}
.flatMap { succeeded =>
if (! succeeded)
Failure(new IOException(s"""Unable to create "$path""""))
else
Success(true)
}
}
else {
val useTime = if (time < 0) System.currentTimeMillis else time
if (! file.setLastModified(useTime))
Failure(new IOException(s"""Unable to set time on "$path""""))
else
Success(true)
}
}
private lazy val DrivePathPattern = "^([A-Za-z]?:)?(.*)$".r
/** Split a Windows-style path into drive name and path portions.
*
* @param path the path
*
* @return a (drive, path) tuple, either component of which can be
* * an empty string
*/
def splitDrivePath(path: String): (String, String) = {
path match {
case DrivePathPattern(driveSpec, subPath) =>
Option(driveSpec)
.map {
case ":" => ("", subPath)
case _ => (driveSpec, subPath)
}
.getOrElse(("", subPath))
case _ => ("", path)
}
}
/** Converts a path name from its operating system-specific format to a
* universal path notation. Universal path notation always uses a
* Unix-style "/" to separate path elements. A universal path can be
* converted to a native (operating system-specific) path via the
* `native_path()` function. Note that on POSIX-compliant systems,
* this function simply returns the `path` parameter unmodified.
*
* @param path the path to convert to universal path notation
*
* @return the universal path
*/
def universalPath(path: String): String = makeUniversalPath(path)
/** Converts a path name from universal path notation to the operating
* system-specific format. Universal path notation always uses a
* Unix-style "/" to separate path elements. A native path can be
* converted to a universal path via the `universal_path()`
* function. Note that on POSIX-compliant systems, this function simply
* returns the `path` parameter unmodified.
*
* @param path the path to convert from universtal to native path notation
*
* @return the native path
*/
def nativePath(path: String): String = makeNativePath(path)
/** Normalize a path, eliminating double slashes, resolving embedded
* ".." strings (e.g., "/foo/../bar" becomes "/bar"), etc. Works for
* Windows and Posix operating systems.
*
* @param path the path
*
* @return the normalized path
*/
def normalizePath(path: String): String = doPathNormalizing(path)
/** Normalize a path, eliminating double slashes, resolving embedded
* ".." strings (e.g., "/foo/../bar" becomes "/bar"), etc. Works for
* Windows and Posix operating systems.
*
* @param path the path
*
* @return the normalized path
*/
def normalizePath(path: Path): Path = {
Paths.get(doPathNormalizing(path.toString))
}
/** Normalize a Windows path name. Handles UNC paths. Adapted from the
* Python version of normpath() in Python's `os.ntpath` module.
*
* @param path the path
*
* @return the normalized path
*/
def normalizeWindowsPath(path: String): String = {
// We need to be careful here. If the prefix is empty, and the path
// starts with a backslash, it could either be an absolute path on
// the current drive (\dir1\dir2\file) or a UNC filename
// (\\server\mount\dir1\file). It is therefore imperative NOT to
// collapse multiple backslashes blindly in that case. The code
// below preserves multiple backslashes when there is no drive
// letter. This means that the invalid filename \\\a\b is preserved
// unchanged, where a\\\b is normalized to a\b.
val (prefix, newPath) = splitDrivePath(path) match {
case ("", subPath) =>
// No drive letter - preserve initial backslashes
(subPath takeWhile (_ == '\\') mkString "",
subPath dropWhile (_ == '\\') mkString "")
case (pfx, subPath) =>
// We have a drive letter.
(pfx + "\\", subPath dropWhile (_ == '\\') mkString "")
}
// Normalize the path pieces. Note: normalizePathPieces() doesn't
// handle leading ".." in an absolute path, such as "\\..\\..". We
// handle that later.
val piecesTemp = normalizePathPieces(newPath.split("\\\\").toList)
// Remove any leading ".." that shouldn't be there.
val newPieces =
if (prefix == "\\")
piecesTemp dropWhile (_ == "..")
else
piecesTemp
// If the path is now empty, substitute ".".
if ((prefix.length == 0) && newPieces.isEmpty)
"."
else
prefix + (newPieces mkString "\\")
}
/** Adapted from the Python version of normpath() in Python's
* `os.posixpath` module.
*
* @param path the path
*
* @return the normalized path
*/
def normalizePosixPath(path: String): String = {
path match {
case "" => "."
case "." => "."
case _ =>
// POSIX allows one or two initial slashes, but treats
// three or more as a single slash. We don't do that here.
// Two initial slashes is also collapsed into one.
val initialSlashes =
if (path.startsWith("/"))
1
else
0
// Normalize the path pieces. Note: normalizePathPieces()
// doesn't handle leading ".." in an absolute path, such as
// "/../..". We handle that later.
//
// Note: Must also account for a single leading ".", which
// must be preserved
val pieces =
path.split("/").toList match {
case Nil => Nil
case "." :: remainder => remainder
case other => other
}
val normalizedPieces1 = normalizePathPieces(pieces)
// Remove any leading ".." that shouldn't be there.
val normalizedPieces2 =
if (path startsWith "/")
normalizedPieces1 dropWhile (_ == "..")
else
normalizedPieces1
val result =
("/" * initialSlashes) + (normalizedPieces2 mkString "/")
// An empty string is "."
if (result == "")
"."
else
result
}
}
/** Find the longest common path prefix from a list of paths. Based on
* [[https://rosettacode.org/wiki/Find_common_directory_path#Advanced]]
*
* @param paths the paths
*
* @return the longest common path, which might be the empty string
*/
// reduceLeft() is okay here, since we're checking the size first.
@SuppressWarnings(Array("org.wartremover.warts.TraversableOps"))
def longestCommonPathPrefix(paths: List[String]): String = {
val PathSep = "/"
val BoundaryRe = s"(?=[$PathSep])(?<=[^$PathSep])|(?=[^$PathSep])(?<=[$PathSep])"
def common(a: List[String], b: List[String]): List[String] = {
(a, b) match {
case (a :: as, b :: bs) if a equals b => a :: common(as, bs)
case _ => Nil
}
}
if (paths.length < 2) {
paths.headOption.getOrElse("")
}
else {
val uPaths = paths
val res = paths
// Convert all paths to "universal" paths (i.e., with "/" characters,
// even if we're on Windows). We'll convert back when we're done.
.map(universalPath)
// Split on path boundaries
.map { _.split(BoundaryRe).toList }
// Find the common prefix
.reduceLeft(common)
// Rebuild
.mkString
// Convert back to a native path
nativePath(res)
}
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
/** Convert a file into a path array. Borrowed from SBT source code.
*/
private def toPathArray(file: File): Array[String] = {
@tailrec def toPathList(f: File, current: List[String]): List[String] = {
// Can't use map, to preserve tail-recursion.
Option(f) match {
case Some(f2) => toPathList(f2.getParentFile, f2.getName :: current)
case None => current
}
}
toPathList(file.getCanonicalFile, Nil).toArray
}
/** Get the length of the common prefix between two arrays.
*/
private def commonPrefix[T](a: Array[T], b: Array[T]): Int = {
@tailrec def common(count: Int): Int = {
if ((count >= a.length) || (count >= b.length) || (a(count) != b(count)))
count
else
common(count + 1)
}
common(0)
}
/** For the eglob algorithm to work, the pattern needs to be split into a
* (directory, subpattern) pair, where the subpattern is relative. This
* splitting operating is operating system-dependent, largely because
* of Windows' stupid drive letters. This variable holds a partially
* applied function for the splitter, determined the first time it is
* referenced. That way, eglob() doesn't do this same match on every
* call.
*/
private lazy val eglobPatternSplitter = os match {
case (Mac | Posix) => splitPosixEglobPattern _
case Windows => splitWindowsEglobPattern _
case _ => splitPosixEglobPattern _
}
/** Windows pattern splitter for eglob(). See description for the
* eglobPatternSplitter value, above.
*
* @param pattern the pattern to split
*
* @return a (directory, subpattern) tuple
*/
private def splitWindowsEglobPattern(pattern: String): (String, String) = {
splitDrivePath(pattern) match {
case ("", "") =>
(".", ".")
case ("", path) =>
(path, ".")
case (drive, "") =>
(".", drive)
case (drive, path) =>
// Hack: Can't handle non-absolute paths in a drive.
// Pretend a drive letter means "absolute". Note that
// "drive" can be empty here, which is fine.
if (path(0) == '\\')
(path drop 1, drive + "\\")
else
(path, drive + "\\")
}
}
/** Posix pattern splitter for eglob(). See description for the
* eglobPatternSplitter value, above.
*
* @param pattern the pattern to split
*
* @return a (directory, subpattern) tuple
*/
private def splitPosixEglobPattern(pattern: String): (String, String) = {
if (pattern.length == 0)
(".", ".")
else if (pattern(0) == fileSeparatorChar)
(pattern drop 1, "/")
else
(pattern, ".")
}
/** Path normalization is operating system-specific. This value
* holds the real path normalizer, determined once.
*/
private lazy val doPathNormalizing = os match {
case (Mac | Posix) => normalizePosixPath _
case Windows => normalizeWindowsPath _
case _ => (path: String) => path
}
/** Shared between normalizeWindowsPath() and normalizePosixPath(),
* this function normalizes the pieces of a path, handling embedded "..",
* empty elements (from splitting when there are adjacent file separators),
* etc.
*
* @param pieces path components, with no separators
*
* @return sanitized list of path components
*/
private def normalizePathPieces(pieces: List[String]): List[String] = {
pieces match {
case Nil =>
Nil
case "" :: tail =>
normalizePathPieces(tail)
case "." :: tail =>
normalizePathPieces(tail)
case a :: ".." :: tail =>
normalizePathPieces(tail)
case head :: tail =>
List[String](head) ++ normalizePathPieces(tail)
}
}
/** Native-to-universal path conversion is operating system-specific.
* These values hold the real converters, determined once.
*/
private lazy val makeUniversalPath: (String) => String = os match {
case (Mac | Posix) =>
(path: String) => path
case Windows =>
(path: String) => path.replace(fileSeparator, "/")
case _ =>
(path: String) => path
}
private lazy val makeNativePath: (String) => String = os match {
case (Mac | Posix) =>
(path: String) => path
case Windows =>
(path: String) => path.replace("/", fileSeparator)
case _ =>
(path: String) => path
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy