diff --git a/modules/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala b/modules/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala index 7eb896dc1..2dd3299c2 100644 --- a/modules/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala +++ b/modules/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala @@ -38,7 +38,7 @@ final case class ResolutionParams( .toMap Seq( - InMemoryRepository(map) + TemporaryInMemoryRepository(map, cache) ) } diff --git a/modules/lm-coursier/src/main/scala/lmcoursier/internal/TemporaryInMemoryRepository.scala b/modules/lm-coursier/src/main/scala/lmcoursier/internal/TemporaryInMemoryRepository.scala new file mode 100644 index 000000000..7bf2721bc --- /dev/null +++ b/modules/lm-coursier/src/main/scala/lmcoursier/internal/TemporaryInMemoryRepository.scala @@ -0,0 +1,216 @@ +package lmcoursier.internal + +import java.io.{File, FileNotFoundException, IOException} +import java.net.{HttpURLConnection, URL, URLConnection} + +import coursier.cache.{CacheUrl, FileCache} +import coursier.core._ +import coursier.util.{EitherT, Monad} + +import scala.util.Try + +object TemporaryInMemoryRepository { + + def closeConn(conn: URLConnection): Unit = { + Try(conn.getInputStream).toOption.filter(_ != null).foreach(_.close()) + conn match { + case conn0: HttpURLConnection => + Try(conn0.getErrorStream).toOption.filter(_ != null).foreach(_.close()) + conn0.disconnect() + case _ => + } + } + + def exists( + url: URL, + localArtifactsShouldBeCached: Boolean + ): Boolean = + exists(url, localArtifactsShouldBeCached, None) + + def exists( + url: URL, + localArtifactsShouldBeCached: Boolean, + cacheOpt: Option[FileCache[Nothing]] + ): Boolean = { + + // Sometimes HEAD attempts fail even though standard GETs are fine. + // E.g. https://github.com/NetLogo/NetLogo/releases/download/5.3.1/NetLogo.jar + // returning 403s. Hence the second attempt below. + + val protocolSpecificAttemptOpt = { + + def ifFile: Option[Boolean] = { + if (localArtifactsShouldBeCached && !new File(url.toURI).exists()) { + val cachePath = coursier.cache.CacheDefaults.location + // 'file' here stands for the protocol (e.g. it's https instead for https:// URLs) + Some(new File(cachePath, s"file/${url.getPath}").exists()) + } else { + Some(new File(url.toURI).exists()) // FIXME Escaping / de-escaping needed here? + } + } + + def ifHttp: Option[Boolean] = { + // HEAD request attempt, adapted from http://stackoverflow.com/questions/22541629/android-how-can-i-make-an-http-head-request/22545275#22545275 + + var conn: URLConnection = null + try { + conn = CacheUrl.urlConnection( + url.toString, + None, + followHttpToHttpsRedirections = cacheOpt.fold(false)(_.followHttpToHttpsRedirections), + followHttpsToHttpRedirections = cacheOpt.fold(false)(_.followHttpsToHttpRedirections), + sslSocketFactoryOpt = cacheOpt.flatMap(_.sslSocketFactoryOpt), + hostnameVerifierOpt = cacheOpt.flatMap(_.hostnameVerifierOpt), + method = "HEAD", + maxRedirectionsOpt = cacheOpt.flatMap(_.maxRedirections) + ) + // Even though the finally clause handles this too, this has to be run here, so that we return Some(true) + // iff this doesn't throw. + conn.getInputStream.close() + Some(true) + } + catch { + case _: FileNotFoundException => Some(false) + case _: IOException => None // error other than not found + } + finally { + if (conn != null) + closeConn(conn) + } + } + + url.getProtocol match { + case "file" => ifFile + case "http" | "https" => ifHttp + case _ => None + } + } + + def genericAttempt: Boolean = { + var conn: URLConnection = null + try { + conn = url.openConnection() + // NOT setting request type to HEAD here. + conn.getInputStream.close() + true + } + catch { + case _: IOException => false + } + finally { + if (conn != null) + closeConn(conn) + } + } + + protocolSpecificAttemptOpt + .getOrElse(genericAttempt) + } + + def apply( + fallbacks: Map[(Module, String), (URL, Boolean)] + ): TemporaryInMemoryRepository = + new TemporaryInMemoryRepository(fallbacks, localArtifactsShouldBeCached = false, None) + + def apply( + fallbacks: Map[(Module, String), (URL, Boolean)], + localArtifactsShouldBeCached: Boolean + ): TemporaryInMemoryRepository = + new TemporaryInMemoryRepository(fallbacks, localArtifactsShouldBeCached, None) + + def apply[F[_]]( + fallbacks: Map[(Module, String), (URL, Boolean)], + cache: FileCache[F] + ): TemporaryInMemoryRepository = + new TemporaryInMemoryRepository( + fallbacks, + localArtifactsShouldBeCached = cache.localArtifactsShouldBeCached, + Some(cache.asInstanceOf[FileCache[Nothing]]) + ) + +} + +final class TemporaryInMemoryRepository private( + val fallbacks: Map[(Module, String), (URL, Boolean)], + val localArtifactsShouldBeCached: Boolean, + val cacheOpt: Option[FileCache[Nothing]] +) extends Repository { + + def find[F[_]]( + module: Module, + version: String, + fetch: Repository.Fetch[F] + )(implicit + F: Monad[F] + ): EitherT[F, String, (Artifact.Source, Project)] = { + + def res = fallbacks + .get((module, version)) + .fold[Either[String, (Artifact.Source, Project)]](Left("No fallback URL found")) { + case (url, _) => + + println(s"($module, $version) -> $url") + + val urlStr = url.toExternalForm + val idx = urlStr.lastIndexOf('/') + + if (idx < 0 || urlStr.endsWith("/")) + Left(s"$url doesn't point to a file") + else { + val (dirUrlStr, fileName) = urlStr.splitAt(idx + 1) + + if (TemporaryInMemoryRepository.exists(url, localArtifactsShouldBeCached, cacheOpt)) { + println("returning proj") + val proj = Project( + module, + version, + Nil, + Map.empty, + None, + Nil, + Nil, + Nil, + None, + None, + None, + relocated = false, + None, + Nil, + Info.empty + ) + + Right((this, proj)) + } else + Left(s"$fileName not found under $dirUrlStr") + } + } + + // EitherT(F.bind(F.point(()))(_ => F.point(res))) + EitherT(F.map(F.point(()))(_ => res)) + } + + def artifacts( + dependency: Dependency, + project: Project, + overrideClassifiers: Option[Seq[Classifier]] + ): Seq[(Publication, Artifact)] = { + println(s"artifacts($dependency)") + fallbacks + .get(dependency.moduleVersion) + .toSeq + .map { + case (url, changing) => + println(s"$url, $changing") + val url0 = url.toString + val ext = url0.substring(url0.lastIndexOf('.') + 1) + val pub = Publication( + dependency.module.name.value, // ??? + Type(ext), + Extension(ext), + Classifier.empty + ) + (pub, Artifact(url0, Map.empty, Map.empty, changing, optional = false, None)) + } + } + +}