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[string] 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 Version[] getVersions(string package_id) 36 { 37 import std.algorithm.sorting : sort; 38 auto md = getMetadata(package_id); 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 void fetchPackage(NativePath path, string packageId, Dependency dep, bool pre_release) 51 { 52 import std.format : format; 53 auto md = getMetadata(packageId); 54 Json best = getBestPackage(md, packageId, dep, pre_release); 55 if (best.type == Json.Type.null_) 56 return; 57 auto vers = best["version"].get!string; 58 auto url = m_mavenUrl~NativePath("%s/%s/%s-%s.zip".format(packageId, vers, packageId, vers)); 59 60 try { 61 retryDownload(url, path, 3, httpTimeout); 62 return; 63 } 64 catch(HTTPStatusException e) { 65 if (e.status == 404) throw e; 66 else logDebug("Failed to download package %s from %s", packageId, url); 67 } 68 catch(Exception e) { 69 logDebug("Failed to download package %s from %s", packageId, url); 70 } 71 throw new Exception("Failed to download package %s from %s".format(packageId, url)); 72 } 73 74 Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) 75 { 76 auto md = getMetadata(packageId); 77 return getBestPackage(md, packageId, dep, pre_release); 78 } 79 80 private Json getMetadata(string packageId) 81 { 82 import dub.internal.undead.xml; 83 84 auto now = Clock.currTime(UTC()); 85 if (auto pentry = packageId in m_metadataCache) { 86 if (pentry.cacheTime + m_maxCacheTime > now) 87 return pentry.data; 88 m_metadataCache.remove(packageId); 89 } 90 91 auto url = m_mavenUrl~NativePath(packageId~"/maven-metadata.xml"); 92 93 logDebug("Downloading maven metadata for %s", packageId); 94 string xmlData; 95 96 try 97 xmlData = cast(string)retryDownload(url, 3, httpTimeout); 98 catch(HTTPStatusException e) { 99 if (e.status == 404) { 100 logDebug("Maven metadata %s not found at %s (404): %s", packageId, description, e.msg); 101 return Json(null); 102 } 103 else throw e; 104 } 105 106 auto json = Json(["name": Json(packageId), "versions": Json.emptyArray]); 107 auto xml = new DocumentParser(xmlData); 108 109 xml.onStartTag["versions"] = (ElementParser xml) { 110 xml.onEndTag["version"] = (in Element e) { 111 json["versions"] ~= serializeToJson(["name": packageId, "version": e.text]); 112 }; 113 xml.parse(); 114 }; 115 xml.parse(); 116 117 m_metadataCache[packageId] = CacheEntry(json, now); 118 return json; 119 } 120 121 SearchResult[] searchPackages(string query) 122 { 123 // Only exact search is supported 124 // This enables retrieval of dub packages on dub run 125 auto md = getMetadata(query); 126 if (md.type == Json.Type.null_) 127 return null; 128 auto json = getBestPackage(md, query, Dependency.any, true); 129 return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)]; 130 } 131 }