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

ammonite.ops.Path.scala Maven / Gradle / Ivy

package ammonite.ops

import java.io.InputStream
import java.nio.charset.Charset

import acyclic.file

import scala.io.Codec
import scala.util.Try

/**
  * Enforces a standard interface for constructing [[BasePath]]-like things
  * from java types of various sorts
  */
sealed trait PathFactory[PathType <: BasePath] extends (String => PathType){
  def apply(f: java.io.File): PathType = apply(f.getPath)
  def apply(s: String): PathType = apply(java.nio.file.Paths.get(s))
  def apply(f: java.nio.file.Path): PathType
}

/**
  * A path which is either an absolute [[Path]], a relative [[RelPath]],
  * or a [[ResourcePath]] with shared APIs and implementations.
  *
  * Most of the filesystem-independent path-manipulation logic that lets you
  * splice paths together or navigate in and out of paths lives in this interface
  */
sealed trait BasePath{
  type ThisType <: BasePath
  /**
    * The individual path segments of this path.
    */
  def segments: Seq[String]

  /**
    * Combines this path with the given relative path, returning
    * a path of the same type as this one (e.g. `Path` returns `Path`,
    * `RelPath` returns `RelPath`
    */
  def /(subpath: RelPath): ThisType

  /**
    * Relativizes this path with the given `base` path, finding a
    * relative path `p` such that base/p == this.
    *
    * Note that you can only relativize paths of the same type, e.g.
    * `Path` & `Path` or `RelPath` & `RelPath`. In the case of `RelPath`,
    * this can throw a [[PathError.NoRelativePath]] if there is no
    * relative path that satisfies the above requirement in the general
    * case.
    */
  def relativeTo(target: ThisType): RelPath

  /**
    * This path starts with the target path, including if it's identical
    */
  def startsWith(target: ThisType): Boolean

  /**
    * The last segment in this path. Very commonly used, e.g. it
    * represents the name of the file/folder in filesystem paths
    */
  def last: String

  /**
    * Gives you the file extension of this path, or the empty
    * string if there is no extension
    */
  def ext: String
}

object BasePath {

  def invalidChars = Set('/')
  def checkSegment(s: String) = {
    def fail(msg: String) = throw PathError.InvalidSegment(s, msg)
    def considerStr =
      "use the Path(...) or RelPath(...) constructor calls to convert them. "

    s.find(BasePath.invalidChars) match{
      case Some(c) => fail(
        s"[$c] is not a valid character to appear in a path segment. " +
          "If you want to parse an absolute or relative path that may have " +
          "multiple segments, e.g. path-strings coming from external sources" +
          considerStr
      )
      case None =>
    }
    def externalStr = "If you are dealing with path-strings coming from external sources, "
    s match{
      case "" =>
        fail(
          "Ammonite-Ops does not allow empty path segments " +
            externalStr + considerStr
        )
      case "." =>
        fail(
          "Ammonite-Ops does not allow [.] as a path segment " +
            externalStr + considerStr
        )
      case ".." =>
        fail(
          "Ammonite-Ops does not allow [..] as a path segment " +
            externalStr +
            considerStr +
            "If you want to use the `..` segment manually to represent going up " +
            "one level in the path, use the `up` segment from `ammonite.ops.up` " +
            "e.g. an external path foo/bar/../baz translates into 'foo/'bar/up/'baz."
        )
      case _ =>
    }
  }
  def chunkify(s: java.nio.file.Path) = {
    import collection.JavaConversions._
    s.iterator().map(_.toString).filter(_ != ".").filter(_ != "").toVector
  }
}


/**
  * Represents a value that is either an absolute [[Path]] or a
  * relative [[ResourcePath]], and can be constructed from
  */
sealed trait FilePath extends BasePath
object FilePath extends PathFactory[FilePath]{
  def apply(f: java.nio.file.Path) = {
    if (f.isAbsolute) Path(f)
    else RelPath(f)
  }
}

trait BasePathImpl extends BasePath{
  def segments: Seq[String]

  protected[this] def make(p: Seq[String], ups: Int): ThisType

