Newer
Older
dub_jkp / source / dub / packagesupplier.d
  1. /**
  2. A package supplier, able to get some packages to the local FS.
  3.  
  4. Copyright: © 2012-2013 Matthias Dondorff
  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: drop the "best package" behavior and let retrievePackage/getPackageDescription take a Version instead of Dependency
  27.  
  28. /// Supplies packages, this is done by supplying the latest possible version
  29. /// which is available.
  30. interface PackageSupplier {
  31. /// Returns a hunman readable representation of the supplier
  32. @property string description();
  33.  
  34. Version[] getVersions(string package_id);
  35.  
  36. /// path: absolute path to store the package (usually in a zip format)
  37. void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release);
  38.  
  39. /// returns the metadata for the package
  40. Json getPackageDescription(string packageId, Dependency dep, bool pre_release);
  41.  
  42. /// perform cache operation
  43. void cacheOp(Path cacheDir, CacheOp op);
  44.  
  45. static struct SearchResult { string name, description, version_; }
  46. SearchResult[] searchPackages(string query);
  47. }
  48.  
  49. /// operations on package supplier cache
  50. enum CacheOp {
  51. load,
  52. store,
  53. clean,
  54. }
  55.  
  56. class FileSystemPackageSupplier : PackageSupplier {
  57. private {
  58. Path m_path;
  59. }
  60.  
  61. this(Path root) { m_path = root; }
  62.  
  63. override @property string description() { return "file repository at "~m_path.toNativeString(); }
  64.  
  65. Version[] getVersions(string package_id)
  66. {
  67. Version[] ret;
  68. foreach (DirEntry d; dirEntries(m_path.toNativeString(), package_id~"*", SpanMode.shallow)) {
  69. Path p = Path(d.name);
  70. logDebug("Entry: %s", p);
  71. enforce(to!string(p.head)[$-4..$] == ".zip");
  72. auto vers = p.head.toString()[package_id.length+1..$-4];
  73. logDebug("Version: %s", vers);
  74. ret ~= Version(vers);
  75. }
  76. ret.sort();
  77. return ret;
  78. }
  79.  
  80. void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release)
  81. {
  82. enforce(path.absolute);
  83. logInfo("Storing package '"~packageId~"', version requirements: %s", dep);
  84. auto filename = bestPackageFile(packageId, dep, pre_release);
  85. enforce(existsFile(filename));
  86. copyFile(filename, path);
  87. }
  88.  
  89. Json getPackageDescription(string packageId, Dependency dep, bool pre_release)
  90. {
  91. auto filename = bestPackageFile(packageId, dep, pre_release);
  92. return jsonFromZip(filename, "dub.json");
  93. }
  94.  
  95. void cacheOp(Path cacheDir, CacheOp op) {
  96. }
  97.  
  98. SearchResult[] searchPackages(string query) {
  99. return null;
  100. }
  101.  
  102. private Path bestPackageFile(string packageId, Dependency dep, bool pre_release)
  103. {
  104. Path toPath(Version ver) {
  105. return m_path ~ (packageId ~ "-" ~ ver.toString() ~ ".zip");
  106. }
  107. auto versions = getVersions(packageId).filter!(v => dep.matches(v)).array;
  108. enforce(versions.length > 0, format("No package %s found matching %s", packageId, dep));
  109. foreach_reverse (ver; versions) {
  110. if (pre_release || !ver.isPreRelease)
  111. return toPath(ver);
  112. }
  113. return toPath(versions[$-1]);
  114. }
  115. }
  116.  
  117.  
  118. /// Client PackageSupplier using the registry available via registerVpmRegistry
  119. class RegistryPackageSupplier : PackageSupplier {
  120. private {
  121. URL m_registryUrl;
  122. struct CacheEntry { Json data; SysTime cacheTime; }
  123. CacheEntry[string] m_metadataCache;
  124. Duration m_maxCacheTime;
  125. bool m_metadataCacheDirty;
  126. }
  127.  
  128. this(URL registry)
  129. {
  130. m_registryUrl = registry;
  131. m_maxCacheTime = 24.hours();
  132. }
  133.  
  134. override @property string description() { return "registry at "~m_registryUrl.toString(); }
  135.  
  136. Version[] getVersions(string package_id)
  137. {
  138. Version[] ret;
  139. Json md = getMetadata(package_id);
  140. foreach (json; md["versions"]) {
  141. auto cur = Version(cast(string)json["version"]);
  142. ret ~= cur;
  143. }
  144. ret.sort();
  145. return ret;
  146. }
  147.  
  148. void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release)
  149. {
  150. import std.array : replace;
  151. Json best = getBestPackage(packageId, dep, pre_release);
  152. auto vers = best["version"].get!string;
  153. auto url = m_registryUrl ~ Path(PackagesPath~"/"~packageId~"/"~vers~".zip");
  154. logDiagnostic("Found download URL: '%s'", url);
  155. download(url, path);
  156. }
  157.  
  158. Json getPackageDescription(string packageId, Dependency dep, bool pre_release)
  159. {
  160. return getBestPackage(packageId, dep, pre_release);
  161. }
  162.  
  163. void cacheOp(Path cacheDir, CacheOp op)
  164. {
  165. auto path = cacheDir ~ cacheFileName;
  166. final switch (op)
  167. {
  168. case CacheOp.store:
  169. if (!m_metadataCacheDirty) return;
  170. if (!cacheDir.existsFile())
  171. mkdirRecurse(cacheDir.toNativeString());
  172. // TODO: method is slow due to Json escaping
  173. atomicWriteJsonFile(path, m_metadataCache.serializeToJson());
  174. break;
  175.  
  176. case CacheOp.load:
  177. if (!path.existsFile()) return;
  178. try deserializeJson(m_metadataCache, jsonFromFile(path));
  179. catch (Exception e) {
  180. import std.encoding;
  181. logWarn("Error loading package cache file %s: %s", path.toNativeString(), e.msg);
  182. logDebug("Full error: %s", e.toString().sanitize());
  183. }
  184. break;
  185.  
  186. case CacheOp.clean:
  187. if (path.existsFile()) removeFile(path);
  188. m_metadataCache.destroy();
  189. break;
  190. }
  191. m_metadataCacheDirty = false;
  192. }
  193.  
  194. private @property string cacheFileName()
  195. {
  196. import std.digest.md;
  197. auto hash = m_registryUrl.toString.md5Of();
  198. return m_registryUrl.host ~ hash[0 .. $/2].toHexString().idup ~ ".json";
  199. }
  200.  
  201. private Json getMetadata(string packageId)
  202. {
  203. auto now = Clock.currTime(UTC());
  204. if (auto pentry = packageId in m_metadataCache) {
  205. if (pentry.cacheTime + m_maxCacheTime > now)
  206. return pentry.data;
  207. m_metadataCache.remove(packageId);
  208. m_metadataCacheDirty = true;
  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. auto jsonData = cast(string)download(url);
  217. Json json = parseJsonString(jsonData, url.toString());
  218. // strip readme data (to save size and time)
  219. foreach (ref v; json["versions"])
  220. v.remove("readme");
  221. m_metadataCache[packageId] = CacheEntry(json, now);
  222. m_metadataCacheDirty = true;
  223. return json;
  224. }
  225.  
  226. SearchResult[] searchPackages(string query) {
  227. import std.uri : encodeComponent;
  228. auto url = m_registryUrl;
  229. url.localURI = "/api/packages/search?q="~encodeComponent(query);
  230. string data;
  231. try
  232. data = cast(string)download(url);
  233. catch (Exception)
  234. return null;
  235. import std.algorithm : map;
  236. return data.parseJson.opt!(Json[])
  237. .map!(j => SearchResult(j["name"].opt!string, j["description"].opt!string, j["version"].opt!string))
  238. .array;
  239. }
  240.  
  241. private Json getBestPackage(string packageId, Dependency dep, bool pre_release)
  242. {
  243. Json md = getMetadata(packageId);
  244. Json best = null;
  245. Version bestver;
  246. foreach (json; md["versions"]) {
  247. auto cur = Version(cast(string)json["version"]);
  248. if (!dep.matches(cur)) continue;
  249. if (best == null) best = json;
  250. else if (pre_release) {
  251. if (cur > bestver) best = json;
  252. } else if (bestver.isPreRelease) {
  253. if (!cur.isPreRelease || cur > bestver) best = json;
  254. } else if (!cur.isPreRelease && cur > bestver) best = json;
  255. bestver = Version(cast(string)best["version"]);
  256. }
  257. enforce(best != null, "No package candidate found for "~packageId~" "~dep.toString());
  258. return best;
  259. }
  260. }
  261.  
  262. private enum PackagesPath = "packages";