All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sbt.io.IO.scala Maven / Gradle / Ivy

There is a newer version: 1.10.0
Show newest version
/*
 * sbt IO
 * Copyright Scala Center, Lightbend, and Mark Harrah
 *
 * Licensed under Apache License 2.0
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package sbt.io

import java.io._
import java.net.{ URI, URISyntaxException, URL }
import java.nio.charset.Charset
import java.nio.file.attribute.PosixFilePermissions
import java.nio.file.{ Path => NioPath, _ }
import java.util.{ Locale, Properties }
import java.util.jar.{ Attributes, JarEntry, JarFile, JarOutputStream, Manifest }
import java.util.zip.{ CRC32, ZipEntry, ZipInputStream, ZipOutputStream }

import sbt.internal.io.ErrorHandling.translate
import sbt.internal.io.{ Milli, Retry }
import sbt.io.Using._
import sbt.nio.file.FileTreeView

import scala.Function.tupled
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.collection.immutable
import scala.collection.immutable.TreeSet
import scala.collection.mutable.{ HashMap, HashSet }
import scala.reflect.{ Manifest => SManifest }
import scala.util.control.Exception._
import scala.util.control.NonFatal

/** A collection of File, URL, and I/O utility methods. */
object IO {

  /** The maximum number of times a unique temporary filename is attempted to be created. */
  private val MaximumTries = 10

  /** The producer of randomness for unique name generation. */
  private lazy val random = new java.util.Random
  val temporaryDirectory = new File(System.getProperty("java.io.tmpdir"))

  /** The size of the byte or char buffer used in various methods. */
  private val BufferSize = 8192

  /** File scheme name */
  private[sbt] val FileScheme = "file"

  /** The newline string for this system, as obtained by the line.separator system property. */
  val Newline = System.getProperty("line.separator")

  val utf8 = Charset.forName("UTF-8")

  private lazy val jrtFs = FileSystems.getFileSystem(URI.create("jrt:/"))

  /**
   * Returns the NIO Path to the directory, Java module, or the JAR file containing the class file `cl`.
   * If the location cannot be determined, an error is generated.
   * Note that for JDK 11 onwards, a module will return a jrt path.
   */
  def classLocationPath(cl: Class[?]): NioPath = {
    val u = classLocation(cl)
    val p = u.getProtocol match {
      case FileScheme => Option(toFile(u).toPath)
      case "jar"      => urlAsFile(u) map { _.toPath }
      case "jrt"      => Option(IO.jrtFs.getPath(u.getPath))
      case _          => None
    }
    p.getOrElse(sys.error(s"Unable to create File from $u for $cl"))
  }

  /**
   * Returns a NIO Path to the directory, Java module, or the JAR file for type `A` (as determined by an implicit Manifest).
   * If the location cannot be determined, an error is generated.
   * Note that for JDK 11 onwards, a module will return a jrt path.
   */
  def classLocationPath[A](implicit mf: SManifest[A]): NioPath =
    classLocationPath(mf.runtimeClass)

  /**
   * Returns the directory, Java module, or the JAR containing the class file `cl`.
   * If the location cannot be determined or it is not a file, an error is generated.
   * Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`, so it will return `None`.
   */
  def classLocationFileOption(cl: Class[?]): Option[File] = {
    val u = classLocation(cl)
    urlAsFile(u)
  }

  /**
   * Returns the directory, Java module, or the JAR containing the class file for type `T` (as determined by an implicit Manifest).
   * If the location cannot be determined or it is not a file, an error is generated.
   * Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`, so it will return `None`.
   */
  def classLocationFileOption[A](implicit mf: SManifest[A]): Option[File] =
    classLocationFileOption(mf.runtimeClass)

  /**
   * Returns the directory, Java module, or the JAR file containing the class file `cl`.
   * If the location cannot be determined or it is not a file, an error is generated.
   * Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`.
   */
  @deprecated(
    "classLocationFile may not work on JDK 11. Use classfileLocation, classLocationFileOption, or classLocationPath instead.",
    "1.3.0"
  )
  def classLocationFile(cl: Class[?]): File =
    classLocationFileOption(cl).getOrElse(sys.error(s"Unable to create File from $cl"))

  /**
   * Returns the directory, Java module, or the JAR file containing the class file for type `T` (as determined by an implicit Manifest).
   * If the location cannot be determined, an error is generated.
   * Note that for JDK 11 onwards, the returned module path cannot be expressed as `File`.
   */
  @deprecated(
    "classLocationFile may not work on JDK 11. Use classfileLocation, classLocationFileOption, or classLocationPath instead.",
    "1.3.0"
  )
  def classLocationFile[T](implicit mf: SManifest[T]): File = classLocationFile(mf.runtimeClass)

  /**
   * Returns the URL to the directory, Java module, or the JAR file containing the class file `cl`.
   * If the location cannot be determined or it is not a file, an error is generated.
   * Note that for JDK 11 onwards, a module will return a jrt URL such as `jrt:/java.base`.
   */
  def classLocation(cl: Class[?]): URL = {
    def localcl: Option[URL] =
      Option(cl.getProtectionDomain.getCodeSource) flatMap { codeSource =>
        Option(codeSource.getLocation)
      }
    // This assumes that classes without code sources are System classes, and thus located in jars.
    // It returns a URL that looks like jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar!/java/lang/Integer.class
    val clsfile = s"${cl.getName.replace('.', '/')}.class"
    def syscl: Option[URL] =
      Option(ClassLoader.getSystemClassLoader) flatMap { classLoader =>
        Option(classLoader.getResource(clsfile))
      }
    try {
      localcl
        .orElse(syscl)
        .map(url =>
          url.getProtocol match {
            case "jar" =>
              val path = url.getPath
              val end = path.indexOf('!')
              new URI(
                if (end == -1) path
                else path.substring(0, end)
              ).toURL
            case "jrt" =>
              val path = url.getPath
              val end = path.indexOf('/', 1)
              new URI(
                s"jrt://${if (end == -1) path else path.substring(0, end)}"
              ).toURL
            case _ => url
          }
        )
        .getOrElse(sys.error("No class location for " + cl))
    } catch {
      case NonFatal(e) =>
        e.printStackTrace()
        throw e
    }
  }

  /**
   * Returns the URL to the directory, Java module, or the JAR file containing the class file `cl`.
   * If the location cannot be determined or it is not a file, an error is generated.
   * Note that for JDK 11 onwards, a module will return a jrt path.
   */
  def classLocation[A](implicit mf: SManifest[A]): URL =
    classLocation(mf.runtimeClass)

  /**
   * Returns a URL for the classfile containing the given class file for type `T` (as determined by an implicit Manifest).
   * If the location cannot be determined, an error is generated.
   */
  def classfileLocation[T](implicit mf: SManifest[T]): URL = classfileLocation(mf.runtimeClass)

