Changes in files API, checksum verifying

This commit is contained in:
Alexandre Archambault 2015-07-05 14:41:38 +01:00
parent 8d98eca154
commit 38f6eda609
3 changed files with 192 additions and 72 deletions

View File

@ -256,7 +256,7 @@ case class Coursier(
files0 files0
} }
val tasks = artifacts.map(artifact => files.file(artifact, cachePolicy).run.map(artifact.->)) val tasks = artifacts.map(artifact => files.file(artifact).run.map(artifact.->))
def printTask = Task{ def printTask = Task{
if (verbose0 >= 0 && artifacts.nonEmpty) if (verbose0 >= 0 && artifacts.nonEmpty)
println(s"Found ${artifacts.length} artifacts") println(s"Found ${artifacts.length} artifacts")

View File

@ -76,8 +76,8 @@ object Repository {
implicit class ArtifactExtensions(val underlying: Artifact) extends AnyVal { implicit class ArtifactExtensions(val underlying: Artifact) extends AnyVal {
def withDefaultChecksums: Artifact = def withDefaultChecksums: Artifact =
underlying.copy(checksumUrls = underlying.checksumUrls ++ Seq( underlying.copy(checksumUrls = underlying.checksumUrls ++ Seq(
"md5" -> (underlying.url + ".md5"), "MD5" -> (underlying.url + ".md5"),
"sha1" -> (underlying.url + ".sha1") "SHA-1" -> (underlying.url + ".sha1")
)) ))
def withDefaultSignature: Artifact = def withDefaultSignature: Artifact =
underlying.copy(extra = underlying.extra ++ Seq( underlying.copy(extra = underlying.extra ++ Seq(

View File

@ -1,10 +1,11 @@
package coursier package coursier
import java.net.{ URI, URL } import java.net.URL
import java.security.MessageDigest
import java.util.concurrent.{ Executors, ExecutorService } import java.util.concurrent.{ Executors, ExecutorService }
import scala.annotation.tailrec import scala.annotation.tailrec
import scalaz.{ -\/, \/-, \/, EitherT } import scalaz._
import scalaz.concurrent.{ Task, Strategy } import scalaz.concurrent.{ Task, Strategy }
import java.io._ import java.io._
@ -19,85 +20,182 @@ case class Files(
lazy val defaultPool = lazy val defaultPool =
Executors.newFixedThreadPool(concurrentDownloadCount, Strategy.DefaultDaemonThreadFactory) Executors.newFixedThreadPool(concurrentDownloadCount, Strategy.DefaultDaemonThreadFactory)
def file( def withLocal(artifact: Artifact): Artifact = {
val isLocal =
artifact.url.startsWith("file://") &&
artifact.checksumUrls.values.forall(_.startsWith("file://"))
def local(url: String) =
if (url.startsWith("file://"))
url.stripPrefix("file://")
else
cache.find{case (base, _) => url.startsWith(base)} match {
case None => ???
case Some((base, cacheDir)) =>
cacheDir + "/" + url.stripPrefix(base)
}
if (artifact.extra.contains("local") || isLocal)
artifact
else
artifact.copy(extra = artifact.extra + ("local" ->
artifact.copy(
url = local(artifact.url),
checksumUrls = artifact.checksumUrls
.mapValues(local)
.toVector
.toMap,
extra = Map.empty
)
))
}
def download(
artifact: Artifact, artifact: Artifact,
cachePolicy: CachePolicy withChecksums: Boolean = true
)(implicit
cachePolicy: CachePolicy,
pool: ExecutorService = defaultPool
): Task[Seq[((File, String), FileError \/ Unit)]] = {
val artifact0 = withLocal(artifact)
.extra
.getOrElse("local", artifact)
val pairs =
Seq(artifact0.url -> artifact.url) ++ {
if (withChecksums)
(artifact0.checksumUrls.keySet intersect artifact.checksumUrls.keySet)
.toList
.map(sumType => artifact0.checksumUrls(sumType) -> artifact.checksumUrls(sumType))
else
Nil
}
def locally(file: File) =
Task {
if (file.exists()) {
logger.foreach(_.foundLocally(file))
\/-(file)
} else
-\/(FileError.NotFound(file.toString): FileError)
}
// FIXME Things can go wrong here and are not properly handled,
// e.g. what if the connection gets closed during the transfer?
// (partial file on disk?)
def remote(file: File, url: String) =
Task {
try {
file.getParentFile.mkdirs()
logger.foreach(_.downloadingArtifact(url))
val url0 = new URL(url)
val b = Array.fill[Byte](Files.bufferSize)(0)
val in = new BufferedInputStream(url0.openStream(), Files.bufferSize)
try {
val out = new FileOutputStream(file)
try {
@tailrec
def helper(): Unit = {
val read = in.read(b)
if (read >= 0) {
out.write(b, 0, read)
helper()
}
}
helper()
} finally out.close()
} finally in.close()
logger.foreach(_.downloadedArtifact(url, success = true))
\/-(file)
}
catch { case e: Exception =>
logger.foreach(_.downloadedArtifact(url, success = false))
-\/(FileError.DownloadError(e.getMessage))
}
}
val tasks =
for ((f, url) <- pairs if url != ("file://" + f)) yield {
val file = new File(f)
cachePolicy(locally(file))(remote(file, url))
.map(e => (file, url) -> e.map(_ => ()))
}
Nondeterminism[Task].gather(tasks)
}
def validateChecksum(
artifact: Artifact,
sumType: String
)(implicit )(implicit
pool: ExecutorService = defaultPool pool: ExecutorService = defaultPool
): EitherT[Task, String, File] = { ): Task[FileError \/ Unit] = {
val artifact0 = withLocal(artifact)
.extra
.getOrElse("local", artifact)
if (artifact.url.startsWith("file:///")) {
val f = new File(new URI(artifact.url) .getPath)
EitherT(Task.now(
if (f.exists()) {
logger.foreach(_.foundLocally(f))
\/-(f)
} else -\/("Not found")
))
} else {
cache.find{case (base, _) => artifact.url.startsWith(base)} match {
case None => ???
case Some((base, cacheDir)) =>
val file = new File(cacheDir, artifact.url.stripPrefix(base))
def locally = { artifact0.checksumUrls.get(sumType) match {
Task { case Some(sumFile) =>
if (file.exists()) { Task {
logger.foreach(_.foundLocally(file)) val sum = scala.io.Source.fromFile(sumFile)
\/-(file) .getLines()
} .toStream
else -\/("Not found in cache") .headOption
} .mkString
} .takeWhile(!_.isSpaceChar)
def remote = { val md = MessageDigest.getInstance(sumType)
// FIXME A lot of things can go wrong here and are not properly handled: val is = new FileInputStream(new File(artifact0.url))
// - checksums should be validated try Files.withContent(is, md.update(_, 0, _))
// - what if the connection gets closed during the transfer (partial file on disk)? finally is.close()
// - what if someone is trying to write this file at the same time? (no locking of any kind yet)
// - ...
Task { val digest = md.digest()
try { val calculatedSum = f"${BigInt(1, digest)}%040x"
file.getParentFile.mkdirs()
logger.foreach(_.downloadingArtifact(artifact.url)) if (sum == calculatedSum)
\/-(())
else
-\/(FileError.WrongChecksum(sumType, calculatedSum, sum, artifact0.url, sumFile))
}
val url = new URL(artifact.url) case None =>
val b = Array.fill[Byte](Files.bufferSize)(0) Task.now(-\/(FileError.ChecksumNoFound(sumType, artifact0.url)))
val in = new BufferedInputStream(url.openStream(), Files.bufferSize)
try {
val out = new FileOutputStream(file)
try {
@tailrec
def helper(): Unit = {
val read = in.read(b)
if (read >= 0) {
out.write(b, 0, read)
helper()
}
}
helper()
} finally out.close()
} finally in.close()
logger.foreach(_.downloadedArtifact(artifact.url, success = true))
\/-(file)
}
catch { case e: Exception =>
logger.foreach(_.downloadedArtifact(artifact.url, success = false))
-\/(e.getMessage)
}
}
}
EitherT(cachePolicy(locally)(remote))
}
} }
} }
def file(
artifact: Artifact,
checksum: Option[String] = Some("SHA-1")
)(implicit
cachePolicy: CachePolicy,
pool: ExecutorService = defaultPool
): EitherT[Task, FileError, File] =
EitherT{
val res =
download(artifact)
.map(results =>
results.head._2.map(_ => results.head._1._1)
)
checksum.fold(res) { sumType =>
res
.flatMap{
case err @ -\/(_) => Task.now(err)
case \/-(f) =>
validateChecksum(artifact, sumType)
.map(_.map(_ => f))
}
}
}
} }
object Files { object Files {
@ -139,4 +237,26 @@ object Files {
} .leftMap(_.getMessage) } .leftMap(_.getMessage)
} }
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)
}
}
}
sealed trait FileError
object FileError {
case class DownloadError(message: String) extends FileError
case class NotFound(file: String) extends FileError
case class Locked(file: String) extends FileError
case class ChecksumNoFound(sumType: String, file: String) extends FileError
case class WrongChecksum(sumType: String, got: String, expected: String, file: String, sumFile: String) extends FileError
} }