1 module dub.packagesuppliers.maven; 2 3 import dub.packagesuppliers.packagesupplier; 4 5 /** 6 Maven repository based package supplier. 7 8 This package supplier connects to a maven repository 9 to search for available packages. 10 */ 11 class MavenRegistryPackageSupplier : PackageSupplier { 12 import dub.internal.utils : retryDownload, HTTPStatusException; 13 import dub.internal.vibecompat.data.json : serializeToJson; 14 import dub.internal.vibecompat.inet.path : InetPath; 15 import dub.internal.vibecompat.inet.url : URL; 16 import dub.internal.logging; 17 18 import std.datetime : Clock, Duration, hours, SysTime, UTC; 19 20 private { 21 enum httpTimeout = 16; 22 URL m_mavenUrl; 23 struct CacheEntry { Json data; SysTime cacheTime; } 24 CacheEntry[PackageName] m_metadataCache; 25 Duration m_maxCacheTime; 26 } 27 28 this(URL mavenUrl) 29 { 30 m_mavenUrl = mavenUrl; 31 m_maxCacheTime = 24.hours(); 32 } 33 34 override @property string description() { return "maven repository at "~m_mavenUrl.toString(); } 35 36 override Version[] getVersions(in PackageName name) 37 { 38 import std.algorithm.sorting : sort; 39 auto md = getMetadata(name.main); 40 if (md.type == Json.Type.null_) 41 return null; 42 Version[] ret; 43 foreach (json; md["versions"]) { 44 auto cur = Version(json["version"].get!string); 45 ret ~= cur; 46 } 47 ret.sort(); 48 return ret; 49 } 50 51 override ubyte[] fetchPackage(in PackageName name, 52 in VersionRange dep, bool pre_release) 53 { 54 import std.format : format; 55 auto md = getMetadata(name.main); 56 Json best = getBestPackage(md, name.main, dep, pre_release); 57 if (best.type == Json.Type.null_) 58 return null; 59 auto vers = best["version"].get!string; 60 auto url = m_mavenUrl ~ InetPath( 61 "%s/%s/%s-%s.zip".format(name.main, vers, name.main, vers)); 62 63 try { 64 return retryDownload(url, 3, httpTimeout); 65 } 66 catch(HTTPStatusException e) { 67 if (e.status == 404) throw e; 68 else logDebug("Failed to download package %s from %s", name.main, url); 69 } 70 catch(Exception e) { 71 logDebug("Failed to download package %s from %s", name.main, url); 72 } 73 throw new Exception("Failed to download package %s from %s".format(name.main, url)); 74 } 75 76 override Json fetchPackageRecipe(in PackageName name, in VersionRange dep, 77 bool pre_release) 78 { 79 auto md = getMetadata(name); 80 return getBestPackage(md, name, dep, pre_release); 81 } 82 83 private Json getMetadata(in PackageName name) 84 { 85 import dub.internal.undead.xml; 86 87 auto now = Clock.currTime(UTC()); 88 if (auto pentry = name.main in m_metadataCache) { 89 if (pentry.cacheTime + m_maxCacheTime > now) 90 return pentry.data; 91 m_metadataCache.remove(name.main); 92 } 93 94 auto url = m_mavenUrl ~ InetPath(name.main.toString() ~ "/maven-metadata.xml"); 95 96 logDebug("Downloading maven metadata for %s", name.main); 97 string xmlData; 98 99 try 100 xmlData = cast(string)retryDownload(url, 3, httpTimeout); 101 catch(HTTPStatusException e) { 102 if (e.status == 404) { 103 logDebug("Maven metadata %s not found at %s (404): %s", name.main, description, e.msg); 104 return Json(null); 105 } 106 else throw e; 107 } 108 109 auto json = Json([ 110 "name": Json(name.main.toString()), 111 "versions": Json.emptyArray 112 ]); 113 auto xml = new DocumentParser(xmlData); 114 115 xml.onStartTag["versions"] = (ElementParser xml) { 116 xml.onEndTag["version"] = (in Element e) { 117 json["versions"] ~= serializeToJson([ 118 "name": name.main.toString(), 119 "version": e.text, 120 ]); 121 }; 122 xml.parse(); 123 }; 124 xml.parse(); 125 126 m_metadataCache[name.main] = CacheEntry(json, now); 127 return json; 128 } 129 130 SearchResult[] searchPackages(string query) 131 { 132 // Only exact search is supported 133 // This enables retrieval of dub packages on dub run 134 auto md = getMetadata(PackageName(query)); 135 if (md.type == Json.Type.null_) 136 return null; 137 auto json = getBestPackage(md, PackageName(query), VersionRange.Any, true); 138 return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)]; 139 } 140 }