  def /(subpath: RelPath) = make(
    segments.dropRight(subpath.ups) ++ subpath.segments,
    math.max(subpath.ups - segments.length, 0)
  )

  def ext = {
    if (!segments.last.contains('.')) ""
    else segments.last.split('.').lastOption.getOrElse("")
  }

  def last = segments.last
}

/**
 * An absolute path on the filesystem. Note that the path is
 * normalized and cannot contain any empty or ".". Parent ".."
 * segments can only occur at the left-end of the path, and
 * are collapsed into a single number [[ups]].
 */
case class RelPath private[ops] (segments: Vector[String], ups: Int)
extends FilePath with BasePathImpl{
  type ThisType = RelPath
  require(ups >= 0)
  protected[this] def make(p: Seq[String], ups: Int) = new RelPath(p.toVector, ups + this.ups)
  def relativeTo(base: RelPath): RelPath = {
    if (base.ups < ups) {
      new RelPath(segments, ups + base.segments.length)
    } else if (base.ups == ups) {
      val commonPrefix = {
        val maxSize = scala.math.min(segments.length, base.segments.length)
        var i = 0
        while ( i < maxSize && segments(i) == base.segments(i)) i += 1
        i
      }
      val newUps = base.segments.length - commonPrefix

      new RelPath(segments.drop(commonPrefix), ups + newUps)
    } else throw PathError.NoRelativePath(this, base)
  }

  def startsWith(target: RelPath) = {
    this.segments.startsWith(target.segments) && this.ups == target.ups
  }

  override def toString = segments.mkString("/")
  override def hashCode = segments.hashCode() + ups.hashCode()
  override def equals(o: Any): Boolean = o match {
    case p: RelPath => segments == p.segments && p.ups == ups
    case _ => false
  }
}

object RelPath extends RelPathStuff with PathFactory[RelPath]{
  def apply(f: java.nio.file.Path): RelPath = {

    import collection.JavaConversions._
    require(!f.isAbsolute, f + " is not an relative path")

    val segments = BasePath.chunkify(f.normalize())
    val (ups, rest) = segments.partition(_ == "..")
    new RelPath(rest, ups.length)
  }

  implicit def SymPath(s: Symbol): RelPath = StringPath(s.name)
  implicit def StringPath(s: String): RelPath = {
    BasePath.checkSegment(s)
    new RelPath(Vector(s), 0)

  }

  implicit def SeqPath[T](s: Seq[T])(implicit conv: T => RelPath): RelPath = {
    s.foldLeft(empty){_ / _}
  }

  implicit def ArrayPath[T](s: Array[T])(implicit conv: T => RelPath): RelPath = SeqPath(s)


  implicit val relPathOrdering: Ordering[RelPath] =
    Ordering.by((rp: RelPath) => (rp.ups, rp.segments.length, rp.segments.toIterable))
}
trait RelPathStuff{
  val up: RelPath = new RelPath(Vector.empty, 1)
  val empty: RelPath = new RelPath(Vector.empty, 0)
  implicit class RelPathStart(p1: String){
    def /(subpath: RelPath) = empty/p1/subpath
  }
  implicit class RelPathStart2(p1: Symbol){
    def /(subpath: RelPath) = empty/p1/subpath
  }
}


object Path extends PathFactory[Path]{
  def apply(p: FilePath, base: Path) = p match{
    case p: RelPath => base/p
    case p: Path => p
  }
  def apply(f: java.io.File, base: Path): Path = apply(FilePath(f), base)
  def apply(s: String, base: Path): Path = apply(FilePath(s), base)
  def apply(f: java.nio.file.Path, base: Path): Path = apply(FilePath(f), base)
  def apply(f: java.nio.file.Path): Path = {
    import collection.JavaConversions._
    val chunks = BasePath.chunkify(f)
    if (chunks.count(_ == "..") > chunks.size / 2) throw PathError.AbsolutePathOutsideRoot

    require(f.isAbsolute, f + " is not an absolute path")
    Path(f.getRoot, BasePath.chunkify(f.normalize()))
  }

