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