1 module dub.packagesuppliers.filesystem;
2 
3 import dub.internal.logging;
4 import dub.internal.vibecompat.inet.path;
5 import dub.packagesuppliers.packagesupplier;
6 
7 import std.exception : enforce;
8 
9 /**
10 	File system based package supplier.
11 
12 	This package supplier searches a certain directory for files with names of
13 	the form "[package name]-[version].zip".
14 */
15 class FileSystemPackageSupplier : PackageSupplier {
16 	private {
17 		NativePath m_path;
18 	}
19 
20 	this(NativePath root) { m_path = root; }
21 
22 	override @property string description() { return "file repository at "~m_path.toNativeString(); }
23 
24 	Version[] getVersions(in PackageName name)
25 	{
26 		import std.algorithm.sorting : sort;
27 		import std.file : dirEntries, DirEntry, SpanMode;
28 		import std.conv : to;
29 		import dub.semver : isValidVersion;
30 		Version[] ret;
31         const zipFileGlob = name.main.toString() ~ "*.zip";
32 		foreach (DirEntry d; dirEntries(m_path.toNativeString(), zipFileGlob, SpanMode.shallow)) {
33 			NativePath p = NativePath(d.name);
34 			auto vers = p.head.name[name.main.toString().length+1..$-4];
35 			if (!isValidVersion(vers)) {
36 				logDebug("Ignoring entry '%s' because it isn't a version of package '%s'", p, name.main);
37 				continue;
38 			}
39 			logDebug("Entry: %s", p);
40 			logDebug("Version: %s", vers);
41 			ret ~= Version(vers);
42 		}
43 		ret.sort();
44 		return ret;
45 	}
46 
47 	override ubyte[] fetchPackage(in PackageName name,
48 		in VersionRange dep, bool pre_release)
49 	{
50 		import dub.internal.vibecompat.core.file : readFile, existsFile;
51 		logInfo("Storing package '%s', version requirements: %s", name.main, dep);
52 		auto filename = bestPackageFile(name, dep, pre_release);
53 		enforce(existsFile(filename));
54 		return readFile(filename);
55 	}
56 
57 	override Json fetchPackageRecipe(in PackageName name, in VersionRange dep,
58 		bool pre_release)
59 	{
60 		import std.array : split;
61 		import std.path : stripExtension;
62 		import std.algorithm : startsWith, endsWith;
63 		import dub.internal.utils : packageInfoFileFromZip;
64 		import dub.recipe.io : parsePackageRecipe;
65 		import dub.recipe.json : toJson;
66 
67 		auto filePath = bestPackageFile(name, dep, pre_release);
68 		string packageFileName;
69 		string packageFileContent = packageInfoFileFromZip(filePath, packageFileName);
70 		auto recipe = parsePackageRecipe(packageFileContent, packageFileName);
71 		Json json = toJson(recipe);
72 		auto basename = filePath.head.name;
73 		enforce(basename.endsWith(".zip"), "Malformed package filename: " ~ filePath.toNativeString);
74 		enforce(basename.startsWith(name.main.toString()),
75 			"Malformed package filename: " ~ filePath.toNativeString);
76 		json["version"] = basename[name.main.toString().length + 1 .. $-4];
77 		return json;
78 	}
79 
80 	SearchResult[] searchPackages(string query)
81 	{
82 		// TODO!
83 		return null;
84 	}
85 
86 	private NativePath bestPackageFile(in PackageName name, in VersionRange dep,
87 		bool pre_release)
88 	{
89 		import std.algorithm.iteration : filter;
90 		import std.array : array;
91 		import std.format : format;
92 		NativePath toPath(Version ver) {
93 			return m_path ~ "%s-%s.zip".format(name.main, ver);
94 		}
95 		auto versions = getVersions(name).filter!(v => dep.matches(v)).array;
96 		enforce(versions.length > 0, format("No package %s found matching %s", name.main, dep));
97 		foreach_reverse (ver; versions) {
98 			if (pre_release || !ver.isPreRelease)
99 				return toPath(ver);
100 		}
101 		return toPath(versions[$-1]);
102 	}
103 }