  /**
   * Returns a URL for the classfile containing the given class
   * If the location cannot be determined, an error is generated.
   */
  def classfileLocation(cl: Class[?]): URL = {
    val clsfile = s"${cl.getName.replace('.', '/')}.class"
    def localcl: Option[URL] =
      Option(cl.getClassLoader) flatMap { classLoader =>
        Option(classLoader.getResource(clsfile))
      }
    def syscl: Option[URL] =
      Option(ClassLoader.getSystemClassLoader) flatMap { classLoader =>
        Option(classLoader.getResource(clsfile))
      }
    try {
      localcl
        .orElse(syscl)
        .getOrElse(sys.error("No class location for " + cl))
    } catch {
      case NonFatal(e) =>
        e.printStackTrace()
        throw e
    }
  }

  /**
   * Constructs a File corresponding to `url`, which must have a scheme of `file`.
   * This method properly works around an issue with a simple conversion to URI and then to a File.
   *
   * On Windows this can accept the following patterns of URLs:
   *
   * `val u0 = new URL("file:C:\\Users\\foo/.sbt/preloaded")`,
   * `val u1 = new URL("file:/C:\\Users\\foo/.sbt/preloaded")`,
   * `val u2 = new URL("file://unc/Users/foo/.sbt/preloaded")`,
   * `val u3 = new URL("file:///C:\\Users\\foo/.sbt/preloaded")`, and
   * `val u4 = new URL("file:////unc/Users/foo/.sbt/preloaded")`.
   */
  def toFile(url: URL): File =
    try {
      uriToFile(url.toURI)
    } catch { case _: URISyntaxException => new File(url.getPath) }

  def toFile(uri: URI): File =
    try {
      uriToFile(uri)
    } catch { case _: URISyntaxException => new File(uri.getPath) }

  /** Converts the given URL to a File.  If the URL is for an entry in a jar, the File for the jar is returned. */
  def asFile(url: URL): File = urlAsFile(url) getOrElse sys.error("URL is not a file: " + url)
  def urlAsFile(url: URL): Option[File] =
    url.getProtocol match {
      case FileScheme => Some(toFile(url))
      case "jar" =>
        val path = url.getPath
        val end = path.indexOf('!')
        Some(uriToFile(if (end == -1) path else path.substring(0, end)))
      case _ => None
    }

  private[this] def uriToFile(uriString: String): File = uriToFile(new URI(uriString))

  /**
   * Converts the given file URI to a File.
   */
  private[this] def uriToFile(uri: URI): File = {
    val part = uri.getSchemeSpecificPart
    // scheme might be omitted for relative URI reference.
    assert(
      Option(uri.getScheme) match {
        case None | Some(FileScheme) => true
        case _                       => false
      },
      s"Expected protocol to be '$FileScheme' or empty in URI $uri"
    )
    Option(uri.getAuthority) match {
      case None if part startsWith "/" => new File(uri)
      case _                           =>
        // https://github.com/sbt/sbt/issues/564
        // https://github.com/sbt/sbt/issues/3086
        // http://blogs.msdn.com/b/ie/archive/2006/12/06/file-uris-in-windows.aspx
        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5086147
        // The specific problem here is that `uri` will have a defined authority component for UNC names like //foo/bar/some/path.jar
        // but the File constructor requires URIs with an undefined authority component.
        if (!(part startsWith "/") && (part contains ":")) new File("//" + part)
        else new File(part)
    }
  }

  def assertDirectory(file: File) =
    assert(
      file.isDirectory,
      (if (file.exists) "Not a directory: " else "Directory not found: ") + file
    )

  def assertDirectories(file: File*) = file.foreach(assertDirectory)

  // "base.extension" -> (base, extension)
  /**
   * Splits the given string into base and extension strings.
   * If `name` contains no period, the base string is the input string and the extension is the empty string.
   * Otherwise, the base is the substring up until the last period (exclusive) and
   * the extension is the substring after the last period.
   *
   * For example, `split("Build.scala") == ("Build", "scala")`
   */
  def split(name: String): (String, String) = {
    val lastDot = name.lastIndexOf('.')
    if (lastDot >= 0)
      (name.substring(0, lastDot), name.substring(lastDot + 1))
    else
      (name, "")
  }

  /**
   * Each input file in `files` is created if it doesn't exist.
   * If a file already exists, the last modified time is set to the current time.
   * It is not guaranteed that all files will have the same last modified time after this call.
   */
  def touch(files: Traversable[File]): Unit = files foreach (f => { touch(f); () })

  /**
   * Creates a file at the given location if it doesn't exist.
   * If the file already exists and `setModified` is true, this method sets the last modified time to the current time.
   */
  def touch(file: File, setModified: Boolean = true): Unit = Retry {
    val absFile = file.getAbsoluteFile
    createDirectory(absFile.getParentFile)
    val created = translate("Could not create file " + absFile) { absFile.createNewFile() }
    if (created || absFile.isDirectory)
      ()
    else if (setModified && !setModifiedTimeOrFalse(absFile, System.currentTimeMillis))
      sys.error("Could not update last modified time for file " + absFile)
  }

  /** Creates directories `dirs` and all parent directories.  It tries to work around a race condition in `File.mkdirs()` by retrying up to a limit. */
  def createDirectories(dirs: Traversable[File]): Unit =
    dirs.foreach(createDirectory)

  /** Creates directory `dir` and all parent directories.  It tries to work around a race condition in `File.mkdirs()` by retrying up to a limit. */
  def createDirectory(dir: File): Unit = {
    def failBase = "Could not create directory " + dir
    // Need a retry because Files.createDirectories may fail before succeeding on (at least) windows.
    val path = dir.toPath
    try
      Retry(
        try Files.createDirectories(path)
        catch { case _: IOException if Files.isDirectory(path) => },
        excludedExceptions = classOf[FileAlreadyExistsException]
      )
    catch { case e: IOException => throw new IOException(failBase + ": " + e, e) }
    ()
  }

  /** Gzips the file 'in' and writes it to 'out'.  'in' cannot be the same file as 'out'. */
  def gzip(in: File, out: File): Unit = {
    require(in != out, "Input file cannot be the same as the output file.")
    Using.fileInputStream(in) { inputStream =>
      Using.fileOutputStream()(out) { outputStream =>
        gzip(inputStream, outputStream)
      }
    }
  }

  /** Gzips the InputStream 'in' and writes it to 'output'.  Neither stream is closed. */
  def gzip(input: InputStream, output: OutputStream): Unit =
    gzipOutputStream(output)(gzStream => transfer(input, gzStream))

  /** Gunzips the file 'in' and writes it to 'out'.  'in' cannot be the same file as 'out'. */
  def gunzip(in: File, out: File): Unit = {
    require(in != out, "Input file cannot be the same as the output file.")
    Using.fileInputStream(in) { inputStream =>
      Using.fileOutputStream()(out) { outputStream =>
        gunzip(inputStream, outputStream)
      }
    }
  }

  /** Gunzips the InputStream 'input' and writes it to 'output'.  Neither stream is closed. */
  def gunzip(input: InputStream, output: OutputStream): Unit =
    gzipInputStream(input)(gzStream => transfer(gzStream, output))

  def unzip(
      from: File,
      toDirectory: File,
      filter: NameFilter = AllPassFilter,
      preserveLastModified: Boolean = true
  ): Set[File] =
    fileInputStream(from)(in => unzipStream(in, toDirectory, filter, preserveLastModified))

  def unzipURL(
      from: URL,
      toDirectory: File,
      filter: NameFilter = AllPassFilter,
      preserveLastModified: Boolean = true
  ): Set[File] =
    urlInputStream(from)(in => unzipStream(in, toDirectory, filter, preserveLastModified))

  def unzipStream(
      from: InputStream,
      toDirectory: File,
      filter: NameFilter = AllPassFilter,
      preserveLastModified: Boolean = true
  ): Set[File] = {
    createDirectory(toDirectory)
    zipInputStream(from)(zipInput =>
      extract(zipInput, toDirectory.toPath, filter, preserveLastModified)
    )
  }

  private def extract(
      from: ZipInputStream,
      toDirectory: NioPath,
      filter: NameFilter,
      preserveLastModified: Boolean
  ) = {
    val set = new HashSet[NioPath]
    val canonicalDirPath = toDirectory.normalize().toString
    def validateExtractPath(name: String, target: NioPath): Unit =
      if (!target.normalize().toString.startsWith(canonicalDirPath)) {
        throw new RuntimeException(s"Entry ($name) is outside of the target directory")
      }
    @tailrec def next(): Unit = {
      val entry = from.getNextEntry
      if (entry == null)
        ()
      else {
        val name = entry.getName
        if (filter.accept(name)) {
          val target = toDirectory.resolve(name)
          validateExtractPath(name, target)
          // log.debug("Extracting zip entry '" + name + "' to '" + target + "'")
          if (entry.isDirectory)
            createDirectory(target.toFile)
          else {
            set += target
            translate("Error extracting zip entry '" + name + "' to '" + target + "': ") {
              fileOutputStream(false)(target.toFile)(out => transfer(from, out))
            }
          }
          if (preserveLastModified)
            setModifiedTimeOrFalse(target.toFile, entry.getTime)
        } else {
          // log.debug("Ignoring zip entry '" + name + "'")
        }
        from.closeEntry()
        next()
      }
    }
    next()
    (Set() ++ set).map(_.toFile)
  }

  // TODO: provide a better API to download things.
  // /** Retrieves the content of the given URL and writes it to the given File. */
  // def download(url: URL, to: File) =
  //   Using.urlInputStream(url) { inputStream =>
  //     transfer(inputStream, to)
  //   }

  /** Copies the contents of `in` to `out`. */
  def transfer(in: File, out: File): Unit =
    fileInputStream(in)(in => transfer(in, out))

  /**
   * Copies the contents of the input file `in` to the `out` stream.
   * The output stream is not closed by this method.
   */
  def transfer(in: File, out: OutputStream): Unit =
    fileInputStream(in)(in => transfer(in, out))

  /** Copies all bytes from the given input stream to the given File.  The input stream is not closed by this method. */
  def transfer(in: InputStream, to: File): Unit =
    Using.fileOutputStream()(to) { outputStream =>
      transfer(in, outputStream)
    }

  /**
   * Copies all bytes from the given input stream to the given output stream.
   * Neither stream is closed.
   */
  def transfer(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, false)

  /**
   * Copies all bytes from the given input stream to the given output stream.  The
   * input stream is closed after the method completes.
   */
  def transferAndClose(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, true)
  private def transferImpl(in: InputStream, out: OutputStream, close: Boolean) = {
    try {
      val buffer = new Array[Byte](BufferSize)
      @tailrec def read(): Unit = {
        val byteCount = in.read(buffer)
        if (byteCount >= 0) {
          out.write(buffer, 0, byteCount)
          read()
        }
      }
      read()
    } finally {
      if (close) in.close
    }
  }

  /**
   * Creates a temporary directory and provides its location to the given function.  The directory
   * is deleted after the function returns if `keepDirectory` is set to false.
   */
  def withTemporaryDirectory[T](action: File => T, keepDirectory: Boolean): T = {
    val dir = createTemporaryDirectory
    try {
      action(dir)
    } finally {
      if (!keepDirectory) delete(dir)
    }
  }

  /**
   * Overload of `withTemporaryDirectory` with `keepDirectory` set to false.
   */
  def withTemporaryDirectory[T](action: File => T): T =
    withTemporaryDirectory(action, keepDirectory = false)

  /** Creates a directory in the default temporary directory with a name generated from a random integer. */
  def createTemporaryDirectory: File = createUniqueDirectory(temporaryDirectory)

  /** Creates a directory in `baseDirectory` with a name generated from a random integer */
  def createUniqueDirectory(baseDirectory: File): File = {
    def create(tries: Int): File = {
      if (tries > MaximumTries)
        sys.error("Could not create temporary directory.")
      else {
        val randomName = "sbt_" + java.lang.Integer.toHexString(random.nextInt)
        val f = new File(baseDirectory, randomName)

        try {
          createDirectory(f); f
        } catch { case NonFatal(_) => create(tries + 1) }
      }
    }
    create(0)
  }

  /**
   * Creates a file in the default temporary directory, calls `action` with the
   * file, deletes the file if `keepFile` is set to true, and returns the
   * result of calling `action`. The name of the file will begin with `prefix`,
   * which must be at least three characters long, and end with `postfix`, which
   * has no minimum length.
   */
  def withTemporaryFile[T](prefix: String, postfix: String, keepFile: Boolean)(
      action: File => T
  ): T = {
    val file = Files.createTempFile(prefix, postfix)
    try {
      action(file.toFile())
    } finally {
      if (!keepFile) file.toFile().delete(); ()
    }
  }

  /**
   * Overload of `withTemporaryFile` with `keepFile` set to false.
   */
  def withTemporaryFile[T](prefix: String, postfix: String)(action: File => T): T =
    withTemporaryFile(prefix, postfix, keepFile = false)(action)

  private[sbt] def jars(dir: File): Iterable[File] = listFiles(dir, GlobFilter("*.jar"))

  /** Deletes all empty directories in the set.  Any non-empty directories are ignored. */
  def deleteIfEmpty(dirs: collection.Set[File]): Unit = {
    val isEmpty = new HashMap[File, Boolean]
    def visit(f: File): Boolean =
      isEmpty.getOrElseUpdate(f, dirs(f) && f.isDirectory && (f.listFiles forall visit))

    dirs foreach visit
    for (case (f, true) <- isEmpty) f.delete
  }

  /** Deletes each file or directory (recursively) in `files`. */
  def delete(files: Iterable[File]): Unit = files.foreach(delete)

  /** Deletes each file or directory in `files` recursively.  Any empty parent directories are deleted, recursively. */
  def deleteFilesEmptyDirs(files: Iterable[File]): Unit = {
    def isEmptyDirectory(dir: File) = dir.isDirectory && listFiles(dir).isEmpty
    def parents(fs: Set[File]) = fs flatMap (f => Option(f.getParentFile))
    @tailrec def deleteEmpty(dirs: Set[File]): Unit = {
      val empty = dirs filter isEmptyDirectory
      if (
        empty.nonEmpty
      ) // looks funny, but this is true if at least one of `dirs` is an empty directory
        {
          empty foreach { _.delete() }
          deleteEmpty(parents(empty))
        }
    }

    delete(files)
    deleteEmpty(parents(files.toSet))
  }

