summaryrefslogtreecommitdiff
path: root/main/api/src/mill/api/PathRef.scala
diff options
context:
space:
mode:
Diffstat (limited to 'main/api/src/mill/api/PathRef.scala')
-rw-r--r--main/api/src/mill/api/PathRef.scala122
1 files changed, 122 insertions, 0 deletions
diff --git a/main/api/src/mill/api/PathRef.scala b/main/api/src/mill/api/PathRef.scala
new file mode 100644
index 00000000..24f3627e
--- /dev/null
+++ b/main/api/src/mill/api/PathRef.scala
@@ -0,0 +1,122 @@
+package mill.api
+
+import java.io.IOException
+import java.nio.file.attribute.BasicFileAttributes
+import java.nio.file.{FileVisitResult, FileVisitor}
+import java.nio.{file => jnio}
+import java.security.{DigestOutputStream, MessageDigest}
+
+import upickle.default.{ReadWriter => RW}
+
+
+/**
+ * A wrapper around `os.Path` that calculates it's hashcode based
+ * on the contents of the filesystem underneath it. Used to ensure filesystem
+ * changes can bust caches which are keyed off hashcodes.
+ */
+case class PathRef(path: os.Path, quick: Boolean, sig: Int){
+ override def hashCode() = sig
+}
+
+object PathRef{
+ def apply(path: os.Path, quick: Boolean = false) = {
+ val sig = {
+ val digest = MessageDigest.getInstance("MD5")
+ val digestOut = new DigestOutputStream(DummyOutputStream, digest)
+ if (os.exists(path)){
+ for((path, attrs) <- os.walk.attrs(path, includeTarget = true, followLinks = true)){
+ digest.update(path.toString.getBytes)
+ if (!attrs.isDir) {
+ if (quick){
+ val value = (attrs.mtime, attrs.size).hashCode()
+ digest.update((value >>> 24).toByte)
+ digest.update((value >>> 16).toByte)
+ digest.update((value >>> 8).toByte)
+ digest.update(value.toByte)
+ } else if (jnio.Files.isReadable(path.toNIO)) {
+ val is = os.read.inputStream(path)
+ IO.stream(is, digestOut)
+ is.close()
+ }
+ }
+ }
+ }
+
+ java.util.Arrays.hashCode(digest.digest())
+
+ }
+ new PathRef(path, quick, sig)
+ }
+
+ implicit def jsonFormatter: RW[PathRef] = upickle.default.readwriter[String].bimap[PathRef](
+ p => {
+ (if (p.quick) "qref" else "ref") + ":" +
+ String.format("%08x", p.sig: Integer) + ":" +
+ p.path.toString()
+ },
+ s => {
+ val Array(prefix, hex, path) = s.split(":", 3)
+ PathRef(
+ os.Path(path),
+ prefix match{ case "qref" => true case "ref" => false},
+ // Parsing to a long and casting to an int is the only way to make
+ // round-trip handling of negative numbers work =(
+ java.lang.Long.parseLong(hex, 16).toInt
+ )
+ }
+ )
+}
+
+
+import java.io.{InputStream, OutputStream}
+
+/**
+ * Misc IO utilities, eventually probably should be pushed upstream into
+ * ammonite-ops
+ */
+object IO {
+ def stream(src: InputStream, dest: OutputStream) = {
+ val buffer = new Array[Byte](4096)
+ while ( {
+ src.read(buffer) match {
+ case -1 => false
+ case n =>
+ dest.write(buffer, 0, n)
+ true
+ }
+ }) ()
+ }
+
+
+ def unpackZip(src: os.Path, dest: os.RelPath = "unpacked")
+ (implicit ctx: Ctx.Dest) = {
+
+ val byteStream = os.read.inputStream(src)
+ val zipStream = new java.util.zip.ZipInputStream(byteStream)
+ while({
+ zipStream.getNextEntry match{
+ case null => false
+ case entry =>
+ if (!entry.isDirectory) {
+ val entryDest = ctx.dest / dest / os.RelPath(entry.getName)
+ os.makeDir.all(entryDest / os.up)
+ val fileOut = new java.io.FileOutputStream(entryDest.toString)
+ IO.stream(zipStream, fileOut)
+ fileOut.close()
+ }
+ zipStream.closeEntry()
+ true
+ }
+ })()
+ PathRef(ctx.dest / dest)
+ }
+}
+
+import java.io.{ByteArrayInputStream, OutputStream}
+
+object DummyInputStream extends ByteArrayInputStream(Array())
+object DummyOutputStream extends java.io.OutputStream{
+ override def write(b: Int) = ()
+ override def write(b: Array[Byte]) = ()
+ override def write(b: Array[Byte], off: Int, len: Int) = ()
+}