1 module dub.packagesuppliers.registry; 2 3 import dub.packagesuppliers.packagesupplier; 4 5 package enum PackagesPath = "packages"; 6 7 /** 8 Online registry based package supplier. 9 10 This package supplier connects to an online registry (e.g. 11 $(LINK https://code.dlang.org/)) to search for available packages. 12 */ 13 class RegistryPackageSupplier : PackageSupplier { 14 import dub.internal.utils : download, retryDownload, HTTPStatusException; 15 import dub.internal.vibecompat.core.log; 16 import dub.internal.vibecompat.data.json : parseJson, parseJsonString, serializeToJson; 17 import dub.internal.vibecompat.inet.url : URL; 18 19 import std.uri : encodeComponent; 20 import std.datetime : Clock, Duration, hours, SysTime, UTC; 21 private { 22 URL m_registryUrl; 23 struct CacheEntry { Json data; SysTime cacheTime; } 24 CacheEntry[string] m_metadataCache; 25 Duration m_maxCacheTime; 26 } 27 28 this(URL registry) 29 { 30 m_registryUrl = registry; 31 m_maxCacheTime = 24.hours(); 32 } 33 34 override @property string description() { return "registry at "~m_registryUrl.toString(); } 35 36 Version[] getVersions(string package_id) 37 { 38 import std.algorithm.sorting : sort; 39 auto md = getMetadata(package_id); 40 if (md.type == Json.Type.null_) 41 return null; 42 Version[] ret; 43 foreach (json; md["versions"]) { 44 auto cur = Version(cast(string)json["version"]); 45 ret ~= cur; 46 } 47 ret.sort(); 48 return ret; 49 } 50 51 void fetchPackage(NativePath path, string packageId, Dependency dep, bool pre_release) 52 { 53 import std.array : replace; 54 import std.format : format; 55 auto md = getMetadata(packageId); 56 Json best = getBestPackage(md, packageId, dep, pre_release); 57 if (best.type == Json.Type.null_) 58 return; 59 auto vers = best["version"].get!string; 60 auto url = m_registryUrl ~ NativePath(PackagesPath~"/"~packageId~"/"~vers~".zip"); 61 try { 62 retryDownload(url, path); 63 return; 64 } 65 catch(HTTPStatusException e) { 66 if (e.status == 404) throw e; 67 else logDebug("Failed to download package %s from %s", packageId, url); 68 } 69 catch(Exception e) { 70 logDebug("Failed to download package %s from %s", packageId, url); 71 } 72 throw new Exception("Failed to download package %s from %s".format(packageId, url)); 73 } 74 75 Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) 76 { 77 auto md = getMetadata(packageId); 78 return getBestPackage(md, packageId, dep, pre_release); 79 } 80 81 private Json getMetadata(string packageId) 82 { 83 auto now = Clock.currTime(UTC()); 84 if (auto pentry = packageId in m_metadataCache) { 85 if (pentry.cacheTime + m_maxCacheTime > now) 86 return pentry.data; 87 m_metadataCache.remove(packageId); 88 } 89 90 auto url = m_registryUrl ~ NativePath("api/packages/infos"); 91 92 url.queryString = "packages=" ~ 93 encodeComponent(`["` ~ packageId ~ `"]`) ~ "&include_dependencies=true&minimize=true"; 94 95 logDebug("Downloading metadata for %s", packageId); 96 string jsonData; 97 98 jsonData = cast(string)retryDownload(url); 99 100 Json json = parseJsonString(jsonData, url.toString()); 101 foreach (pkg, info; json.get!(Json[string])) 102 { 103 logDebug("adding %s to metadata cache", pkg); 104 m_metadataCache[pkg] = CacheEntry(info, now); 105 } 106 return json[packageId]; 107 } 108 109 SearchResult[] searchPackages(string query) { 110 import std.array : array; 111 import std.algorithm.iteration : map; 112 import std.uri : encodeComponent; 113 auto url = m_registryUrl; 114 url.localURI = "/api/packages/search?q="~encodeComponent(query); 115 string data; 116 data = cast(string)retryDownload(url); 117 return data.parseJson.opt!(Json[]) 118 .map!(j => SearchResult(j["name"].opt!string, j["description"].opt!string, j["version"].opt!string)) 119 .array; 120 } 121 } 122