  /**
   * Deletes `file`, recursively if it is a directory. Note that this method may silently fail to
   * delete the file (or directory) if any errors occur.
   */
  def delete(file: File): Unit = Retry {
    try {
      FileTreeView.default.list(file.toPath).foreach {
        case (dir, attrs) if attrs.isDirectory => delete(dir.toFile)
        case (f, _) =>
          try Files.deleteIfExists(f)
          catch { case _: IOException => }
      }
    } catch {
      case _: IOException => // Silently fail to preserve legacy behavior.
    }
    try Files.deleteIfExists(file.toPath)
    catch { case _: IOException => }
    ()
  }

  /** Returns the children of directory `dir` that match `filter` in a non-null array. */
  def listFiles(filter: java.io.FileFilter)(dir: File): Array[File] =
    wrapNull(dir.listFiles(filter))

  /** Returns the children of directory `dir` that match `filter` in a non-null array. */
  def listFiles(dir: File, filter: java.io.FileFilter): Array[File] =
    wrapNull(dir.listFiles(filter))

  /** Returns the children of directory `dir` in a non-null array. */
  def listFiles(dir: File): Array[File] = wrapNull(dir.listFiles())

  private[sbt] def wrapNull(a: Array[File]) = if (a == null) new Array[File](0) else a

  @deprecated("Please specify whether to use a static timestamp", "1.3.2")
  def jar(sources: Traversable[(File, String)], outputJar: File, manifest: Manifest): Unit =
    archive(sources.toSeq, outputJar, Some(manifest), None)

  /**
   * Creates a jar file.
   *
   * @param sources The files to include in the jar file paired with the entry name in the jar.
   *                Only the pairs explicitly listed are included.
   * @param outputJar The file to write the jar to.
   * @param manifest The manifest for the jar.
   * @param time static timestamp to use for all entries, if any, in milliseconds since Epoch
   */
  def jar(
      sources: Traversable[(File, String)],
      outputJar: File,
      manifest: Manifest,
      time: Option[Long]
  ): Unit =
    archive(sources.toSeq, outputJar, Some(manifest), time)

  @deprecated("Please specify whether to use a static timestamp", "1.3.2")
  def zip(sources: Traversable[(File, String)], outputZip: File): Unit =
    archive(sources.toSeq, outputZip, None, None)

  /**
   * Creates a zip file.
   *
   * @param sources The files to include in the zip file paired with the entry name in the zip.
   *                Only the pairs explicitly listed are included.
   * @param outputZip The file to write the zip to.
   * @param time static timestamp to use for all entries, if any.
   */
  def zip(sources: Traversable[(File, String)], outputZip: File, time: Option[Long]): Unit =
    archive(sources.toSeq, outputZip, None, time)

  private def archive(
      sources: Seq[(File, String)],
      outputFile: File,
      manifest: Option[Manifest],
      time: Option[Long]
  ) = {
    // The zip 'setTime' methods try to convert from the given time to the local time based
    // on java.util.TimeZone.getDefault(). When explicitly specifying the timestamp, we assume
    // this already has been done if desired, so we need to 'convert back' here:
    val localTime = time.map(t => t - java.util.TimeZone.getDefault().getOffset(t))
    if (outputFile.isDirectory)
      sys.error("Specified output file " + outputFile + " is a directory.")
    else {
      val outputDir = outputFile.getParentFile match {
        case null       => new File(".")
        case parentFile => parentFile
      }
      createDirectory(outputDir)
      withZipOutput(outputFile, manifest, localTime) { output =>
        val createEntry: (String => ZipEntry) =
          if (manifest.isDefined) new JarEntry(_) else new ZipEntry(_)
        writeZip(sources, output, localTime)(createEntry)
      }
    }
  }
  private def writeZip(sources: Seq[(File, String)], output: ZipOutputStream, time: Option[Long])(
      createEntry: String => ZipEntry
  ) = {
    val files = sources
      .flatMap { case (file, name) =>
        if (file.isFile) (file, normalizeToSlash(name)) :: Nil else Nil
      }
      .sortBy { case (_, name) =>
        name
      }

    val now = System.currentTimeMillis
    // The CRC32 for an empty value, needed to store directories in zip files
    val emptyCRC = new CRC32().getValue()

    def addDirectoryEntry(name: String) = {
      output putNextEntry makeDirectoryEntry(name)
      output.closeEntry()
    }

    def makeDirectoryEntry(name: String) = {
      //			log.debug("\tAdding directory " + relativePath + " ...")
      val e = createEntry(name)
      e setTime time.getOrElse(now)
      e setSize 0
      e setMethod ZipEntry.STORED
      e setCrc emptyCRC
      e
    }

    def makeFileEntry(file: File, name: String) = {
      //			log.debug("\tAdding " + file + " as " + name + " ...")
      val e = createEntry(name)
      e setTime time.getOrElse(getModifiedTimeOrZero(file))
      e
    }
    def addFileEntry(file: File, name: String) = {
      output putNextEntry makeFileEntry(file, name)
      transfer(file, output)
      output.closeEntry()
    }

    // Calculate directories and add them to the generated Zip
    allDirectoryPaths(files) foreach addDirectoryEntry

    // Add all files to the generated Zip
    files foreach { case (file, name) => addFileEntry(file, name) }
  }

  // map a path a/b/c to List("a", "b")
  private def relativeComponents(path: String): List[String] =
    path.split("/").toList.dropRight(1)

  // map components List("a", "b", "c") to List("a/b/c/", "a/b/", "a/", "")
  private def directories(path: List[String]): List[String] =
    path.foldLeft(List(""))((e, l) => (e.head + l + "/") :: e)

  // map a path a/b/c to List("a/b/", "a/")
  private def directoryPaths(path: String): List[String] =
    directories(relativeComponents(path)).filter(_.length > 1)

  // produce a sorted list of all the subdirectories of all provided files
  private def allDirectoryPaths(files: Iterable[(File, String)]) =
    TreeSet[String]() ++ (files flatMap { case (_, name) => directoryPaths(name) })

  private def normalizeToSlash(name: String) = {
    val sep = File.separatorChar
    if (sep == '/') name else name.replace(sep, '/')
  }

  private def withZipOutput(file: File, manifest: Option[Manifest], time: Option[Long])(
      f: ZipOutputStream => Unit
  ) = {
    fileOutputStream(false)(file) { fileOut =>
      val (zipOut, _) =
        manifest match {
          case Some(mf) =>
            import Attributes.Name.MANIFEST_VERSION
            val main = mf.getMainAttributes
            if (!main.containsKey(MANIFEST_VERSION))
              main.put(MANIFEST_VERSION, "1.0")

            val os = new JarOutputStream(fileOut)
            val e = new ZipEntry(JarFile.MANIFEST_NAME)
            e setTime time.getOrElse(System.currentTimeMillis)
            os.putNextEntry(e)
            mf.write(new BufferedOutputStream(os))
            os.closeEntry()

            (os, "jar")
          case None => (new ZipOutputStream(fileOut), "zip")
        }
      try {
        f(zipOut)
      } finally {
        zipOut.close
      }
    }
  }

