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 }