Newer
Older
dub_jkp / source / dub / packagesupplier.d
  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. auto md = getMetadata(package_id);
  174. if (md.type == Json.Type.null_)
  175. return null;
  176. Version[] ret;
  177. foreach (json; md["versions"]) {
  178. auto cur = Version(cast(string)json["version"]);
  179. ret ~= cur;
  180. }
  181. ret.sort();
  182. return ret;
  183. }
  184.  
  185. void fetchPackage(Path path, string packageId, Dependency dep, bool pre_release)
  186. {
  187. import std.array : replace;
  188. Json best = getBestPackage(packageId, dep, pre_release);
  189. if (best.type == Json.Type.null_)
  190. return;
  191. auto vers = best["version"].get!string;
  192. auto url = m_registryUrl ~ Path(PackagesPath~"/"~packageId~"/"~vers~".zip");
  193. logDiagnostic("Downloading from '%s'", url);
  194. download(url, path);
  195. }
  196.  
  197. Json fetchPackageRecipe(string packageId, Dependency dep, bool pre_release)
  198. {
  199. return getBestPackage(packageId, dep, pre_release);
  200. }
  201.  
  202. private Json getMetadata(string packageId)
  203. {
  204. auto now = Clock.currTime(UTC());
  205. if (auto pentry = packageId in m_metadataCache) {
  206. if (pentry.cacheTime + m_maxCacheTime > now)
  207. return pentry.data;
  208. m_metadataCache.remove(packageId);
  209. }
  210.  
  211. auto url = m_registryUrl ~ Path(PackagesPath ~ "/" ~ packageId ~ ".json");
  212.  
  213. logDebug("Downloading metadata for %s", packageId);
  214. logDebug("Getting from %s", url);
  215.  
  216. string jsonData;
  217. try
  218. jsonData = cast(string)download(url);
  219. catch (HTTPStatusException e)
  220. {
  221. if (e.status != 404)
  222. throw e;
  223. logDebug("Package %s not found in %s: %s", packageId, description, e.msg);
  224. return Json(null);
  225. }
  226. Json json = parseJsonString(jsonData, url.toString());
  227. // strip readme data (to save size and time)
  228. foreach (ref v; json["versions"])
  229. v.remove("readme");
  230. m_metadataCache[packageId] = CacheEntry(json, now);
  231. return json;
  232. }
  233.  
  234. SearchResult[] searchPackages(string query) {
  235. import std.uri : encodeComponent;
  236. auto url = m_registryUrl;
  237. url.localURI = "/api/packages/search?q="~encodeComponent(query);
  238. string data;
  239. data = cast(string)download(url);
  240. import std.algorithm : map;
  241. return data.parseJson.opt!(Json[])
  242. .map!(j => SearchResult(j["name"].opt!string, j["description"].opt!string, j["version"].opt!string))
  243. .array;
  244. }
  245.  
  246. private Json getBestPackage(string packageId, Dependency dep, bool pre_release)
  247. {
  248. Json md = getMetadata(packageId);
  249. if (md.type == Json.Type.null_)
  250. return md;
  251. Json best = null;
  252. Version bestver;
  253. foreach (json; md["versions"]) {
  254. auto cur = Version(cast(string)json["version"]);
  255. if (!dep.matches(cur)) continue;
  256. if (best == null) best = json;
  257. else if (pre_release) {
  258. if (cur > bestver) best = json;
  259. } else if (bestver.isPreRelease) {
  260. if (!cur.isPreRelease || cur > bestver) best = json;
  261. } else if (!cur.isPreRelease && cur > bestver) best = json;
  262. bestver = Version(cast(string)best["version"]);
  263. }
  264. enforce(best != null, "No package candidate found for "~packageId~" "~dep.toString());
  265. return best;
  266. }
  267. }
  268.  
  269. private enum PackagesPath = "packages";