summaryrefslogtreecommitdiff
path: root/main/api/src/mill/api/PathRef.scala
blob: f2312ba8623b358192a2a82d41bffadbebedb184 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package mill.api

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(): Int = sig
}

object PathRef {
  /**
   * Create a [[PathRef]] by recursively digesting the content of a given `path`.
   * @param path The digested path.
   * @param quick If `true` the digest is only based to some file attributes (like mtime and size).
   *              If `false` the digest is created of the files content.
   * @return
   */
  def apply(path: os.Path, quick: Boolean = false): PathRef = {
    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)
              StreamSupport.stream(is, digestOut)
              is.close()
            }
          }
        }
      }

      java.util.Arrays.hashCode(digest.digest())

    }
    new PathRef(path, quick, sig)
  }

  /**
    * Default JSON formatter for [[PathRef]].
    */
  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
      )
    }
  )
}