/** A package supplier, able to get some packages to the local FS. Copyright: © 2012-2013 Matthias Dondorff License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Matthias Dondorff */ module dub.packagesupplier; import dub.dependency; import dub.internal.utils; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.url; import std.algorithm : filter, sort; import std.array : array; import std.conv; import std.datetime; import std.exception; import std.file; import std.string : format; import std.zip; // TODO: drop the "best package" behavior and let retrievePackage/getPackageDescription take a Version instead of Dependency /// Supplies packages, this is done by supplying the latest possible version /// which is available. interface PackageSupplier { /// Returns a hunman readable representation of the supplier @property string description(); Version[] getVersions(string package_id); /// path: absolute path to store the package (usually in a zip format) void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release); /// returns the metadata for the package Json getPackageDescription(string packageId, Dependency dep, bool pre_release); /// perform cache operation void cacheOp(Path cacheDir, CacheOp op); } /// operations on package supplier cache enum CacheOp { load, store, clean, } class FileSystemPackageSupplier : PackageSupplier { private { Path m_path; } this(Path root) { m_path = root; } override @property string description() { return "file repository at "~m_path.toNativeString(); } Version[] getVersions(string package_id) { Version[] ret; foreach (DirEntry d; dirEntries(m_path.toNativeString(), package_id~"*", SpanMode.shallow)) { Path p = Path(d.name); logDebug("Entry: %s", p); enforce(to!string(p.head)[$-4..$] == ".zip"); auto vers = p.head.toString()[package_id.length+1..$-4]; logDebug("Version: %s", vers); ret ~= Version(vers); } ret.sort(); return ret; } void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release) { enforce(path.absolute); logInfo("Storing package '"~packageId~"', version requirements: %s", dep); auto filename = bestPackageFile(packageId, dep, pre_release); enforce(existsFile(filename)); copyFile(filename, path); } Json getPackageDescription(string packageId, Dependency dep, bool pre_release) { auto filename = bestPackageFile(packageId, dep, pre_release); return jsonFromZip(filename, "dub.json"); } void cacheOp(Path cacheDir, CacheOp op) { } private Path bestPackageFile(string packageId, Dependency dep, bool pre_release) { Path toPath(Version ver) { return m_path ~ (packageId ~ "-" ~ ver.toString() ~ ".zip"); } auto versions = getVersions(packageId).filter!(v => dep.matches(v)).array; enforce(versions.length > 0, format("No package %s found matching %s", packageId, dep)); foreach_reverse (ver; versions) { if (pre_release || !ver.isPreRelease) return toPath(ver); } return toPath(versions[$-1]); } } /// Client PackageSupplier using the registry available via registerVpmRegistry class RegistryPackageSupplier : PackageSupplier { private { URL m_registryUrl; struct CacheEntry { Json data; SysTime cacheTime; } CacheEntry[string] m_metadataCache; Duration m_maxCacheTime; bool m_metadataCacheDirty; } this(URL registry) { m_registryUrl = registry; m_maxCacheTime = 24.hours(); } override @property string description() { return "registry at "~m_registryUrl.toString(); } Version[] getVersions(string package_id) { Version[] ret; Json md = getMetadata(package_id); foreach (json; md["versions"]) { auto cur = Version(cast(string)json["version"]); ret ~= cur; } ret.sort(); return ret; } void retrievePackage(Path path, string packageId, Dependency dep, bool pre_release) { import std.array : replace; Json best = getBestPackage(packageId, dep, pre_release); auto vers = best["version"].get!string; auto url = m_registryUrl ~ Path(PackagesPath~"/"~packageId~"/"~vers~".zip"); logDiagnostic("Found download URL: '%s'", url); download(url, path); } Json getPackageDescription(string packageId, Dependency dep, bool pre_release) { return getBestPackage(packageId, dep, pre_release); } void cacheOp(Path cacheDir, CacheOp op) { auto path = cacheDir ~ cacheFileName; final switch (op) { case CacheOp.store: if (!m_metadataCacheDirty) return; if (!cacheDir.existsFile()) mkdirRecurse(cacheDir.toNativeString()); // TODO: method is slow due to Json escaping writeJsonFile(path, m_metadataCache.serializeToJson()); break; case CacheOp.load: if (!path.existsFile()) return; try deserializeJson(m_metadataCache, jsonFromFile(path)); catch (Exception e) { import std.encoding; logWarn("Error loading package cache file %s: %s", path.toNativeString(), e.msg); logDebug("Full error: %s", e.toString().sanitize()); } break; case CacheOp.clean: if (path.existsFile()) removeFile(path); m_metadataCache.destroy(); break; } m_metadataCacheDirty = false; } private @property string cacheFileName() { import std.digest.md; auto hash = m_registryUrl.toString.md5Of(); return m_registryUrl.host ~ hash[0 .. $/2].toHexString().idup ~ ".json"; } private Json getMetadata(string packageId) { auto now = Clock.currTime(UTC()); if (auto pentry = packageId in m_metadataCache) { if (pentry.cacheTime + m_maxCacheTime > now) return pentry.data; m_metadataCache.remove(packageId); m_metadataCacheDirty = true; } auto url = m_registryUrl ~ Path(PackagesPath ~ "/" ~ packageId ~ ".json"); logDebug("Downloading metadata for %s", packageId); logDebug("Getting from %s", url); auto jsonData = cast(string)download(url); Json json = parseJsonString(jsonData, url.toString()); // strip readme data (to save size and time) foreach (ref v; json["versions"]) v.remove("readme"); m_metadataCache[packageId] = CacheEntry(json, now); m_metadataCacheDirty = true; return json; } private Json getBestPackage(string packageId, Dependency dep, bool pre_release) { Json md = getMetadata(packageId); Json best = null; Version bestver; foreach (json; md["versions"]) { auto cur = Version(cast(string)json["version"]); if (!dep.matches(cur)) continue; if (best == null) best = json; else if (pre_release) { if (cur > bestver) best = json; } else if (bestver.isPreRelease) { if (!cur.isPreRelease || cur > bestver) best = json; } else if (!cur.isPreRelease && cur > bestver) best = json; bestver = Version(cast(string)best["version"]); } enforce(best != null, "No package candidate found for "~packageId~" "~dep.toString()); return best; } } private enum PackagesPath = "packages";