sbt/cache/src/main/scala/coursier/Cache.scala

966 lines
29 KiB
Scala
Raw Normal View History

2015-06-25 01:18:57 +02:00
package coursier
import java.math.BigInteger
2016-03-06 14:45:58 +01:00
import java.net.{ HttpURLConnection, URL, URLConnection, URLStreamHandler }
2015-07-06 02:48:26 +02:00
import java.nio.channels.{ OverlappingFileLockException, FileLock }
2015-12-30 01:34:48 +01:00
import java.nio.file.{ StandardCopyOption, Files => NioFiles }
import java.security.MessageDigest
import java.util.concurrent.{ ConcurrentHashMap, Executors, ExecutorService }
import java.util.regex.Pattern
2015-06-25 01:18:57 +02:00
2016-05-06 13:53:55 +02:00
import coursier.core.Authentication
2015-12-31 16:26:18 +01:00
import coursier.ivy.IvyRepository
2016-05-06 13:53:55 +02:00
import coursier.util.Base64.Encoder
2015-12-31 16:26:18 +01:00
2015-06-25 01:18:57 +02:00
import scala.annotation.tailrec
import scalaz._
import scalaz.Scalaz.ToEitherOps
import scalaz.concurrent.{ Task, Strategy }
2015-06-25 01:18:57 +02:00
2015-12-30 01:34:32 +01:00
import java.io.{ Serializable => _, _ }
2015-06-25 01:18:57 +02:00
2016-05-31 15:18:33 +02:00
import scala.concurrent.duration.{ Duration, DurationInt }
2016-05-31 15:18:29 +02:00
import scala.util.Try
2016-05-06 13:53:55 +02:00
trait AuthenticatedURLConnection extends URLConnection {
def authenticate(authentication: Authentication): Unit
}
2015-12-30 01:34:41 +01:00
object Cache {
// Check SHA-1 if available, else be fine with no checksum
val defaultChecksums = Seq(Some("SHA-1"), None)
private val unsafeChars: Set[Char] = " %$&+,:;=?@<>#".toSet
// Scala version of http://stackoverflow.com/questions/4571346/how-to-encode-url-to-avoid-special-characters-in-java/4605848#4605848
// '/' was removed from the unsafe character list
private def escape(input: String): String = {
def toHex(ch: Int) =
(if (ch < 10) '0' + ch else 'A' + ch - 10).toChar
def isUnsafe(ch: Char) =
ch > 128 || ch < 0 || unsafeChars(ch)
input.flatMap {
case ch if isUnsafe(ch) =>
"%" + toHex(ch / 16) + toHex(ch % 16)
case other =>
other.toString
}
}
2016-05-06 13:53:55 +02:00
private def localFile(url: String, cache: File, user: Option[String]): File = {
2016-04-05 16:24:39 +02:00
val path =
if (url.startsWith("file:///"))
url.stripPrefix("file://")
else if (url.startsWith("file:/"))
url.stripPrefix("file:")
else
// FIXME Should we fully parse the URL here?
// FIXME Should some safeguards be added against '..' components in paths?
url.split(":", 2) match {
case Array(protocol, remaining) =>
val remaining0 =
if (remaining.startsWith("///"))
remaining.stripPrefix("///")
else if (remaining.startsWith("/"))
remaining.stripPrefix("/")
else
throw new Exception(s"URL $url doesn't contain an absolute path")
2016-05-29 23:45:58 +02:00
val remaining1 =
if (remaining0.endsWith("/"))
// keeping directory content in .directory files
remaining0 + ".directory"
else
remaining0
2016-05-06 13:53:55 +02:00
new File(
cache,
2016-05-29 23:45:58 +02:00
escape(protocol + "/" + user.fold("")(_ + "@") + remaining1.dropWhile(_ == '/'))
2016-05-06 13:53:55 +02:00
).toString
case _ =>
throw new Exception(s"No protocol found in URL $url")
}
2016-04-05 16:24:39 +02:00
new File(path)
}
2015-12-30 01:34:48 +01:00
private def readFullyTo(
in: InputStream,
out: OutputStream,
logger: Option[Logger],
2015-12-30 01:34:49 +01:00
url: String,
alreadyDownloaded: Long
2015-12-30 01:34:48 +01:00
): Unit = {
val b = Array.fill[Byte](bufferSize)(0)
@tailrec
def helper(count: Long): Unit = {
val read = in.read(b)
if (read >= 0) {
out.write(b, 0, read)
out.flush()
logger.foreach(_.downloadProgress(url, count + read))
helper(count + read)
}
}
2015-12-30 01:34:49 +01:00
helper(alreadyDownloaded)
2015-12-30 01:34:48 +01:00
}
private val processStructureLocks = new ConcurrentHashMap[File, AnyRef]
/**
* Should be acquired when doing operations changing the file structure of the cache (creating
* new directories, creating / acquiring locks, ...), so that these don't hinder each other.
*
* Should hopefully address some transient errors seen on the CI of ensime-server.
*/
private def withStructureLock[T](cache: File)(f: => T): T = {
val intraProcessLock = Option(processStructureLocks.get(cache)).getOrElse {
val lock = new AnyRef
val prev = Option(processStructureLocks.putIfAbsent(cache, lock))
prev.getOrElse(lock)
}
intraProcessLock.synchronized {
val lockFile = new File(cache, ".structure.lock")
lockFile.getParentFile.mkdirs()
var out = new FileOutputStream(lockFile)
try {
var lock: FileLock = null
try {
lock = out.getChannel.lock()
try f
finally {
lock.release()
lock = null
out.close()
out = null
lockFile.delete()
}
}
finally if (lock != null) lock.release()
} finally if (out != null) out.close()
}
}
private def withLockFor[T](cache: File, file: File)(f: => FileError \/ T): FileError \/ T = {
2015-12-30 01:34:48 +01:00
val lockFile = new File(file.getParentFile, s"${file.getName}.lock")
var out: FileOutputStream = null
withStructureLock(cache) {
lockFile.getParentFile.mkdirs()
out = new FileOutputStream(lockFile)
}
2015-12-30 01:34:48 +01:00
try {
var lock: FileLock = null
try {
lock = out.getChannel.tryLock()
if (lock == null)
-\/(FileError.Locked(file))
else
try f
finally {
lock.release()
lock = null
out.close()
out = null
lockFile.delete()
}
}
catch {
case e: OverlappingFileLockException =>
-\/(FileError.Locked(file))
}
finally if (lock != null) lock.release()
} finally if (out != null) out.close()
}
private def downloading[T](
url: String,
file: File,
logger: Option[Logger]
)(
f: => FileError \/ T
): FileError \/ T =
try {
val o = new Object
val prev = urlLocks.putIfAbsent(url, o)
if (prev == null) {
logger.foreach(_.downloadingArtifact(url, file))
val res =
try \/-(f)
catch {
case nfe: FileNotFoundException if nfe.getMessage != null =>
logger.foreach(_.downloadedArtifact(url, success = false))
-\/(-\/(FileError.NotFound(nfe.getMessage)))
case e: Exception =>
logger.foreach(_.downloadedArtifact(url, success = false))
throw e
2015-12-30 01:34:48 +01:00
}
finally {
urlLocks.remove(url)
}
for (res0 <- res)
logger.foreach(_.downloadedArtifact(url, success = res0.isRight))
2015-12-30 01:34:48 +01:00
res.merge
2015-12-30 01:34:48 +01:00
} else
-\/(FileError.ConcurrentDownload(url))
}
catch { case e: Exception =>
2016-05-06 13:53:53 +02:00
-\/(FileError.DownloadError(s"Caught $e${Option(e.getMessage).fold("")(" (" + _ + ")")}"))
2015-12-30 01:34:48 +01:00
}
private def temporaryFile(file: File): File = {
val dir = file.getParentFile
val name = file.getName
new File(dir, s"$name.part")
}
2015-12-30 01:34:49 +01:00
private val partialContentResponseCode = 206
2016-03-06 14:45:58 +01:00
private val handlerClsCache = new ConcurrentHashMap[String, Option[URLStreamHandler]]
private def handlerFor(url: String): Option[URLStreamHandler] = {
val protocol = url.takeWhile(_ != ':')
Option(handlerClsCache.get(protocol)) match {
case None =>
val clsName = s"coursier.cache.protocol.${protocol.capitalize}Handler"
val clsOpt =
try Some(Thread.currentThread().getContextClassLoader.loadClass(clsName))
catch {
case _: ClassNotFoundException =>
None
}
def printError(e: Exception): Unit =
scala.Console.err.println(
2016-05-06 13:53:53 +02:00
s"Cannot instantiate $clsName: $e${Option(e.getMessage).fold("")(" ("+_+")")}"
2016-03-06 14:45:58 +01:00
)
val handlerOpt = clsOpt.flatMap {
cls =>
try Some(cls.newInstance().asInstanceOf[URLStreamHandler])
catch {
case e: InstantiationException =>
printError(e)
None
case e: IllegalAccessException =>
printError(e)
None
case e: ClassCastException =>
printError(e)
None
}
}
val prevOpt = Option(handlerClsCache.putIfAbsent(protocol, handlerOpt))
prevOpt.getOrElse(handlerOpt)
case Some(handlerOpt) =>
handlerOpt
}
}
2016-05-06 13:53:55 +02:00
private val BasicRealm = (
"^" +
Pattern.quote("Basic realm=\"") +
"([^" + Pattern.quote("\"") + "]*)" +
Pattern.quote("\"") +
"$"
).r
private def basicAuthenticationEncode(user: String, password: String): String =
(user + ":" + password).getBytes("UTF-8").toBase64
2016-03-06 14:45:58 +01:00
/**
* Returns a `java.net.URL` for `s`, possibly using the custom protocol handlers found under the
* `coursier.cache.protocol` namespace.
*
* E.g. URL `"test://abc.com/foo"`, having protocol `"test"`, can be handled by a
* `URLStreamHandler` named `coursier.cache.protocol.TestHandler` (protocol name gets
* capitalized, and suffixed with `Handler` to get the class name).
*
* @param s
* @return
*/
def url(s: String): URL =
new URL(null, s, handlerFor(s).orNull)
2015-12-30 01:34:43 +01:00
private def download(
artifact: Artifact,
cache: File,
2015-12-30 01:34:32 +01:00
checksums: Set[String],
cachePolicy: CachePolicy,
2015-12-30 01:34:41 +01:00
pool: ExecutorService,
2016-05-31 15:18:29 +02:00
logger: Option[Logger] = None,
2016-05-31 15:18:33 +02:00
ttl: Option[Duration] = defaultTtl
): Task[Seq[((File, String), FileError \/ Unit)]] = {
2015-12-30 01:34:41 +01:00
implicit val pool0 = pool
// Reference file - if it exists, and we get not found errors on some URLs, we assume
// we can keep track of these missing, and not try to get them again later.
2016-04-05 16:24:39 +02:00
val referenceFileOpt = artifact
.extra
.get("metadata")
2016-05-06 13:53:55 +02:00
.map(a => localFile(a.url, cache, a.authentication.map(_.user)))
def referenceFileExists: Boolean = referenceFileOpt.exists(_.exists())
2016-03-06 14:45:58 +01:00
def urlConn(url0: String) = {
val conn = url(url0).openConnection() // FIXME Should this be closed?
2015-12-30 01:34:33 +01:00
// Dummy user-agent instead of the default "Java/...",
// so that we are not returned incomplete/erroneous metadata
// (Maven 2 compatibility? - happens for snapshot versioning metadata,
2015-12-30 01:34:43 +01:00
// this is SO FSCKING CRAZY)
2015-12-30 01:34:33 +01:00
conn.setRequestProperty("User-Agent", "")
2016-05-06 13:53:55 +02:00
for (auth <- artifact.authentication)
conn match {
case authenticated: AuthenticatedURLConnection =>
authenticated.authenticate(auth)
case conn0: HttpURLConnection =>
conn0.setRequestProperty(
"Authorization",
"Basic " + basicAuthenticationEncode(auth.user, auth.password)
)
case _ =>
// FIXME Authentication is ignored
}
2015-12-30 01:34:33 +01:00
conn
}
2015-12-30 01:34:32 +01:00
2015-12-30 01:34:33 +01:00
def fileLastModified(file: File): EitherT[Task, FileError, Option[Long]] =
EitherT {
Task {
\/- {
val lastModified = file.lastModified()
if (lastModified > 0L)
Some(lastModified)
else
None
} : FileError \/ Option[Long]
}
}
def urlLastModified(
url: String,
currentLastModifiedOpt: Option[Long], // for the logger
logger: Option[Logger]
): EitherT[Task, FileError, Option[Long]] =
2015-12-30 01:34:33 +01:00
EitherT {
Task {
urlConn(url) match {
case c: HttpURLConnection =>
logger.foreach(_.checkingUpdates(url, currentLastModifiedOpt))
2015-12-30 01:34:33 +01:00
var success = false
try {
c.setRequestMethod("HEAD")
val remoteLastModified = c.getLastModified
// TODO 404 Not found could be checked here
val res =
if (remoteLastModified > 0L)
Some(remoteLastModified)
else
None
success = true
logger.foreach(_.checkingUpdatesResult(url, currentLastModifiedOpt, res))
res.right
} finally {
if (!success)
logger.foreach(_.checkingUpdatesResult(url, currentLastModifiedOpt, None))
2015-12-30 01:34:33 +01:00
}
2015-11-29 20:21:45 +01:00
2015-12-30 01:34:33 +01:00
case other =>
-\/(FileError.DownloadError(s"Cannot do HEAD request with connection $other ($url)"))
}
}
2015-12-30 01:34:32 +01:00
}
def fileExists(file: File): Task[Boolean] =
Task {
file.exists()
}
2016-05-31 15:18:29 +02:00
def ttlFile(file: File): File =
new File(file.getParent, s".${file.getName}.checked")
def lastCheck(file: File): Task[Option[Long]] = {
val ttlFile0 = ttlFile(file)
Task {
if (ttlFile0.exists())
Some(ttlFile0.lastModified()).filter(_ > 0L)
else
None
}
}
/** Not wrapped in a `Task` !!! */
def doTouchCheckFile(file: File): Unit = {
val ts = System.currentTimeMillis()
val f = ttlFile(file)
if (f.exists())
f.setLastModified(ts)
else {
val fos = new FileOutputStream(f)
fos.write(Array.empty[Byte])
fos.close()
}
}
def shouldDownload(file: File, url: String): EitherT[Task, FileError, Boolean] = {
2016-05-31 15:18:29 +02:00
2016-05-31 15:18:33 +02:00
def checkNeeded = ttl.fold(Task.now(true)) { ttl =>
if (ttl.isFinite())
lastCheck(file).flatMap {
case None => Task.now(true)
case Some(ts) =>
Task(System.currentTimeMillis()).map(_ > ts + ttl.toMillis)
}
else
Task.now(false)
2016-05-31 15:18:29 +02:00
}
def check = for {
2015-12-30 01:34:33 +01:00
fileLastModOpt <- fileLastModified(file)
urlLastModOpt <- urlLastModified(url, fileLastModOpt, logger)
2015-12-30 01:34:33 +01:00
} yield {
val fromDatesOpt = for {
fileLastMod <- fileLastModOpt
urlLastMod <- urlLastModOpt
} yield fileLastMod < urlLastMod
fromDatesOpt.getOrElse(true)
}
2015-12-30 01:34:32 +01:00
EitherT {
fileExists(file).flatMap {
case false =>
Task.now(true.right)
case true =>
2016-05-31 15:18:29 +02:00
checkNeeded.flatMap {
case false =>
Task.now(false.right)
case true =>
check.run.flatMap {
case \/-(false) =>
Task {
doTouchCheckFile(file)
\/-(false)
}
case other =>
Task.now(other)
}
}
}
}
}
2016-05-06 13:53:55 +02:00
def responseCode(conn: URLConnection): Option[Int] =
conn match {
case conn0: HttpURLConnection =>
2016-05-06 13:53:55 +02:00
Some(conn0.getResponseCode)
case _ =>
None
}
def realm(conn: URLConnection): Option[String] =
conn match {
case conn0: HttpURLConnection =>
Option(conn0.getHeaderField("WWW-Authenticate")).collect {
case BasicRealm(realm) => realm
}
case _ =>
2016-05-06 13:53:55 +02:00
None
}
2016-05-06 13:53:55 +02:00
def remote(
file: File,
url: String
): EitherT[Task, FileError, Unit] =
2015-12-30 01:34:32 +01:00
EitherT {
Task {
withLockFor(cache, file) {
2015-12-30 01:34:48 +01:00
downloading(url, file, logger) {
2015-12-30 01:34:49 +01:00
val tmp = temporaryFile(file)
val alreadyDownloaded = tmp.length()
val conn0 = urlConn(url)
val (partialDownload, conn) = conn0 match {
case conn0: HttpURLConnection if alreadyDownloaded > 0L =>
conn0.setRequestProperty("Range", s"bytes=$alreadyDownloaded-")
if (conn0.getResponseCode == partialContentResponseCode) {
val ackRange = Option(conn0.getHeaderField("Content-Range")).getOrElse("")
if (ackRange.startsWith(s"bytes $alreadyDownloaded-"))
(true, conn0)
else
// unrecognized Content-Range header -> start a new connection with no resume
(false, urlConn(url))
} else
(false, conn0)
case _ => (false, conn0)
}
2016-05-06 13:53:55 +02:00
if (responseCode(conn) == Some(404))
FileError.NotFound(url, permanent = Some(true)).left
2016-05-06 13:53:55 +02:00
else if (responseCode(conn) == Some(401))
FileError.Unauthorized(url, realm = realm(conn)).left
else {
for (len0 <- Option(conn.getContentLengthLong) if len0 >= 0L) {
val len = len0 + (if (partialDownload) alreadyDownloaded else 0L)
logger.foreach(_.downloadLength(url, len, alreadyDownloaded))
}
2015-12-30 01:34:49 +01:00
val in = new BufferedInputStream(conn.getInputStream, bufferSize)
2015-12-30 01:34:48 +01:00
val result =
try {
val out = withStructureLock(cache) {
tmp.getParentFile.mkdirs()
new FileOutputStream(tmp, partialDownload)
}
2016-05-31 15:18:29 +02:00
try readFullyTo(in, out, logger, url, if (partialDownload) alreadyDownloaded else 0L)
finally out.close()
} finally in.close()
2015-12-30 01:34:49 +01:00
withStructureLock(cache) {
file.getParentFile.mkdirs()
NioFiles.move(tmp.toPath, file.toPath, StandardCopyOption.ATOMIC_MOVE)
}
2015-12-30 01:34:49 +01:00
for (lastModified <- Option(conn.getLastModified) if lastModified > 0L)
file.setLastModified(lastModified)
2015-12-30 01:34:48 +01:00
2016-05-31 15:18:29 +02:00
doTouchCheckFile(file)
result.right
}
}
}
}
}
2015-12-30 01:34:48 +01:00
def remoteKeepErrors(file: File, url: String): EitherT[Task, FileError, Unit] = {
2015-12-30 01:34:48 +01:00
val errFile = new File(file.getParentFile, "." + file.getName + ".error")
2015-12-30 01:34:48 +01:00
def validErrFileExists =
EitherT {
Task {
(referenceFileExists && errFile.exists()).right[FileError]
}
}
2015-12-30 01:34:48 +01:00
def createErrFile =
EitherT {
Task {
if (referenceFileExists) {
if (!errFile.exists())
NioFiles.write(errFile.toPath, "".getBytes("UTF-8"))
2015-12-30 01:34:48 +01:00
}
().right[FileError]
}
}
def deleteErrFile =
EitherT {
Task {
if (errFile.exists())
errFile.delete()
().right[FileError]
2015-12-30 01:34:32 +01:00
}
}
def retainError =
EitherT {
remote(file, url).run.flatMap {
case err @ -\/(FileError.NotFound(_, Some(true))) =>
createErrFile.run.map(_ => err)
case other =>
deleteErrFile.run.map(_ => other)
}
}
cachePolicy match {
case CachePolicy.FetchMissing | CachePolicy.LocalOnly | CachePolicy.LocalUpdate | CachePolicy.LocalUpdateChanging =>
validErrFileExists.flatMap { exists =>
if (exists)
EitherT(Task.now(FileError.NotFound(url, Some(true)).left[Unit]))
else
retainError
}
case CachePolicy.ForceDownload | CachePolicy.Update | CachePolicy.UpdateChanging =>
retainError
}
}
def checkFileExists(file: File, url: String, log: Boolean = true): EitherT[Task, FileError, Unit] =
2015-12-30 01:34:33 +01:00
EitherT {
Task {
if (file.exists()) {
logger.foreach(_.foundLocally(url, file))
\/-(())
} else
-\/(FileError.NotFound(file.toString))
}
}
2016-04-05 16:24:39 +02:00
val urls =
artifact.url +: {
checksums
.intersect(artifact.checksumUrls.keySet)
.toSeq
.map(artifact.checksumUrls)
}
val tasks =
2016-04-05 16:24:39 +02:00
for (url <- urls) yield {
2016-05-06 13:53:55 +02:00
val file = localFile(url, cache, artifact.authentication.map(_.user))
val res =
if (url.startsWith("file:/")) {
2016-01-11 21:20:55 +01:00
// for debug purposes, flaky with URL-encoded chars anyway
// def filtered(s: String) =
// s.stripPrefix("file:/").stripPrefix("//").stripSuffix("/")
// assert(
// filtered(url) == filtered(file.toURI.toString),
// s"URL: ${filtered(url)}, file: ${filtered(file.toURI.toString)}"
// )
2015-12-30 01:34:33 +01:00
checkFileExists(file, url)
} else {
def update = shouldDownload(file, url).flatMap {
case true =>
remoteKeepErrors(file, url)
case false =>
EitherT(Task.now[FileError \/ Unit](().right))
}
val cachePolicy0 = cachePolicy match {
case CachePolicy.UpdateChanging if !artifact.changing =>
CachePolicy.FetchMissing
case CachePolicy.LocalUpdateChanging if !artifact.changing =>
CachePolicy.LocalOnly
case other =>
other
}
cachePolicy0 match {
case CachePolicy.LocalOnly =>
checkFileExists(file, url)
case CachePolicy.LocalUpdateChanging | CachePolicy.LocalUpdate =>
checkFileExists(file, url, log = false).flatMap { _ =>
update
}
case CachePolicy.UpdateChanging | CachePolicy.Update =>
update
case CachePolicy.FetchMissing =>
checkFileExists(file, url) orElse remoteKeepErrors(file, url)
case CachePolicy.ForceDownload =>
remoteKeepErrors(file, url)
}
}
2015-12-30 01:34:33 +01:00
res.run.map((file, url) -> _)
}
Nondeterminism[Task].gather(tasks)
}
def parseChecksum(content: String): Option[BigInteger] = {
val lines = content
.linesIterator
.toVector
parseChecksumLine(lines) orElse parseChecksumAlternative(lines)
}
// matches md5 or sha1
private val checksumPattern = Pattern.compile("^[0-9a-f]{32}([0-9a-f]{8})?")
private def findChecksum(elems: Seq[String]): Option[BigInteger] =
elems.collectFirst {
case rawSum if checksumPattern.matcher(rawSum).matches() =>
new BigInteger(rawSum, 16)
}
private def parseChecksumLine(lines: Seq[String]): Option[BigInteger] =
findChecksum(lines.map(_.toLowerCase.replaceAll("\\s", "")))
private def parseChecksumAlternative(lines: Seq[String]): Option[BigInteger] =
findChecksum(lines.flatMap(_.toLowerCase.split("\\s+")))
def validateChecksum(
artifact: Artifact,
2015-12-30 01:34:41 +01:00
sumType: String,
cache: File,
2015-12-30 01:34:41 +01:00
pool: ExecutorService
2015-12-30 01:34:32 +01:00
): EitherT[Task, FileError, Unit] = {
2015-12-30 01:34:41 +01:00
implicit val pool0 = pool
2016-05-06 13:53:55 +02:00
val localFile0 = localFile(artifact.url, cache, artifact.authentication.map(_.user))
2015-12-30 01:34:32 +01:00
EitherT {
2016-04-05 16:24:39 +02:00
artifact.checksumUrls.get(sumType) match {
case Some(sumUrl) =>
2016-05-06 13:53:55 +02:00
val sumFile = localFile(sumUrl, cache, artifact.authentication.map(_.user))
2016-04-05 16:24:39 +02:00
2015-12-30 01:34:32 +01:00
Task {
val sumOpt = parseChecksum(
2016-04-05 16:24:39 +02:00
new String(NioFiles.readAllBytes(sumFile.toPath), "UTF-8")
)
sumOpt match {
case None =>
2016-04-05 16:24:39 +02:00
FileError.ChecksumFormatError(sumType, sumFile.getPath).left
case Some(sum) =>
val md = MessageDigest.getInstance(sumType)
2016-04-05 16:24:39 +02:00
val is = new FileInputStream(localFile0)
try withContent(is, md.update(_, 0, _))
finally is.close()
val digest = md.digest()
val calculatedSum = new BigInteger(1, digest)
if (sum == calculatedSum)
().right
else
FileError.WrongChecksum(
sumType,
calculatedSum.toString(16),
sum.toString(16),
2016-04-05 16:24:39 +02:00
localFile0.getPath,
sumFile.getPath
).left
2015-12-30 01:34:32 +01:00
}
}
2015-12-30 01:34:32 +01:00
case None =>
2016-04-05 16:24:39 +02:00
Task.now(FileError.ChecksumNotFound(sumType, localFile0.getPath).left)
2015-12-30 01:34:32 +01:00
}
2015-06-25 01:18:57 +02:00
}
}
def file(
artifact: Artifact,
cache: File = default,
2016-01-03 16:38:29 +01:00
cachePolicy: CachePolicy = CachePolicy.FetchMissing,
checksums: Seq[Option[String]] = defaultChecksums,
2015-12-30 01:34:41 +01:00
logger: Option[Logger] = None,
2016-05-31 15:18:29 +02:00
pool: ExecutorService = defaultPool,
2016-05-31 15:18:33 +02:00
ttl: Option[Duration] = defaultTtl
2015-12-30 01:34:32 +01:00
): EitherT[Task, FileError, File] = {
2015-12-30 01:34:41 +01:00
implicit val pool0 = pool
2015-12-30 01:34:32 +01:00
val checksums0 = if (checksums.isEmpty) Seq(None) else checksums
val res = EitherT {
download(
artifact,
2015-12-30 01:34:41 +01:00
cache,
2015-12-30 01:34:32 +01:00
checksums = checksums0.collect { case Some(c) => c }.toSet,
2015-12-30 01:34:41 +01:00
cachePolicy,
pool,
2016-05-31 15:18:29 +02:00
logger = logger,
ttl = ttl
2015-12-30 01:34:32 +01:00
).map { results =>
val checksum = checksums0.find {
case None => true
case Some(c) =>
artifact.checksumUrls.get(c).exists { cUrl =>
results.exists { case ((_, u), b) =>
u == cUrl && b.isRight
}
}
}
2015-12-30 01:34:32 +01:00
val ((f, _), res) = results.head
res.flatMap { _ =>
checksum match {
case None =>
// FIXME All the checksums should be in the error, possibly with their URLs
// from artifact.checksumUrls
-\/(FileError.ChecksumNotFound(checksums0.last.get, ""))
case Some(c) => \/-((f, c))
}
}
}
}
2015-12-30 01:34:32 +01:00
res.flatMap {
case (f, None) => EitherT(Task.now[FileError \/ File](\/-(f)))
case (f, Some(c)) =>
2015-12-30 01:34:41 +01:00
validateChecksum(artifact, c, cache, pool).map(_ => f)
2015-12-30 01:34:32 +01:00
}
}
2015-11-29 20:21:45 +01:00
def fetch(
cache: File = default,
2016-01-03 16:38:29 +01:00
cachePolicy: CachePolicy = CachePolicy.FetchMissing,
checksums: Seq[Option[String]] = defaultChecksums,
2015-12-30 01:34:41 +01:00
logger: Option[Logger] = None,
2016-05-31 15:18:29 +02:00
pool: ExecutorService = defaultPool,
2016-05-31 15:18:33 +02:00
ttl: Option[Duration] = defaultTtl
2015-12-30 01:34:32 +01:00
): Fetch.Content[Task] = {
artifact =>
2015-12-30 01:34:41 +01:00
file(
artifact,
cache,
cachePolicy,
checksums = checksums,
logger = logger,
2016-05-31 15:18:29 +02:00
pool = pool,
ttl = ttl
2016-07-24 14:56:45 +02:00
).leftMap(_.describe).flatMap { f =>
val res = if (!f.isDirectory && f.exists()) {
Try(new String(NioFiles.readAllBytes(f.toPath), "UTF-8")) match {
case scala.util.Success(content) =>
Right(content)
case scala.util.Failure(e) =>
Left(s"Could not read (file:${f.getCanonicalPath}): ${e.getMessage}")
}
} else Left(s"Could not read (file:${f.getCanonicalPath}) (isFile:${!f.isDirectory}) (exists:${f.exists()})")
EitherT.fromEither(Task.now[Either[String, String]](res))
}
}
private lazy val ivy2HomeUri = {
// a bit touchy on Windows... - don't try to manually write down the URI with s"file://..."
val str = new File(sys.props("user.home") + "/.ivy2/").toURI.toString
if (str.endsWith("/"))
str
else
str + "/"
}
2016-07-03 17:21:17 +02:00
lazy val ivy2Local = IvyRepository.fromPattern(
(ivy2HomeUri + "local/") +: coursier.ivy.Pattern.default,
dropInfoAttributes = true
)
2016-07-03 17:21:17 +02:00
lazy val ivy2Cache = IvyRepository.parse(
ivy2HomeUri + "cache/" +
"(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]",
metadataPatternOpt = Some(
ivy2HomeUri + "cache/" +
"(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[organisation]/[module]/[type]-[revision](-[classifier]).[ext]"
),
withChecksums = false,
withSignatures = false,
dropInfoAttributes = true
2016-07-03 17:21:17 +02:00
).getOrElse(
throw new Exception("Cannot happen")
)
lazy val default = new File(
sys.env.getOrElse(
"COURSIER_CACHE",
sys.props("user.home") + "/.coursier/cache/v1"
)
).getAbsoluteFile
val defaultConcurrentDownloadCount = 6
2015-06-25 01:18:57 +02:00
2015-12-30 01:34:41 +01:00
lazy val defaultPool =
Executors.newFixedThreadPool(defaultConcurrentDownloadCount, Strategy.DefaultDaemonThreadFactory)
2016-05-31 15:18:33 +02:00
lazy val defaultTtl: Option[Duration] = {
2016-05-31 15:18:29 +02:00
def fromString(s: String) =
2016-05-31 15:18:33 +02:00
Try(Duration(s)).toOption
2016-05-31 15:18:29 +02:00
val fromEnv = sys.env.get("COURSIER_TTL").flatMap(fromString)
def fromProps = sys.props.get("coursier.ttl").flatMap(fromString)
2016-07-25 00:19:01 +02:00
def default = 24.hours
2016-05-31 15:18:29 +02:00
fromEnv
.orElse(fromProps)
.orElse(Some(default))
}
2015-12-30 01:34:41 +01:00
2015-12-30 01:34:32 +01:00
private val urlLocks = new ConcurrentHashMap[String, Object]
trait Logger {
2015-11-29 20:22:25 +01:00
def foundLocally(url: String, f: File): Unit = {}
2015-12-30 01:34:32 +01:00
def downloadingArtifact(url: String, file: File): Unit = {}
@deprecated("Use / override the variant with 3 arguments instead")
2015-11-29 20:22:25 +01:00
def downloadLength(url: String, length: Long): Unit = {}
def downloadLength(url: String, totalLength: Long, alreadyDownloaded: Long): Unit = {
downloadLength(url, totalLength)
}
2015-11-29 20:22:25 +01:00
def downloadProgress(url: String, downloaded: Long): Unit = {}
2015-11-29 20:22:25 +01:00
def downloadedArtifact(url: String, success: Boolean): Unit = {}
def checkingUpdates(url: String, currentTimeOpt: Option[Long]): Unit = {}
def checkingUpdatesResult(url: String, currentTimeOpt: Option[Long], remoteTimeOpt: Option[Long]): Unit = {}
}
2015-06-25 01:18:57 +02:00
var bufferSize = 1024*1024
def readFullySync(is: InputStream) = {
val buffer = new ByteArrayOutputStream()
val data = Array.ofDim[Byte](16384)
var nRead = is.read(data, 0, data.length)
while (nRead != -1) {
buffer.write(data, 0, nRead)
nRead = is.read(data, 0, data.length)
}
buffer.flush()
buffer.toByteArray
}
def withContent(is: InputStream, f: (Array[Byte], Int) => Unit): Unit = {
val data = Array.ofDim[Byte](16384)
var nRead = is.read(data, 0, data.length)
while (nRead != -1) {
f(data, nRead)
nRead = is.read(data, 0, data.length)
}
}
}