  val root = Path(java.nio.file.Paths.get("").toAbsolutePath.getRoot)
  val home = Path(System.getProperty("user.home"))

  implicit val pathOrdering: Ordering[Path] =
    Ordering.by((rp: Path) => (rp.segments.length, rp.segments.toIterable))
}

/**
 * An absolute path on the filesystem. Note that the path is
 * normalized and cannot contain any empty `""`, `"."` or `".."` segments
 */
case class Path private[ops] (root: java.nio.file.Path, segments: Vector[String])
extends FilePath with BasePathImpl with Readable{
  protected[ops] def getInputStream = java.nio.file.Files.newInputStream(toNIO)
  type ThisType = Path

  def toNIO = root.resolve(segments.mkString(root.getFileSystem.getSeparator))

  protected[this] def make(p: Seq[String], ups: Int) = {
    if (ups > 0){
      throw PathError.AbsolutePathOutsideRoot
    }
    new Path(root, p.toVector)
  }
  override def toString = toNIO.toString

  override def equals(o: Any): Boolean = o match {
    case p: Path => segments == p.segments
    case _ => false
  }
  override def hashCode = segments.hashCode()

  def startsWith(target: Path) = this.segments.startsWith(target.segments)

  /**
    * Obtain the final path to a file by resolving symlinks if any.
    * @return Some(path) or else None if the symlink is invalid or other error.
    */
  def tryFollowLinks: Option[Path] = Try(Path(toNIO.toRealPath())).toOption

  def relativeTo(base: Path): RelPath = {
    var newUps = 0
    var s2 = base.segments

    while(!segments.startsWith(s2)){
      s2 = s2.dropRight(1)
      newUps += 1
    }
    RelPath(segments.drop(s2.length), newUps)
  }

  def toIO = toNIO.toFile

  override def getBytes = java.nio.file.Files.readAllBytes(toNIO)
  import collection.JavaConversions._

  override def getLines(charSet: Codec) = {
    java.nio.file.Files.readAllLines(toNIO, charSet.charSet).toVector
  }
}


object ResourcePath{
  def resource(resRoot: ResourceRoot) = {
    ResourcePath(resRoot, Vector.empty)
  }
}

/**
  * Classloaders are tricky: http://stackoverflow.com/questions/12292926
  *
  * @param resRoot
  * @param segments
  */
case class ResourcePath private[ops](resRoot: ResourceRoot, segments: Vector[String])
  extends BasePathImpl with Readable{
  type ThisType = ResourcePath
  override def toString = resRoot.errorName + "/" + segments.mkString("/")

  protected[ops] def getInputStream = {
    resRoot.getResourceAsStream(segments.mkString("/")) match{
      case null => throw new ResourceNotFoundException(this)
      case stream => stream
    }
  }
  protected[this] def make(p: Seq[String], ups: Int) = {
    if (ups > 0){
      throw PathError.AbsolutePathOutsideRoot
    }
    new ResourcePath(resRoot, p.toVector)
  }

  def relativeTo(base: ResourcePath) = {
    var newUps = 0
    var s2 = base.segments

    while(!segments.startsWith(s2)){
      s2 = s2.dropRight(1)
      newUps += 1
    }
    RelPath(segments.drop(s2.length), newUps)
  }


  def startsWith(target: ResourcePath) = {
    segments.startsWith(target.segments)
  }

}


/**
  * Thrown when you try to read from a resource that doesn't exist.
  * @param path
  */
case class ResourceNotFoundException(path: ResourcePath) extends Exception(path.toString)



object PathError{
  type IAE = IllegalArgumentException
  private[this] def errorMsg(s: String, msg: String) =
    s"[$s] is not a valid path segment. $msg"

  case class InvalidSegment(segment: String, msg: String) extends IAE(errorMsg(segment, msg))

  case object AbsolutePathOutsideRoot
    extends IAE("The path created has enough ..s that it would start outside the root directory")

  case class NoRelativePath(src: RelPath, base: RelPath)
    extends IAE(s"Can't relativize relative paths $src from $base")
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy