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";