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 : 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 	import vibe.core.file : existsFile, readFileUTF8;
50 	import vibe.stream.operations : readAllUTF8;
51 
52 	logInfo("Polling '%s' for updates...", fileOrUrl);
53 	try {
54 		DbPackage[] packs;
55 		URL url;
56 		auto path = NativePath(fileOrUrl);
57 
58 		string source_text;
59 
60 		if (path.existsFile) source_text = path.readFileUTF8;
61 		else {
62 			url = URL(fileOrUrl);
63 			source_text = requestHTTP(url ~ InetPath("api/packages/dump"))
64 				.bodyReader
65 				.readAllUTF8;
66 		}
67 
68 		packs = source_text.deserializeJson!(DbPackage[]);
69 
70 		logInfo("Updates for '%s' downloaded.", url);
71 
72 		bool[BsonObjectID] current_packs;
73 		foreach (p; packs) current_packs[p._id] = true;
74 
75 		// first, remove all packages that don't exist anymore to avoid possible name conflicts
76 		foreach (id; registry.availablePackageIDs)
77 			if (id !in current_packs) {
78 				try {
79 					auto pack = registry.db.getPackage(id);
80 					logInfo("Removing package '%s", pack.name);
81 					registry.removePackage(pack.name, User.ID(pack.owner));
82 				} catch (Exception e) {
83 					logError("Failed to remove package with ID '%s': %s", id, e.msg);
84 					logDiagnostic("Full error: %s", e.toString().sanitize);
85 				}
86 			}
87 
88 		// then add/update all existing packages
89 		foreach (p; packs) {
90 			try {
91 				logInfo("Updating package '%s'", p.name);
92 				registry.addOrSetPackage(p);
93 			} catch (Exception e) {
94 				logError("Failed to add/update package '%s': %s", p.name, e.msg);
95 				logDiagnostic("Full error: %s", e.toString().sanitize);
96 			}
97 		}
98 
99 		logInfo("Updates for '%s' successfully processed.", fileOrUrl);
100 	} catch (Exception e) {
101 		logError("Fetching updated packages failed: %s", e.msg);
102 		logDiagnostic("Full error: %s", e.toString().sanitize);
103 	}
104 }