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

better.files.File.scala Maven / Gradle / Ivy

There is a newer version: 3.9.2
Show newest version
package better.files

import java.io.{File => JFile, FileSystem => JFileSystem, _} //TODO: Scala 2.10 does not like java.io._
import java.net.URI
import java.nio.channels.{OverlappingFileLockException, AsynchronousFileChannel, FileChannel, NonWritableChannelException, NonReadableChannelException}
import java.nio.file._, attribute._
import java.security.MessageDigest
import java.time.Instant
import java.util.zip.{Deflater, ZipFile}
import javax.xml.bind.DatatypeConverter

import scala.collection.JavaConversions._
import scala.io.{BufferedSource, Codec, Source}
import scala.util.Properties

/**
  * Scala wrapper around java.nio.files.Path
  */
class File private(val path: Path) {
  //TODO: LinkOption?

  def pathAsString: String =
    path.toString

  def toJava: JFile =
    path.toFile

  /**
    * Name of file
    * Certain files may not have a name e.g. root directory - returns empty string in that case
    *
    * @return
    */
  def name: String =
    nameOption getOrElse ""

  /**
    * Certain files may not have a name e.g. root directory - returns None in that case
    *
    * @return
    */
  def nameOption: Option[String] =
    Option(path.getFileName).map(_.toString)

  def root: File =
    path.getRoot

  def nameWithoutExtension: String =
    if (hasExtension) name.substring(0, name lastIndexOf ".") else name

  /**
    * @return extension (including the dot) of this file if it is a regular file and has an extension, else None
    */
  def extension: Option[String] =
    extension()

  /**
    * @param includeDot  whether the dot should be included in the extension or not
    * @param includeAll  whether all extension tokens should be included, or just the last one e.g. for bundle.tar.gz should it be .tar.gz or .gz
    * @param toLowerCase to lowercase the extension or not e.g. foo.HTML should have .html or .HTML
    * @return extension of this file if it is a regular file and has an extension, else None
    */
  def extension(includeDot: Boolean = true, includeAll: Boolean = false, toLowerCase: Boolean = true): Option[String] = {
    when(hasExtension) {
      val dot = if (includeAll) name indexOf "." else name lastIndexOf "."
      val index = if (includeDot) dot else dot + 1
      val extension = name.substring(index)
      if (toLowerCase) extension.toLowerCase else extension
    }
  }

  def hasExtension: Boolean =
    (isRegularFile || notExists) && (name contains ".")

  /**
    * Changes the file-extension by renaming this file; if file does not have an extension, it adds the extension
    * Example usage file"foo.java".changeExtensionTo(".scala")
    */
  def changeExtensionTo(extension: String): File =
    if (isRegularFile) renameTo(s"$nameWithoutExtension$extension") else this

  def contentType: Option[String] =
    Option(Files.probeContentType(path))

  /**
    * Return parent of this file
    * NOTE: This API returns null if this file is the root;
    * please use parentOption if you expect to handle roots
    *
    * @see parentOption
    * @return
    */
  def parent: File =
    parentOption.orNull

  /**
    *
    * @return Some(parent) of this file or None if this is the root and thus has no parent
    */
  def parentOption: Option[File] =
    Option(path.getParent).map(File.apply)

  def /(child: String): File = path resolve child

  def createChild(child: String, asDirectory: Boolean = false)(implicit attributes: File.Attributes = File.Attributes.default, linkOptions: File.LinkOptions = File.LinkOptions.default): File =
    (this / child).createIfNotExists(asDirectory)(attributes, linkOptions)

  /**
    * Create this file. If it exists, don't do anything
    *
    * @param asDirectory   If you want this file to be created as a directory instead, set this to true (false by default)
    * @param createParents If you also want all the parents to be created from root to this file (false by defailt)
    * @param attributes
    * @param linkOptions
    * @return
    */
  def createIfNotExists(asDirectory: Boolean = false, createParents: Boolean = false)(implicit attributes: File.Attributes = File.Attributes.default, linkOptions: File.LinkOptions = File.LinkOptions.default): this.type = {
    if (exists(linkOptions)) {
      this
    } else if (asDirectory) {
      createDirectories()(attributes)
    } else {
      if (createParents) parent.createDirectories()(attributes)
      Files.createFile(path, attributes: _*)
      this
    }
  }