  /**
   * Returns the relative file for `file` relative to directory `base` or None if `base` is not a parent of `file`.
   * If `file` or `base` are not absolute, they are first resolved against the current working directory.
   */
  def relativizeFile(base: File, file: File): Option[File] =
    relativize(base, file).map(path => new File(path))

  /**
   * Returns the path for `file` relative to directory `base` or None if `base` is not a parent of `file`.
   * If `file` or `base` are not absolute, they are first resolved against the current working directory.
   */
  def relativize(base: File, file: File): Option[String] = {
    // "On UNIX systems, a pathname is absolute if its prefix is "/"."
    // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html#isAbsolute
    // "This typically involves removing redundant names such as "." and ".." from the pathname, resolving symbolic links (on UNIX platforms)"
    // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html#getCanonicalPath()
    // We do not want to use getCanonicalPath because if we resolve the symbolic link, that could change
    // the outcome of copyDirectory's target structure.
    // Path#normailize is able to expand ".." without expanding the symlink.
    // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/Path.html#normalize()
    // "Returns a path that is this path with redundant name elements eliminated."
    def toAbsolutePath(x: File): NioPath = {
      val p = x.toPath
      if (!p.isAbsolute) p.toAbsolutePath
      else p
    }
    val basePath = toAbsolutePath(base).normalize
    val filePath = toAbsolutePath(file).normalize
    if (filePath startsWith basePath) {
      val relativePath =
        catching(classOf[IllegalArgumentException]) opt (basePath relativize filePath)
      relativePath map (_.toString)
    } else None
  }

  def copy(sources: Traversable[(File, File)]): Set[File] = copy(sources, CopyOptions())

  /**
   * For each pair in `sources`, copies the contents of the first File (the source) to the location
   * of the second File (the target).
   *
   * See [[sbt.io.CopyOptions]] for docs on the options available.
   *
   * Any parent directories that do not exist are created.
   * The set of all target files is returned, whether or not they were updated by this method.
   */
  def copy(sources: Traversable[(File, File)], options: CopyOptions): Set[File] =
    copy(sources, options.overwrite, options.preserveLastModified, options.preserveExecutable)

  def copy(
      sources: Traversable[(File, File)],
      overwrite: Boolean,
      preserveLastModified: Boolean,
      preserveExecutable: Boolean
  ): Set[File] =
    sources.map(tupled(copyImpl(overwrite, preserveLastModified, preserveExecutable))).toSet

  private def copyImpl(
      overwrite: Boolean,
      preserveLastModified: Boolean,
      preserveExecutable: Boolean
  )(from: File, to: File): File = {
    if (overwrite || !to.exists || getModifiedTimeOrZero(from) > getModifiedTimeOrZero(to)) {
      if (from.isDirectory)
        createDirectory(to)
      else {
        createDirectory(to.getParentFile)
        copyFile(from, to, preserveLastModified, preserveExecutable)
      }
    }
    to
  }

  def copyDirectory(source: File, target: File): Unit = copyDirectory(source, target, CopyOptions())

  /**
   * Copies the contents of each file in the `source` directory to the corresponding file in the
   * `target` directory.
   *
   * See [[sbt.io.CopyOptions]] for docs on the options available.
   *
   * Files in `target` without a corresponding file in `source` are left unmodified in any case.
   * Any parent directories that do not exist are created.
   */
  def copyDirectory(source: File, target: File, options: CopyOptions): Unit =
    copyDirectory(
      source,
      target,
      options.overwrite,
      options.preserveLastModified,
      options.preserveExecutable
    )

  def copyDirectory(
      source: File,
      target: File,
      overwrite: Boolean = false,
      preserveLastModified: Boolean = false,
      preserveExecutable: Boolean = true
  ): Unit = {
    val sources = PathFinder(source).allPaths pair Path.rebase(source, target)
    copy(sources, overwrite, preserveLastModified, preserveExecutable)
    ()
  }

  def copyFile(sourceFile: File, targetFile: File): Unit =
    copyFile(sourceFile, targetFile, CopyOptions())

  /**
   * Copies the contents of `sourceFile` to the location of `targetFile`, overwriting any existing content.
   *
   * See [[sbt.io.CopyOptions]] for docs on the options available.
   */
  def copyFile(sourceFile: File, targetFile: File, options: CopyOptions): Unit =
    copyFile(sourceFile, targetFile, options.preserveLastModified, options.preserveExecutable)

  def copyFile(
      sourceFile: File,
      targetFile: File,
      preserveLastModified: Boolean = false,
      preserveExecutable: Boolean = true
  ): Unit = {
    // NOTE: when modifying this code, test with larger values of CopySpec.MaxFileSizeBits than default

    require(sourceFile.exists, "Source file '" + sourceFile.getAbsolutePath + "' does not exist.")
    require(
      !sourceFile.isDirectory,
      "Source file '" + sourceFile.getAbsolutePath + "' is a directory."
    )
    fileInputChannel(sourceFile) { in =>
      fileOutputChannel(targetFile) { out =>
        // maximum bytes per transfer according to  from http://dzone.com/snippets/java-filecopy-using-nio
        val max = (64L * 1024 * 1024) - (32 * 1024)
        val total = in.size
        def loop(offset: Long): Long =
          if (offset < total)
            loop(offset + out.transferFrom(in, offset, max))
          else
            offset
        val copied = loop(0)
        if (copied != in.size)
          sys.error(
            "Could not copy '" + sourceFile + "' to '" + targetFile + "' (" + copied + "/" + in.size + " bytes copied)"
          )
      }
    }
    if (preserveLastModified) {
      copyLastModified(sourceFile, targetFile)
      ()
    }
    if (preserveExecutable) {
      copyExecutable(sourceFile, targetFile)
      ()
    }
  }

  /** Transfers the executable property of `sourceFile` to `targetFile`. */
  def copyExecutable(sourceFile: File, targetFile: File) = {
    val executable = sourceFile.canExecute
    if (executable) targetFile.setExecutable(true)
  }

  /** The default Charset used when not specified: UTF-8. */
  def defaultCharset = utf8

  /**
   * Writes `content` to `file` using `charset` or UTF-8 if `charset` is not explicitly specified.
   * If `append` is `false`, the existing contents of `file` are overwritten.
   * If `append` is `true`, the new `content` is appended to the existing contents.
   * If `file` or any parent directories do not exist, they are created.
   */
  def write(
      file: File,
      content: String,
      charset: Charset = defaultCharset,
      append: Boolean = false
  ): Unit =
    writer(file, content, charset, append) { _.write(content) }

  def writer[T](file: File, content: String, charset: Charset, append: Boolean = false)(
      f: BufferedWriter => T
  ): T =
    if (charset.newEncoder.canEncode(content))
      fileWriter(charset, append)(file)(f)
    else
      sys.error("String cannot be encoded by charset " + charset.name)

  def reader[T](file: File, charset: Charset = defaultCharset)(f: BufferedReader => T): T =
    fileReader(charset)(file) { f }

  /** Reads the full contents of `file` into a String using `charset` or UTF-8 if `charset` is not explicitly specified. */
  def read(file: File, charset: Charset = defaultCharset): String = {
    val out = new ByteArrayOutputStream(file.length.toInt)
    transfer(file, out)
    out.toString(charset.name)
  }

  /** Reads the full contents of `in` into a byte array.  This method does not close `in`. */
  def readStream(in: InputStream, charset: Charset = defaultCharset): String = {
    val out = new ByteArrayOutputStream
    transfer(in, out)
    out.toString(charset.name)
  }

  /** Reads the full contents of `in` into a byte array. */
  def readBytes(file: File): Array[Byte] = fileInputStream(file)(readBytes)

  /** Reads the full contents of `in` into a byte array.  This method does not close `in`. */
  def readBytes(in: InputStream): Array[Byte] = {
    val out = new ByteArrayOutputStream
    transfer(in, out)
    out.toByteArray
  }

  /**
   * Appends `content` to the existing contents of `file` using `charset` or UTF-8 if `charset` is not explicitly specified.
   * If `file` does not exist, it is created, as are any parent directories.
   */
  def append(file: File, content: String, charset: Charset = defaultCharset): Unit =
    write(file, content, charset, true)

  /**
   * Appends `bytes` to the existing contents of `file`.
   * If `file` does not exist, it is created, as are any parent directories.
   */
  def append(file: File, bytes: Array[Byte]): Unit =
    writeBytes(file, bytes, true)

  /**
   * Writes `bytes` to `file`, overwriting any existing content.
   * If any parent directories do not exist, they are first created.
   */
  def write(file: File, bytes: Array[Byte]): Unit =
    writeBytes(file, bytes, false)

  private def writeBytes(file: File, bytes: Array[Byte], append: Boolean): Unit =
    fileOutputStream(append)(file) { _.write(bytes) }

  /** Reads all of the lines from `url` using the provided `charset` or UTF-8 if `charset` is not explicitly specified. */
  def readLinesURL(url: URL, charset: Charset = defaultCharset): List[String] =
    urlReader(charset)(url)(readLines)

  /** Reads all of the lines in `file` using the provided `charset` or UTF-8 if `charset` is not explicitly specified. */
  def readLines(file: File, charset: Charset = defaultCharset): List[String] =
    fileReader(charset)(file)(readLines)

  /** Reads all of the lines from `in`.  This method does not close `in`. */
  def readLines(in: BufferedReader): List[String] =
    foldLines[List[String]](in, Nil)((accum, line) => line :: accum).reverse

  /** Applies `f` to each line read from `in`. This method does not close `in`. */
  def foreachLine(in: BufferedReader)(f: String => Unit): Unit =
    foldLines(in, ())((_, line) => f(line))

  /**
   * Applies `f` to each line read from `in` and the accumulated value of type `T`, with initial value `init`.
   * This method does not close `in`.
   */
  def foldLines[T](in: BufferedReader, init: T)(f: (T, String) => T): T = {
    def readLine(accum: T): T = {
      val line = in.readLine()
      if (line eq null) accum else readLine(f(accum, line))
    }
    readLine(init)
  }

  /**
   * Writes `lines` to `file` using the given `charset` or UTF-8 if `charset` is not explicitly specified.
   * If `append` is `false`, the contents of the file are overwritten.
   * If `append` is `true`, the lines are appended to the file.
   * A newline is written after each line and NOT before the first line.
   * If any parent directories of `file` do not exist, they are first created.
   */
  def writeLines(
      file: File,
      lines: Seq[String],
      charset: Charset = defaultCharset,
      append: Boolean = false
  ): Unit =
    writer(file, lines.headOption.getOrElse(""), charset, append) { w =>
      lines.foreach(line => { w.write(line); w.newLine() })
    }

  /** Writes `lines` to `writer` using `writer`'s `println` method. */
  def writeLines(writer: PrintWriter, lines: Seq[String]): Unit =
    lines foreach writer.println

  /**
   * Writes `properties` to the File `to`, using `label` as the comment on the first line.
   * If any parent directories of `to` do not exist, they are first created.
   */
  def write(properties: Properties, label: String, to: File) =
    fileOutputStream()(to)(output => properties.store(output, label))

  /** Reads the properties in `from` into `properties`.  If `from` does not exist, `properties` is left unchanged. */
  def load(properties: Properties, from: File): Unit =
    if (from.exists)
      fileInputStream(from)(input => properties.load(input))

  /** A pattern used to split a String by path separator characters. */
  private val PathSeparatorPattern = java.util.regex.Pattern.compile(File.pathSeparator)

  /** Splits a String around the platform's path separator characters. */
  def pathSplit(s: String) = PathSeparatorPattern.split(s)

  /**
   * Move the provided files to a temporary location.
   *   If 'f' returns normally, delete the files.
   *   If 'f' throws an Exception, return the files to their original location.
   */
  def stash[T](files: Set[File])(f: => T): T =
    withTemporaryDirectory { dir =>
      val stashed = stashLocations(dir, files.toArray)
      move(stashed)

      try {
        f
      } catch {
        case e: Exception =>
          try {
            move(stashed.map(_.swap)); throw e
          } catch { case _: Exception => throw e }
      }
    }

  private def stashLocations(dir: File, files: Array[File]) =
    for ((file, index) <- files.zipWithIndex) yield (file, new File(dir, index.toHexString))

  // TODO: the reference to the other move overload does not resolve, probably due to a scaladoc bug
  /**
   * For each pair in `files`, moves the contents of the first File to the location of the second.
   * See `sbt.io.IO$.move(java.io.File,java.io.File):Unit` for the behavior of the individual move operations.
   */
  def move(files: Traversable[(File, File)]): Unit =
    files.foreach(Function.tupled(move(_, _)))

  /**
   * Moves the contents of `a` to the location specified by `b`.
   * This method deletes any content already at `b` and creates any parent directories of `b` if they do not exist.
   * It will first try `File.renameTo` and if that fails, resort to copying and then deleting the original file.
   * In either case, the original File will not exist on successful completion of this method.
   */
  def move(a: File, b: File): Unit = {
    if (b.exists)
      delete(b)
    createDirectory(b.getParentFile)
    if (!a.renameTo(b)) {
      copyFile(a, b, true)
      delete(a)
    }
  }

  /**
   * Applies `f` to a buffered gzip `OutputStream` for `file`.
   * The streams involved are opened before calling `f` and closed after it returns.
   * The result is the result of `f`.
   */
  def gzipFileOut[T](file: File)(f: OutputStream => T): T =
    Using.fileOutputStream()(file) { fout =>
      Using.gzipOutputStream(fout) { outg =>
        Using.bufferedOutputStream(outg)(f)
      }
    }

  /**
   * Applies `f` to a buffered gzip `InputStream` for `file`.
   * The streams involved are opened before calling `f` and closed after it returns.
   * The result is the result of `f`.
   */
  def gzipFileIn[T](file: File)(f: InputStream => T): T =
    Using.fileInputStream(file) { fin =>
      Using.gzipInputStream(fin) { ing =>
        Using.bufferedInputStream(ing)(f)
      }
    }

  /**
   * Converts an absolute File to a URI.  The File is converted to a URI (toURI),
   * normalized (normalize), encoded (toASCIIString), and a forward slash ('/') is appended to the path component if
   * it does not already end with a slash.
   */
  def directoryURI(dir: File): URI = {
    assertAbsolute(dir)
    directoryURI(dir.toURI.normalize)
  }

  /**
   * Converts an absolute File to a URI.  The File is converted to a URI (toURI),
   * normalized (normalize), encoded (toASCIIString), and a forward slash ('/') is appended to the path component if
   * it does not already end with a slash.
   */
  def directoryURI(uri: URI): URI = {
    if (!uri.isAbsolute) return uri; // assertAbsolute(uri)
    val str = uri.toASCIIString
    val dirURI =
      if (str.endsWith("/") || uri.getScheme != FileScheme || (uri.getRawFragment ne null))
        uri
      else
        new URI(str + "/")

    dirURI.normalize
  }

  private[sbt] val isWindows: Boolean =
    System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")

  /** Converts the given File to a URI.  If the File is relative, the URI is relative, unlike File.toURI */
  def toURI(f: File): URI = {
    def ensureHeadSlash(name: String) =
      if (name.nonEmpty && name.head != File.separatorChar) s"${File.separatorChar}$name"
      else name

    val p = f.getPath
    if (isWindows && p.nonEmpty && p.head == File.separatorChar) {
      if (p.startsWith("""\\""")) {
        // supports \\laptop\My Documents\Some.doc on Windows
        new URI(FileScheme, normalizeToSlash(p), null)
      } else {
        // supports /tmp on Windows
        new URI(FileScheme, "", normalizeToSlash(p), null)
      }
    } else if (f.isAbsolute) {
      // not using f.toURI to avoid filesystem syscalls
      // we use empty string as host to force file:// instead of just file:
      new URI(FileScheme, "", normalizeToSlash(ensureHeadSlash(f.getAbsolutePath)), null)
    } else {
      // need to use the three argument URI constructor because the single argument version doesn't encode
      new URI(null, normalizeToSlash(f.getPath), null)
    }
  }

  /**
   * Resolves `f` against `base`, which must be an absolute directory.
   * The result is guaranteed to be absolute.
   * If `f` is absolute, it is returned without changes.
   */
  def resolve(base: File, f: File): File = {
    assertAbsolute(base)
    val fabs = if (f.isAbsolute) f else new File(directoryURI(new File(base, f.getPath)))
    assertAbsolute(fabs)
    fabs
  }
  def assertAbsolute(f: File) = assert(f.isAbsolute, "Not absolute: " + f)
  def assertAbsolute(uri: URI) = assert(uri.isAbsolute, "Not absolute: " + uri)

  /** Parses a classpath String into File entries according to the current platform's path separator. */
  def parseClasspath(s: String): Seq[File] =
    if (s.isEmpty) Nil else IO.pathSplit(s).map(new File(_)).toSeq

  /**
   * Constructs an `ObjectInputStream` on `wrapped` that uses `loader` to load classes.
   * See also [[https://github.com/sbt/sbt/issues/136 issue 136]].
   */
  def objectInputStream(wrapped: InputStream, loader: ClassLoader): ObjectInputStream =
    new ObjectInputStream(wrapped) {
      override def resolveClass(osc: ObjectStreamClass): Class[?] = {
        val c = Class.forName(osc.getName, false, loader)
        if (c eq null) super.resolveClass(osc) else c
      }
    }

  /** Returns `true` if the filesystem supports POSIX file attribute view. */
  def isPosix: Boolean = hasPosixFileAttributeView

  /** Returns `true` if the filesystem supports POSIX file attribute view. */
  lazy val hasPosixFileAttributeView: Boolean = supportedFileAttributeViews.contains("posix")

  /** Returns `true` if the filesystem supports file owner attribute view. */
  lazy val hasFileOwnerAttributeView: Boolean = supportedFileAttributeViews.contains("owner")

  /** Returns `true` if the filesystem supports DOS file attribute view. */
  lazy val hasDosFileAttributeView: Boolean = supportedFileAttributeViews.contains("dos")

  /** Returns `true` if the filesystem supports ACL file attribute view. */
  lazy val hasAclFileAttributeView: Boolean = supportedFileAttributeViews.contains("acl")

  /** Returns `true` if the filesystem supports basic file attribute view. */
  lazy val hasBasicFileAttributeView: Boolean = supportedFileAttributeViews.contains("basic")

  /** Returns `true` if the filesystem supports user-defined file attribute view. */
  lazy val hasUserDefinedFileAttributeView: Boolean = supportedFileAttributeViews.contains("user")

  private[this] lazy val supportedFileAttributeViews: Set[String] = {
    FileSystems.getDefault.supportedFileAttributeViews.asScala.toSet
  }

  /**
   * Updates permission of this file.
   * This operation requires underlying filesystem to support `IO.isPosix`.
   *
   * @param file
   * @param permissions Must be 9 character POSIX permission representation e.g. "rwxr-x---"
   */
  def setPermissions(file: File, permissions: String): Unit = {
    Path(file).setPermissions(PosixFilePermissions.fromString(permissions).asScala.toSet)
  }

  /**
   * An alias for `setPermissions`. Updates permission of this file.
   * This operation requires underlying filesystem to support `IO.isPosix`.
   *
   * @param permissions Must be 9 character POSIX permission representation e.g. "rwxr-x---"
   * @param file
   */
  def chmod(permissions: String, file: File): Unit = setPermissions(file, permissions)

  /**
   * Updates the file owner.
   * This operation requires underlying filesystem to support `IO.hasFileOwnerAttributeView`.
   *
   * @param file
   * @param owner
   */
  def setOwner(file: File, owner: String): Unit = Path(file).setOwner(owner)

  /**
   * An alias for `setOwner`. Updates the file owner.
   * This operation requires underlying filesystem to support `IO.hasFileOwnerAttributeView`.
   *
   * @param owner
   * @param file
   */
  def chown(owner: String, file: File): Unit = setOwner(file, owner)

  /**
   * Updates the group owner of the file.
   * This operation requires underlying filesystem to support `IO.hasFileOwnerAttributeView`.
   *
   * @param file
   * @param group
   */
  def setGroup(file: File, group: String): Unit = Path(file).setGroup(group)

  /**
   * An alias for setGroup. Updates the group owner of the file.
   * This operation requires underlying filesystem to support `IO.hasFileOwnerAttributeView`.
   *
   * @param group
   * @param file
   */
  def chgrp(group: String, file: File): Unit = setGroup(file, group)

  /**
   * Return the last modification timestamp of the specified file,
   * in milliseconds since the Unix epoch (January 1, 1970 UTC).
   * This method will use whenever possible native code in order to
   * return a timestamp with a 1-millisecond precision.
   *
   * This is in contrast to lastModified() in java.io.File, and to
   * getLastModifiedTime() in java.nio.file.Files, which on many implementations
   * of the JDK prior to JDK 10 will return timestamps with 1-second precision.
   *
   * If native code support is not available for the JDK/OS in use, this
   * method will revert to the Java calls. Currently supported systems
   * are Linux 64/32 bits, Windows, and OSX, all on Intel hardware.
   *
   * Please note that even on those platforms, not all filesystems
   * support sub-second timestamp resolutions. For instance, ext2/3,
   * FAT, and HFS+ all have a one second resolution or higher for
   * modification times. Conversely, ext4, NTFS, and APFS all support
   * at least millisecond resolution, or finer.
   *
   * If the file does not exist, or if it impossible to obtain the
   * modification time because of access permissions of other reasons,
   * this method will throw a FileNotFoundException or an IOException,
   * as appropriate. This is the same behavior as the nio code in
   * Files.getLastModifiedTime(). However note that, in contrast,
   * Java's traditional lastModified() in java.io.File will return
   * zero if an error occurs.
   *
   * If you do not wish to use native calls, please define the property
   * "sbt.io.jdktimestamps" to "true" (or anything other than "false"),
   * and Java's get/setLastModifiedTime() will be used instead. This
   * setting applies to setModifiedTime() and copyModifiedTime() as well.
   *
   * This method was added in sbt/io v1.1.2, but it may be
   * replaced in the future by a similar method that returns
   * a Try, rather than throwing an exception.
   * It is therefore marked as deprecated, since we cannot
   * guarantee that it will remain available in future versions.
   *
   * @see setModifiedTime
   * @see copyModifiedTime
   */
  @deprecated("This method might be removed in the future, also see getModifiedOrZero()", "1.1.3")
  def getModifiedTime(file: File): Long = Milli.getModifiedTime(file)

  /**
   * Sets the modification time of the file argument, in milliseconds
   * since the Unix epoch (January 1, 1970 UTC).
   * This method will use native code whenever possible in order to
   * achieve a subsecond precision. Please see getModifiedTime() for
   * further information.
   * If it is impossible to set the modification time, the code will
   * throw a FileNotFoundException or an IOException, as appropriate.
   * This is similar to Files.setLastModifiedTime(). Note that, in
   * contrast, Java's traditional setLastModified() will return a
   * boolean false value if an error occurs.
   *
   * This method may not work correctly if mtime is negative.
   *
   * This method was added in sbt/io v1.1.2, but it may be
   * replaced in the future by a similar method that returns
   * a Try, rather than throwing an exception.
   * It is therefore marked as deprecated, since we cannot
   * guarantee that it will remain available in future versions.
   *
   * @see getModifiedTime
   * @see copyModifiedTime
   */
  @deprecated(
    "This method might be removed in the future, also see setModifiedTimeOrFalse()",
    "1.1.3"
  )
  def setModifiedTime(file: File, mtime: Long): Unit = Milli.setModifiedTime(file, mtime)

  /**
   * Copies the last modification time of `fromFile` to `toFile`, with
   * a highest precision possible, by using native code when available.
   *
   * This method copies the timestamps with the highest possible precision
   * offered by the native calls of this system for the filesystem in use.
   * That could be in the region of nanoseconds. It is therefore more
   * precise than using separate getModifiedTime()/setModifiedTime() calls,
   * which will round timestamps to whole milliseconds.
   *
   * If the timestamp cannot be copied, this method will throw
   * a FileNotFoundException or an IOException, as appropriate.
   *
   * This method was added in sbt/io v1.1.2, but it may be
   * replaced in the future by a similar method that returns
   * a Try, rather than throwing an exception.
   * It is therefore marked as deprecated, since we cannot
   * guarantee that it will remain available in future versions.
   *
   * @see getModifiedTime
   * @see setModifiedTime
   */
  @deprecated("This method might be removed in the future, also see copyLastModified()", "1.1.3")
  def copyModifiedTime(fromFile: File, toFile: File): Unit =
    Milli.copyModifiedTime(fromFile, toFile)

  /**
   * Return the last modification timestamp of the specified file,
   * in milliseconds since the Unix epoch (January 1, 1970 UTC).
   *
   * If the specified file does not exist, this method will return 0L.
   *
   * The deprecated method getModifiedTime() has similar semantics,
   * but will throw an exception if the file does not exist.
   * Please refer to its documentation for additional details.
   *
   * @see getModifiedTime
   */
  def getModifiedTimeOrZero(file: File): Long =
    try {
      Retry(Milli.getModifiedTime(file), excludeFileNotFound: _*)
    } catch {
      case _: FileNotFoundException =>
        val unnormalized = file.toPath
        val normalized = unnormalized.normalize.toAbsolutePath
        if (unnormalized != normalized) getModifiedTimeOrZero(normalized.toFile) else 0L
    }

  /**
   * Sets the modification time of the file argument, in milliseconds
   * since the Unix epoch (January 1, 1970 UTC).
   *
   * If the specified file does not exist, this method will return false.
   * It will return true if the file modification time was successfully changed.
   *
   * The deprecated method setModifiedTime() has similar semantics,
   * but will throw an exception if the file does not exist.
   * Please refer to its documentation for additional details.
   *
   * This method may not work correctly if mtime is negative.
   *
   * @see setModifiedTime
   */
  def setModifiedTimeOrFalse(file: File, mtime: Long): Boolean =
    try {
      Retry(Milli.setModifiedTime(file, mtime), excludeFileNotFound: _*)
      true
    } catch {
      case _: FileNotFoundException =>
        val unnormalized = file.toPath
        val normalized = unnormalized.normalize.toAbsolutePath
        if (unnormalized != normalized) setModifiedTimeOrFalse(normalized.toFile, mtime) else false
    }

  /**
   * Transfers the last modified time of `sourceFile` to `targetFile`.
   *
   * Note: this method has a special semantics if files are missing.
   * In particular, if the source file is missing, it will silently
   * set the target modification time to 1st January 1970, which
   * corresponds to the Unix epoch.
   *
   * The method returns true if the target file modification time was
   * successfully changed, false otherwise.
   *
   * The deprecated related method copyModifiedTime() has a somewhat different
   * semantics, please refer to its documentation for additional details.
   *
   * @see copyModifiedTime
   */
  def copyLastModified(sourceFile: File, targetFile: File): Boolean = {
    val last = getModifiedTimeOrZero(sourceFile)
    // getModifiedTimeOrZero can return a negative number, but setLastModified
    // (which may be used by setModifiedTimeOrFalse) doesn't accept it,
    // see Java bug #6791812
    setModifiedTimeOrFalse(targetFile, math.max(last, 0L))
  }

  private val excludeFileNotFound: immutable.Seq[Class[? <: IOException]] = List(
    classOf[FileNotFoundException]
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy