1 /**
2 */
3 module dubregistry.mirror;
4 
5 import dubregistry.registry;
6 import dubregistry.dbcontroller;
7 import userman.db.controller;
8 import vibe.core.log;
9 import vibe.data.bson;
10 import vibe.http.client;
11 import vibe.inet.url;
12 import std.array : array;
13 import std.datetime : SysTime;
14 import std.encoding : sanitize;
15 import std.format : format;
16 
17 void validateMirrorURL(ref string base_url)
18 {
19 	import std.exception : enforce;
20 	import std.algorithm.searching : endsWith;
21 	import vibe.core.file : existsFile;
22 
23 	// Local JSON files are allowed
24 	if (NativePath(base_url).existsFile)
25 		return;
26 
27 	// ensure the URL has a trailing slash
28 	if (!base_url.endsWith('/')) base_url ~= '/';
29 
30 	// check two characteristic API endpoints
31 	enum urls = ["packages/index.json", "api/packages/search?q=foobar"];
32 	foreach (url; urls) {
33 		try {
34 			requestHTTP(base_url ~ url,
35 				(scope req) { req.method = HTTPMethod.HEAD; },
36 				(scope res) {
37 					enforce(res.statusCode < 400,
38 						format("Endpoint '%s' could not be accessed: %s", url, httpStatusText(res.statusCode)));
39 				}
40 			);
41 		} catch (Exception e) {
42 			throw new Exception("The provided mirror URL does not appear to point to a valid DUB registry root: "~e.msg);
43 		}
44 	}
45 }
46 
47 void mirrorRegistry(DubRegistry registry, string fileOrUrl)
48 nothrow {
49 	logInfo("Polling '%s' for updates...", fileOrUrl);
50 	try {
51 		import vibe.core.file : existsFile, readFileUTF8;
52 
53 		DbPackage[] packs;
54 		URL url;
55 		auto path = NativePath(fileOrUrl);
56 
57 		if (path.existsFile)
58 		{
59 			packs = path.readFileUTF8.deserializeJson!(DbPackage[]);
60 		}
61 		else
62 		{
63 			url = URL(fileOrUrl);
64 			packs = requestHTTP(url ~ InetPath("api/packages/dump")).readJson().deserializeJson!(DbPackage[]);
65 		}
66 
67 		logInfo("Updates for '%s' downloaded.", url);
68 
69 		bool[BsonObjectID] current_packs;
70 		foreach (p; packs) current_packs[p._id] = true;
71 
72 		// first, remove all packages that don't exist anymore to avoid possible name conflicts
73 		foreach (id; registry.availablePackageIDs)
74 			if (id !in current_packs) {
75 				try {
76 					auto pack = registry.db.getPackage(id);
77 					logInfo("Removing package '%s", pack.name);
78 					registry.removePackage(pack.name, User.ID(pack.owner));
79 				} catch (Exception e) {
80 					logError("Failed to remove package with ID '%s': %s", id, e.msg);
81 					logDiagnostic("Full error: %s", e.toString().sanitize);
82 				}
83 			}
84 
85 		// then add/update all existing packages
86 		foreach (p; packs) {
87 			try {
88 				logInfo("Updating package '%s'", p.name);
89 				registry.addOrSetPackage(p);
90 			} catch (Exception e) {
91 				logError("Failed to add/update package '%s': %s", p.name, e.msg);
92 				logDiagnostic("Full error: %s", e.toString().sanitize);
93 			}
94 		}
95 
96 		logInfo("Updates for '%s' successfully processed.", fileOrUrl);
97 	} catch (Exception e) {
98 		logError("Fetching updated packages failed: %s", e.msg);
99 		logDiagnostic("Full error: %s", e.toString().sanitize);
100 	}
101 }