  def exists(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    Files.exists(path, linkOptions: _*)

  def notExists(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    Files.notExists(path, linkOptions: _*)

  def sibling(name: String): File =
    path resolveSibling name

  def isSiblingOf(sibling: File): Boolean =
    sibling isChildOf parent

  def siblings: Files =
    parent.list.filterNot(_ == this)

  def isChildOf(parent: File): Boolean =
    parent isParentOf this

  /**
    * Check if this directory contains this file
    *
    * @param file
    * @return true if this is a directory and it contains this file
    */
  def contains(file: File): Boolean =
    isDirectory && (file.path startsWith path)

  def isParentOf(child: File): Boolean =
    contains(child)

  def bytes: Iterator[Byte] =
    newInputStream.buffered.bytes //TODO: ManagedResource here?

  def loadBytes: Array[Byte] =
    Files.readAllBytes(path)

  def byteArray: Array[Byte] =
    loadBytes

  def createDirectory()(implicit attributes: File.Attributes = File.Attributes.default): this.type = {
    Files.createDirectory(path, attributes: _*)
    this
  }

  def createDirectories()(implicit attributes: File.Attributes = File.Attributes.default): this.type = {
    Files.createDirectories(path, attributes: _*)
    this
  }

  def chars(implicit codec: Codec): Iterator[Char] =
    newBufferedReader(codec).chars //TODO: ManagedResource here?

  /**
    * Load all lines from this file
    * Note: Large files may cause an OutOfMemory in which case, use the streaming version @see lineIterator
    *
    * @param codec
    * @return all lines in this file
    */
  def lines(implicit codec: Codec): Traversable[String] =
    Files.readAllLines(path, codec)

  /**
    * Iterate over lines in a file (auto-close stream on complete)
    * NOTE: If the iteration is partial, it may leave a stream open
    * If you want partial iteration use @see lines()
    *
    * @param codec
    * @return
    */
  def lineIterator(implicit codec: Codec): Iterator[String] =
    Files.lines(path, codec).toAutoClosedIterator

  def tokens(implicit config: Scanner.Config = Scanner.Config.default, codec: Codec): Traversable[String] =
    bufferedReader(codec).flatMap(_.tokens(config))

  def contentAsString(implicit codec: Codec): String =
    new String(byteArray, codec)

  def `!`(implicit codec: Codec): String =
    contentAsString(codec)

  def printLines(lines: Iterator[Any])(implicit openOptions: File.OpenOptions = File.OpenOptions.append): this.type = {
    for {
      pw <- printWriter()(openOptions)
      line <- lines
    } pw println line
    this
  }

  /**
    * For large number of lines that may not fit in memory, use printLines
    *
    * @param lines
    * @param openOptions
    * @param codec
    * @return
    */
  def appendLines(lines: String*)(implicit openOptions: File.OpenOptions = File.OpenOptions.append, codec: Codec): this.type = {
    Files.write(path, lines, codec, openOptions: _*)
    this
  }

  def <<(line: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.append, codec: Codec): this.type =
    appendLines(line)(openOptions, codec)

  def >>:(line: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.append, codec: Codec): this.type =
    appendLines(line)(openOptions, codec)

  def appendLine(line: String = "")(implicit openOptions: File.OpenOptions = File.OpenOptions.append, codec: Codec): this.type =
    appendLines(line)(openOptions, codec)

  def append(text: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.append, codec: Codec): this.type =
    append(text.getBytes(codec))(openOptions)

  def append(bytes: Array[Byte])(implicit openOptions: File.OpenOptions): this.type = {
    Files.write(path, bytes, openOptions: _*)
    this
  }

  def appendBytes(bytes: Iterator[Byte])(implicit openOptions: File.OpenOptions = File.OpenOptions.append): this.type =
    writeBytes(bytes)(openOptions)

  /**
    * Write byte array to file. For large contents consider using the writeBytes
    *
    * @param bytes
    * @return this
    */
  def write(bytes: Array[Byte])(implicit openOptions: File.OpenOptions): this.type = {
    Files.write(path, bytes, openOptions: _*)
    this
  }

  def writeBytes(bytes: Iterator[Byte])(implicit openOptions: File.OpenOptions = File.OpenOptions.default): this.type = {
    outputStream(openOptions).foreach(_.buffered write bytes)
    this
  }

  def write(text: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.default, codec: Codec): this.type =
    write(text.getBytes(codec))(openOptions)

  def overwrite(text: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.default, codec: Codec): this.type =
    write(text)(openOptions, codec)

  def <(text: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.default, codec: Codec): this.type =
    write(text)(openOptions, codec)

  def `>:`(text: String)(implicit openOptions: File.OpenOptions = File.OpenOptions.default, codec: Codec): this.type =
    write(text)(openOptions, codec)


  def newBufferedSource(implicit codec: Codec): BufferedSource =
    Source.fromFile(toJava)(codec)

  def bufferedSource(implicit codec: Codec): ManagedResource[BufferedSource] =
    newBufferedSource(codec).autoClosed

  def newRandomAccess(mode: File.RandomAccessMode = File.RandomAccessMode.read): RandomAccessFile =
    new RandomAccessFile(toJava, mode.value)

  def randomAccess(mode: File.RandomAccessMode = File.RandomAccessMode.read): ManagedResource[RandomAccessFile] =
    newRandomAccess(mode).autoClosed //TODO: Mode enum?

  def newBufferedReader(implicit codec: Codec): BufferedReader =
    Files.newBufferedReader(path, codec)

  def bufferedReader(implicit codec: Codec): ManagedResource[BufferedReader] =
    newBufferedReader(codec).autoClosed

  def newBufferedWriter(implicit codec: Codec, openOptions: File.OpenOptions = File.OpenOptions.default): BufferedWriter =
    Files.newBufferedWriter(path, codec, openOptions: _*)

  def bufferedWriter(implicit codec: Codec, openOptions: File.OpenOptions = File.OpenOptions.default): ManagedResource[BufferedWriter] =
    newBufferedWriter(codec, openOptions).autoClosed

  def newFileReader: FileReader =
    new FileReader(toJava)

  def fileReader: ManagedResource[FileReader] =
    newFileReader.autoClosed

  def newFileWriter(append: Boolean = false): FileWriter =
    new FileWriter(toJava, append)

  def fileWriter(append: Boolean = false): ManagedResource[FileWriter] =
    newFileWriter(append).autoClosed

  def newPrintWriter(autoFlush: Boolean = false)(implicit openOptions: File.OpenOptions = File.OpenOptions.default): PrintWriter =
    new PrintWriter(newOutputStream(openOptions), autoFlush)

  def printWriter(autoFlush: Boolean = false)(implicit openOptions: File.OpenOptions = File.OpenOptions.default): ManagedResource[PrintWriter] =
    newPrintWriter(autoFlush)(openOptions).autoClosed

  def newInputStream(implicit openOptions: File.OpenOptions = File.OpenOptions.default): InputStream =
    Files.newInputStream(path, openOptions: _*)

  def inputStream(implicit openOptions: File.OpenOptions = File.OpenOptions.default): ManagedResource[InputStream] =
    newInputStream(openOptions).autoClosed

  def newScanner(implicit config: Scanner.Config = Scanner.Config.default): Scanner =
    Scanner(newBufferedReader(config.codec))(config)

  def scanner(implicit config: Scanner.Config = Scanner.Config.default): ManagedResource[Scanner] =
    newScanner(config).autoClosed

  def newOutputStream(implicit openOptions: File.OpenOptions = File.OpenOptions.default): OutputStream =
    Files.newOutputStream(path, openOptions: _*)

  def outputStream(implicit openOptions: File.OpenOptions = File.OpenOptions.default): ManagedResource[OutputStream] =
    newOutputStream(openOptions).autoClosed

  def newFileChannel(implicit openOptions: File.OpenOptions = File.OpenOptions.default, attributes: File.Attributes = File.Attributes.default): FileChannel =
    FileChannel.open(path, openOptions.toSet, attributes: _*)

  def fileChannel(implicit openOptions: File.OpenOptions = File.OpenOptions.default, attributes: File.Attributes = File.Attributes.default): ManagedResource[FileChannel] =
    newFileChannel(openOptions, attributes).autoClosed

  def newAsynchronousFileChannel(implicit openOptions: File.OpenOptions = File.OpenOptions.default): AsynchronousFileChannel =
    AsynchronousFileChannel.open(path, openOptions: _*)

  def asynchronousFileChannel(implicit openOptions: File.OpenOptions = File.OpenOptions.default): ManagedResource[AsynchronousFileChannel] =
    newAsynchronousFileChannel(openOptions).autoClosed

  def newWatchService: WatchService =
    fileSystem.newWatchService()

  def watchService: ManagedResource[WatchService] =
    newWatchService.autoClosed

  def register(service: WatchService, events: File.Events = File.Events.all): this.type = {
    path.register(service, events.toArray)
    this
  }

  def digest(algorithmName: String): Array[Byte] = {
    val algorithm = MessageDigest.getInstance(algorithmName)
    listRelativePaths.toSeq.sorted foreach { relativePath =>
      val file: File = path resolve relativePath
      val bytes = if (file.isDirectory) relativePath.toString.getBytes else file.loadBytes
      algorithm.update(bytes)
    }
    algorithm.digest()
  }

  /**
    * @return checksum of this file (or directory) in hex format
    */
  def checksum(algorithm: String): String =
    DatatypeConverter.printHexBinary(digest(algorithm))

  def md5: String =
    checksum("MD5")

  def sha1: String =
    checksum("SHA-1")

  def sha256: String =
    checksum("SHA-256")

  def sha512: String =
    checksum("SHA-512")

  /**
    * @return Some(target) if this is a symbolic link (to target) else None
    */
  def symbolicLink: Option[File] =
    when(isSymbolicLink)(Files.readSymbolicLink(path))

  /**
    * @return true if this file (or the file found by following symlink) is a directory
    */
  def isDirectory(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    Files.isDirectory(path, linkOptions: _*)

  /**
    * @return true if this file (or the file found by following symlink) is a regular file
    */
  def isRegularFile(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    Files.isRegularFile(path, linkOptions: _*)

  def isSymbolicLink: Boolean =
    Files.isSymbolicLink(path)

  def isHidden: Boolean =
    Files.isHidden(path)

  def isLocked(mode: File.RandomAccessMode, position: Long = 0L, size: Long = Long.MaxValue, isShared: Boolean = false): Boolean = {
    val channel = newRandomAccess(mode).getChannel
    try {
      channel.tryLock(position, size, isShared).release()
      false
    } catch {
      case _: OverlappingFileLockException | _: NonWritableChannelException | _: NonReadableChannelException => true
    } finally {
      channel.close()
    }
  }

  def isReadLocked(position: Long = 0L, size: Long = Long.MaxValue, isShared: Boolean = false) =
    isLocked(File.RandomAccessMode.read, position, size, isShared)

  def isWriteLocked(position: Long = 0L, size: Long = Long.MaxValue, isShared: Boolean = false) =
    isLocked(File.RandomAccessMode.readWrite, position, size, isShared)

  def list: Files =
    Files.list(path)

  def children: Files = list

  def entries: Files = list

  def listRecursively(implicit visitOptions: File.VisitOptions = File.VisitOptions.default): Files =
    walk()(visitOptions).filterNot(isSamePathAs)

  /**
    * Walk the directory tree recursively upto maxDepth
    *
    * @param maxDepth
    * @return List of children in BFS maxDepth level deep (includes self since self is at depth = 0)
    */
  def walk(maxDepth: Int = Int.MaxValue)(implicit visitOptions: File.VisitOptions = File.VisitOptions.default): Files =
    Files.walk(path, maxDepth, visitOptions: _*) //TODO: that ignores I/O errors?

  def pathMatcher(syntax: File.PathMatcherSyntax)(pattern: String): PathMatcher =
    fileSystem.getPathMatcher(s"${syntax.name}:$pattern")

  /**
    * Util to glob from this file's path
    *
    * @return Set of files that matched
    */
  def glob(pattern: String)(implicit syntax: File.PathMatcherSyntax = File.PathMatcherSyntax.default, visitOptions: File.VisitOptions = File.VisitOptions.default): Files = {
    val matcher = pathMatcher(syntax)(pattern)
    collectChildren(child => matcher.matches(child.path))(visitOptions)
  }

  /**
    * More Scala friendly way of doing Files.walk
    *
    * @param matchFilter
    * @return
    */
  def collectChildren(matchFilter: File => Boolean)(implicit visitOptions: File.VisitOptions = File.VisitOptions.default): Files =
    walk()(visitOptions).filter(matchFilter(_))

  def fileSystem: FileSystem =
    path.getFileSystem

  def uri: URI =
    path.toUri

  /**
    * @return file size (for directories, return size of the directory) in bytes
    */
  def size(implicit visitOptions: File.VisitOptions = File.VisitOptions.default): Long =
    walk()(visitOptions).map(f => Files.size(f.path)).sum

  def permissions(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Set[PosixFilePermission] =
    Files.getPosixFilePermissions(path, linkOptions: _*).toSet

  def permissionsAsString(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): String =
    PosixFilePermissions.toString(permissions(linkOptions))

  def setPermissions(permissions: Set[PosixFilePermission]): this.type = {
    Files.setPosixFilePermissions(path, permissions)
    this
  }

  def addPermission(permission: PosixFilePermission)(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): this.type =
    setPermissions(permissions(linkOptions) + permission)

  def removePermission(permission: PosixFilePermission)(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): this.type =
    setPermissions(permissions(linkOptions) - permission)

  /**
    * test if file has this permission
    */
  def apply(permission: PosixFilePermission)(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    permissions(linkOptions)(permission)

  def isOwnerReadable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.OWNER_READ)(linkOptions)

  def isOwnerWritable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.OWNER_WRITE)(linkOptions)

  def isOwnerExecutable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.OWNER_EXECUTE)(linkOptions)

  def isGroupReadable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.GROUP_READ)(linkOptions)

  def isGroupWritable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.GROUP_WRITE)(linkOptions)

  def isGroupExecutable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.GROUP_EXECUTE)(linkOptions)

  def isOtherReadable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.OTHERS_READ)(linkOptions)

  def isOtherWritable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.OTHERS_WRITE)(linkOptions)

  def isOtherExecutable(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Boolean =
    this(PosixFilePermission.OTHERS_EXECUTE)(linkOptions)

  /**
    * This differs from the above as this checks if the JVM can read this file even though the OS cannot in certain platforms
    *
    * @see isOwnerReadable
    * @return
    */
  def isReadable: Boolean =
    toJava.canRead

  def isWriteable: Boolean =
    toJava.canWrite

  def isExecutable: Boolean =
    toJava.canExecute

  def attributes(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): BasicFileAttributes =
    Files.readAttributes(path, classOf[BasicFileAttributes], linkOptions: _*)

  def posixAttributes(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): PosixFileAttributes =
    Files.readAttributes(path, classOf[PosixFileAttributes], linkOptions: _*)

  def dosAttributes(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): DosFileAttributes =
    Files.readAttributes(path, classOf[DosFileAttributes], linkOptions: _*)

  def owner(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): UserPrincipal =
    Files.getOwner(path, linkOptions: _*)

  def ownerName(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): String =
    owner(linkOptions).getName

  def group(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): GroupPrincipal =
    posixAttributes(linkOptions).group()

  def groupName(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): String =
    group(linkOptions).getName

  def setOwner(owner: String): this.type = {
    Files.setOwner(path, fileSystem.getUserPrincipalLookupService.lookupPrincipalByName(owner))
    this
  }

  def setGroup(group: String): this.type = {
    Files.setOwner(path, fileSystem.getUserPrincipalLookupService.lookupPrincipalByGroupName(group))
    this
  }

  /**
    * Similar to the UNIX command touch - create this file if it does not exist and set its last modification time
    */
  def touch(time: Instant = Instant.now())(implicit attributes: File.Attributes = File.Attributes.default, linkOptions: File.LinkOptions = File.LinkOptions.default): this.type = {
    Files.setLastModifiedTime(createIfNotExists()(attributes, linkOptions).path, FileTime.from(time))
    this
  }

  def lastModifiedTime(implicit linkOptions: File.LinkOptions = File.LinkOptions.default): Instant =
    Files.getLastModifiedTime(path, linkOptions: _*).toInstant

  /**
    * Deletes this file or directory
    *
    * @param swallowIOExceptions If this is set to true, any exception thrown is swallowed
    */
  def delete(swallowIOExceptions: Boolean = false): this.type = {
    try {
      if (isDirectory) list.foreach(_.delete(swallowIOExceptions))
      Files.delete(path)
    } catch {
      case e: IOException if swallowIOExceptions => //e.printStackTrace() //swallow
    }
    this
  }

  def renameTo(newName: String): File =
    moveTo(path resolveSibling newName)

  /**
    *
    * @param destination
    * @param overwrite
    * @return destination
    */
  def moveTo(destination: File, overwrite: Boolean = false): destination.type = {
    Files.move(path, destination.path, File.CopyOptions(overwrite): _*)
    destination
  }

  /**
    *
    * @param destination
    * @param overwrite
    * @return destination
    */
  def copyTo(destination: File, overwrite: Boolean = false): destination.type = {
    if (isDirectory) {//TODO: maxDepth?
      if (overwrite) destination.delete(swallowIOExceptions = true)
      Files.walkFileTree(path, new SimpleFileVisitor[Path] {
        def newPath(subPath: Path): Path = destination.path resolve (path relativize subPath)

        override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes) = {
          Files.createDirectories(newPath(dir))
          super.preVisitDirectory(dir, attrs)
        }

        override def visitFile(file: Path, attrs: BasicFileAttributes) = {
          Files.copy(file, newPath(file), File.CopyOptions(overwrite): _*)
          super.visitFile(file, attrs)
        }
      })
    } else {
      Files.copy(path, destination.path, File.CopyOptions(overwrite): _*)
    }
    destination
  }

  def symbolicLinkTo(destination: File)(implicit attributes: File.Attributes = File.Attributes.default): destination.type = {
    Files.createSymbolicLink(path, destination.path, attributes: _*)
    destination
  }

  def linkTo(destination: File, symbolic: Boolean = false)(implicit attributes: File.Attributes = File.Attributes.default): destination.type = {
    if (symbolic) symbolicLinkTo(destination)(attributes) else {
      Files.createLink(path, destination.path)
      destination
    }
  }

  def listRelativePaths(implicit visitOptions: File.VisitOptions = File.VisitOptions.default): Iterator[Path] =
    walk()(visitOptions).map(relativize)

  def relativize(destination: File): Path =
    path relativize destination.path

  def isSamePathAs(that: File): Boolean =
    this.path == that.path

  def isSameFileAs(that: File): Boolean =
    Files.isSameFile(this.path, that.path)

  /**
    * @return true if this file is exactly same as that file
    *         For directories, it checks for equivalent directory structure
    */
  def isSameContentAs(that: File): Boolean =
    isSimilarContentAs(that)

  def `===`(that: File): Boolean =
    isSameContentAs(that)

  /**
    * Almost same as isSameContentAs but uses faster md5 hashing to compare (and thus small chance of false positive)
    * Also works for directories
    *
    * @param that
    * @return
    */
  def isSimilarContentAs(that: File): Boolean =
    this.md5 == that.md5

  def =!=(that: File): Boolean =
    !isSameContentAs(that)

  override def equals(obj: Any) = {
    obj match {
      case file: File => isSamePathAs(file)
      case _ => false
    }
  }

  /**
    * @return true if file is not present or empty directory or 0-bytes file
    */
  def isEmpty: Boolean = {
    this match {
      case File.Type.Directory(children) => children.isEmpty
      case File.Type.RegularFile(content) => content.isEmpty
      case _ => notExists
    }
  }

  /**
    * If this is a directory, remove all its children
    * If its a file, empty the contents
    *
    * @return this
    */
  def clear(): this.type = {
    this match {
      case File.Type.Directory(children) => children.foreach(_.delete())
      case _ => write(Array.emptyByteArray)(File.OpenOptions.default)
    }
    this
  }

  override def hashCode =
    path.hashCode()

  override def toString =
    pathAsString

  /**
    * Zips this file (or directory)
    *
    * @param destination The destination file; Creates this if it does not exists
    * @return The destination zip file
    */
  def zipTo(destination: File, compressionLevel: Int = Deflater.DEFAULT_COMPRESSION)(implicit codec: Codec): destination.type = {
    val files = if (isDirectory) children.toSeq else Seq(this)
    Cmds.zip(files: _*)(destination, compressionLevel)(codec)
    destination
  }

  /**
    * zip to a temp directory
    *
    * @return the target directory
    */
  def zip(compressionLevel: Int = Deflater.DEFAULT_COMPRESSION)(implicit codec: Codec): File =
    zipTo(destination = File.newTemporaryFile(name, ".zip"), compressionLevel)(codec)

  /**
    * Unzips this zip file
    *
    * @param destination destination folder; Creates this if it does not exist
    * @return The destination where contents are unzipped
    */
  def unzipTo(destination: File)(implicit codec: Codec): destination.type = {
    for {
      zipFile <- new ZipFile(toJava, codec).autoClosed
      entry <- zipFile.entries()
      file = destination.createChild(entry.getName, entry.isDirectory)
      if !entry.isDirectory
    } zipFile.getInputStream(entry) > file.newOutputStream
    destination
  }

  /**
    * unzip to a temporary zip file
    *
    * @return the zip file
    */
  def unzip()(implicit codec: Codec): File = unzipTo(destination = File.newTemporaryDirectory(name))(codec)

  //TODO: add features from https://github.com/sbt/io/blob/master/io/src/main/scala/sbt/io/IO.scala
}

object File {
  def newTemporaryDirectory(prefix: String = "", parent: Option[File] = None)(implicit attributes: Attributes = Attributes.default): File = {
    parent match {
      case Some(dir) => Files.createTempDirectory(dir.path, prefix, attributes: _*)
      case _ => Files.createTempDirectory(prefix, attributes: _*)
    }
  }

  def newTemporaryFile(prefix: String = "", suffix: String = "", parent: Option[File] = None)(implicit attributes: Attributes = Attributes.default): File = {
    parent match {
      case Some(dir) => Files.createTempFile(dir.path, prefix, suffix, attributes: _*)
      case _ => Files.createTempFile(prefix, suffix, attributes: _*)
    }
  }

  implicit def apply(path: Path): File =
    new File(path.toAbsolutePath.normalize())

  def apply(path: String, fragments: String*): File =
    Paths.get(path, fragments: _*)

  def apply(uri: URI): File =
    Paths.get(uri)

  def roots: Iterable[File] =
    FileSystems.getDefault.getRootDirectories.map(File.apply)

  def root: File =
    roots.head

  def home: File =
    Properties.userHome.toFile

  def temp: File =
    Properties.tmpDir.toFile

  def currentWorkingDirectory: File =
    File("")

  type Attributes = Seq[FileAttribute[_]]
  object Attributes {
    val default   : Attributes = Seq.empty
  }

  type CopyOptions = Seq[StandardCopyOption]
  object CopyOptions {
    def apply(overwrite: Boolean) : CopyOptions = if (overwrite) StandardCopyOption.REPLACE_EXISTING +: default else default
    val default                   : CopyOptions = Seq.empty //Seq(StandardCopyOption.COPY_ATTRIBUTES)
  }

  type Events = Seq[WatchEvent.Kind[_]]
  object Events {
    val all     : Events = Seq(StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE)
    val default : Events = all
  }

  type OpenOptions = Seq[OpenOption]
  object OpenOptions {
    val append  : OpenOptions = Seq(StandardOpenOption.APPEND, StandardOpenOption.CREATE)
    val default : OpenOptions = Seq.empty
  }

  type LinkOptions = Seq[LinkOption]
  object LinkOptions {
    val follow    : LinkOptions = Seq.empty
    val noFollow  : LinkOptions = Seq(LinkOption.NOFOLLOW_LINKS)
    val default   : LinkOptions = follow
  }

  type VisitOptions = Seq[FileVisitOption]
  object VisitOptions {
    val follow    : VisitOptions = Seq(FileVisitOption.FOLLOW_LINKS)
    val default   : VisitOptions = Seq.empty
  }

  type Order = Ordering[File]
  object Order {
    val bySize              : Order = Ordering.by(_.size)
    val byName              : Order = Ordering.by(_.name)
    val byDepth             : Order = Ordering.by(_.path.getNameCount)
    val byModificationTime  : Order = Ordering.by(_.lastModifiedTime)
    val byDirectoriesLast   : Order = Ordering.by(_.isDirectory)
    val byDirectoriesFirst  : Order = byDirectoriesLast.reverse
    val default             : Order = byDirectoriesFirst
  }

  /**
    * Denote various file types using this
    *
    * @tparam Content The type of underlying contents e.g. a directory has its children files as contents but a regular file may have bytes as contents
    */
  sealed trait Type[Content] {
    def unapply(file: File): Option[Content]
  }

  object Type {

    case object RegularFile extends Type[BufferedSource] {
      override def unapply(file: File) = when(file.isRegularFile)(file.newBufferedSource)
    }

    case object Directory extends Type[Files] {
      def unapply(file: File) = when(file.isDirectory)(file.children)
    }

    case object SymbolicLink extends Type[File] {
      def unapply(file: File) = file.symbolicLink
    }
  }

  class PathMatcherSyntax private (val name: String)
  object PathMatcherSyntax {
    val glob = new PathMatcherSyntax("glob")
    val regex = new PathMatcherSyntax("regex")
    val default = glob
    def other(syntax: String) = new PathMatcherSyntax(syntax)
  }

  class RandomAccessMode private(val value: String)
  object RandomAccessMode {
    val read = new RandomAccessMode("r")
    val readWrite = new RandomAccessMode("rw")
    val readWriteMetadataSynchronous = new RandomAccessMode("rws")
    val readWriteContentSynchronous = new RandomAccessMode("rwd")
  }

  def numberOfOpenFileDescriptors(): Long = java.lang.management.ManagementFactory.getOperatingSystemMXBean match {
    case os: com.sun.management.UnixOperatingSystemMXBean => os.getMaxFileDescriptorCount
    case os => throw new UnsupportedOperationException(s"Unsupported operating system: $os")
  }

  /**
    * Implement this interface to monitor the root file
    */
  trait Monitor {
    val root: File

    /**
      * Dispatch a StandardWatchEventKind to an appropriate callback
      * Override this if you don't want to manually handle onDelete/onCreate/onModify separately
      *
      * @param eventType
      * @param file
      */
    def onEvent(eventType: WatchEvent.Kind[Path], file: File): Unit = eventType match {
      case StandardWatchEventKinds.ENTRY_CREATE => onCreate(file)
      case StandardWatchEventKinds.ENTRY_MODIFY => onModify(file)
      case StandardWatchEventKinds.ENTRY_DELETE => onDelete(file)
    }

    def start(): Unit

    def onCreate(file: File): Unit

    def onModify(file: File): Unit

    def onDelete(file: File): Unit

    def onUnknownEvent(event: WatchEvent[_]): Unit

    def onException(exception: Throwable): Unit

    def stop(): Unit
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy