1 /** 2 Contains (remote) package supplier interface and implementations. 3 4 Copyright: © 2012-2013 Matthias Dondorff, 2012-2016 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff 7 */ 8 module dub.packagesupplier; 9 10 import dub.dependency; 11 import dub.internal.utils; 12 import dub.internal.vibecompat.core.log; 13 import dub.internal.vibecompat.core.file; 14 import dub.internal.vibecompat.data.json; 15 import dub.internal.vibecompat.inet.url; 16 17 import std.algorithm : filter, sort; 18 import std.array : array; 19 import std.conv; 20 import std.datetime; 21 import std.exception; 22 import std.file; 23 import std.string : format; 24 import std.zip; 25 26 // TODO: Could drop the "best package" behavior and let retrievePackage/ 27 // getPackageDescription take a Version instead of Dependency. But note 28 // this means that two requests to the registry are necessary to retrieve 29 // a package recipe instead of one (first get version list, then the 30 // package recipe) 31 32 /** 33 Base interface for remote package suppliers. 34 35 Provides functionality necessary to query package versions, recipes and 36 contents. 37 */ 38 interface PackageSupplier { 39 /// Represents a single package search result. 40 static struct SearchResult { string name, description, version_; } 41 42 /// Returns a human-readable representation of the package supplier. 43 @property string description(); 44 45 /** Retrieves a list of all available versions(/branches) of a package. 46 47 Throws: Throws an exception if the package name is not known, or if 48 an error occurred while retrieving the version list. 49 */ 50 Version[] getVersions(string package_id); 51 52 /** Downloads a package and stores it as a ZIP file. 53 54 Params: 55 path = Absolute path of the target ZIP file 56 package_id = Name of the package to retrieve 57 dep: Version constraint to match against 58 pre_release: If true, matches the latest pre-release version. 59 Otherwise prefers stable versions. 60 */ 61 void fetchPackage(Path path, string package_id, Dependency dep, bool pre_release); 62 63 /** Retrieves only the recipe of a particular package. 64 65 Params: 66 package_id = Name of the package of which to retrieve the recipe 67 dep: Version constraint to match against 68 pre_release: If true, matches the latest pre-release version. 69 Otherwise prefers stable versions. 70 */ 71 Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release); 72 73 /** Searches for packages matching the given search query term. 74 75 Search queries are currently a simple list of words separated by 76 white space. Results will get ordered from best match to worst. 77 */ 78 SearchResult[] searchPackages(string query); 79 } 80 81 82 /** 83 File system based package supplier. 84 85 This package supplier searches a certain directory for files with names of 86 the form "[package name]-[version].zip". 87 */ 88 class FileSystemPackageSupplier : PackageSupplier { 89 private { 90 Path m_path; 91 } 92 93 this(Path root) { m_path = root; } 94 95 override @property string description() { return "file repository at "~m_path.toNativeString(); } 96 97 Version[] getVersions(string package_id) 98 { 99 Version[] ret; 100 foreach (DirEntry d; dirEntries(m_path.toNativeString(), package_id~"*", SpanMode.shallow)) { 101 Path p = Path(d.name); 102 logDebug("Entry: %s", p); 103 enforce(to!string(p.head)[$-4..$] == ".zip"); 104 auto vers = p.head.toString()[package_id.length+1..$-4]; 105 logDebug("Version: %s", vers); 106 ret ~= Version(vers); 107 } 108 ret.sort(); 109 return ret; 110 } 111 112 void fetchPackage(Path path, string packageId, Dependency dep, bool pre_release) 113 { 114 enforce(path.absolute); 115 logInfo("Storing package '"~packageId~"', version requirements: %s", dep); 116 auto filename = bestPackageFile(packageId, dep, pre_release); 117 enforce(existsFile(filename)); 118 copyFile(filename, path); 119 } 120 121 Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) 122 { 123 auto filename = bestPackageFile(packageId, dep, pre_release); 124 return jsonFromZip(filename, "dub.json"); 125 } 126 127 SearchResult[] searchPackages(string query) 128 { 129 // TODO! 130 return null; 131 } 132 133 private Path bestPackageFile(string packageId, Dependency dep, bool pre_release) 134 { 135 Path toPath(Version ver) { 136 return m_path ~ (packageId ~ "-" ~ ver.toString() ~ ".zip"); 137 } 138 auto versions = getVersions(packageId).filter!(v => dep.matches(v)).array; 139 enforce(versions.length > 0, format("No package %s found matching %s", packageId, dep)); 140 foreach_reverse (ver; versions) { 141 if (pre_release || !ver.isPreRelease) 142 return toPath(ver); 143 } 144 return toPath(versions[$-1]); 145 } 146 } 147 148 149 /** 150 Online registry based package supplier. 151 152 This package supplier connects to an online registry (e.g. 153 $(LINK https://code.dlang.org/)) to search for available packages. 154 */ 155 class RegistryPackageSupplier : PackageSupplier { 156 private { 157 URL m_registryUrl; 158 struct CacheEntry { Json data; SysTime cacheTime; } 159 CacheEntry[string] m_metadataCache; 160 Duration m_maxCacheTime; 161 } 162 163 this(URL registry) 164 { 165 m_registryUrl = registry; 166 m_maxCacheTime = 24.hours(); 167 } 168 169 override @property string description() { return "registry at "~m_registryUrl.toString(); } 170 171 Version[] getVersions(string package_id) 172 { 173 Version[] ret; 174 Json md = getMetadata(package_id); 175 foreach (json; md["versions"]) { 176 auto cur = Version(cast(string)json["version"]); 177 ret ~= cur; 178 } 179 ret.sort(); 180 return ret; 181 } 182 183 void fetchPackage(Path path, string packageId, Dependency dep, bool pre_release) 184 { 185 import std.array : replace; 186 Json best = getBestPackage(packageId, dep, pre_release); 187 auto vers = best["version"].get!string; 188 auto url = m_registryUrl ~ Path(PackagesPath~"/"~packageId~"/"~vers~".zip"); 189 logDiagnostic("Downloading from '%s'", url); 190 download(url, path); 191 } 192 193 Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release) 194 { 195 return getBestPackage(packageId, dep, pre_release); 196 } 197 198 private Json getMetadata(string packageId) 199 { 200 auto now = Clock.currTime(UTC()); 201 if (auto pentry = packageId in m_metadataCache) { 202 if (pentry.cacheTime + m_maxCacheTime > now) 203 return pentry.data; 204 m_metadataCache.remove(packageId); 205 } 206 207 auto url = m_registryUrl ~ Path(PackagesPath ~ "/" ~ packageId ~ ".json"); 208 209 logDebug("Downloading metadata for %s", packageId); 210 logDebug("Getting from %s", url); 211 212 auto jsonData = cast(string)download(url); 213 Json json = parseJsonString(jsonData, url.toString()); 214 // strip readme data (to save size and time) 215 foreach (ref v; json["versions"]) 216 v.remove("readme"); 217 m_metadataCache[packageId] = CacheEntry(json, now); 218 return json; 219 } 220 221 SearchResult[] searchPackages(string query) { 222 import std.uri : encodeComponent; 223 auto url = m_registryUrl; 224 url.localURI = "/api/packages/search?q="~encodeComponent(query); 225 string data; 226 try 227 data = cast(string)download(url); 228 catch (Exception) 229 return null; 230 import std.algorithm : map; 231 return data.parseJson.opt!(Json[]) 232 .map!(j => SearchResult(j["name"].opt!string, j["description"].opt!string, j["version"].opt!string)) 233 .array; 234 } 235 236 private Json getBestPackage(string packageId, Dependency dep, bool pre_release) 237 { 238 Json md = getMetadata(packageId); 239 Json best = null; 240 Version bestver; 241 foreach (json; md["versions"]) { 242 auto cur = Version(cast(string)json["version"]); 243 if (!dep.matches(cur)) continue; 244 if (best == null) best = json; 245 else if (pre_release) { 246 if (cur > bestver) best = json; 247 } else if (bestver.isPreRelease) { 248 if (!cur.isPreRelease || cur > bestver) best = json; 249 } else if (!cur.isPreRelease && cur > bestver) best = json; 250 bestver = Version(cast(string)best["version"]); 251 } 252 enforce(best != null, "No package candidate found for "~packageId~" "~dep.toString()); 253 return best; 254 } 255 } 256 257 private enum PackagesPath = "packages";