1 /** 2 Copyright: © 2013 rejectedsoftware e.K. 3 License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file. 4 Authors: Sönke Ludwig 5 */ 6 module dubregistry.dbcontroller; 7 8 import dub.semver; 9 import std.array; 10 import std.algorithm; 11 import std.exception; 12 //import std.string; 13 import std.typecons : tuple; 14 import std.uni; 15 import vibe.vibe; 16 17 18 class DbController { 19 private { 20 MongoCollection m_packages; 21 MongoCollection m_downloads; 22 } 23 24 this(string dbname) 25 { 26 auto db = connectMongoDB("127.0.0.1").getDatabase(dbname); 27 m_packages = db["packages"]; 28 m_downloads = db["downloads"]; 29 30 // update package format 31 foreach(p; m_packages.find()){ 32 bool any_change = false; 33 if (p["branches"].type == Bson.Type.object) { 34 Bson[] branches; 35 foreach( b; p["branches"] ) 36 branches ~= b; 37 p["branches"] = branches; 38 any_change = true; 39 } 40 if (p["branches"].type == Bson.Type.array) { 41 auto versions = p["versions"].get!(Bson[]); 42 foreach (b; p["branches"]) versions ~= b; 43 p["branches"] = Bson(null); 44 p["versions"] = Bson(versions); 45 any_change = true; 46 } 47 if (any_change) m_packages.update(["_id": p["_id"]], p); 48 } 49 50 // add updateCounter field for packages that don't have it yet 51 m_packages.update(["updateCounter": ["$exists": false]], ["$set" : ["updateCounter" : 0L]], UpdateFlags.MultiUpdate); 52 53 repairVersionOrder(); 54 55 // create indices 56 m_packages.ensureIndex([tuple("name", 1)], IndexFlags.Unique); 57 m_downloads.ensureIndex([tuple("package", 1), tuple("version", 1)]); 58 59 Bson[string] doc; 60 doc["v"] = 1; 61 doc["key"] = ["_fts": Bson("text"), "_ftsx": Bson(1)]; 62 doc["ns"] = db.name ~ "." ~ m_packages.name; 63 doc["name"] = "packages_full_text_search_index"; 64 doc["weights"] = [ 65 "name": Bson(4), 66 "versions.info.description" : Bson(3), 67 // TODO: try to index readme 68 "versions.info.homepage" : Bson(1), 69 "versions.info.author" : Bson(1), 70 ]; 71 doc["background"] = true; 72 db["system.indexes"].insert(doc); 73 } 74 75 void addPackage(ref DbPackage pack) 76 { 77 enforce(m_packages.findOne(["name": pack.name], ["_id": true]).isNull(), "A package with the same name is already registered."); 78 if (pack._id == BsonObjectID.init) 79 pack._id = BsonObjectID.generate(); 80 m_packages.insert(pack); 81 } 82 83 void addOrSetPackage(ref DbPackage pack) 84 { 85 enforce(pack._id != BsonObjectID.init, "Cannot update a packag with no ID."); 86 m_packages.update(["_id": pack._id], pack, UpdateFlags.upsert); 87 } 88 89 DbPackage getPackage(string packname) 90 { 91 auto bpack = m_packages.findOne(["name": packname]); 92 enforce(!bpack.isNull(), "Unknown package name."); 93 return deserializeBson!DbPackage(bpack); 94 } 95 96 DbPackage getPackage(BsonObjectID id) 97 { 98 auto bpack = m_packages.findOne(["_id": id]); 99 enforce(!bpack.isNull(), "Unknown package ID."); 100 return deserializeBson!DbPackage(bpack); 101 } 102 103 auto getAllPackages() 104 { 105 return m_packages.find(Bson.emptyObject, ["name": 1]).map!(p => p["name"].get!string)(); 106 } 107 108 auto getAllPackageIDs() 109 { 110 return m_packages.find(Bson.emptyObject, ["_id": 1]).map!(p => p["_id"].get!BsonObjectID)(); 111 } 112 113 auto getPackageDump() 114 { 115 return m_packages.find(Bson.emptyObject).map!(p => p.deserializeBson!DbPackage); 116 } 117 118 auto getUserPackages(BsonObjectID user_id) 119 { 120 return m_packages.find(["owner": user_id], ["name": 1]).map!(p => p["name"].get!string)(); 121 } 122 123 bool isUserPackage(BsonObjectID user_id, string package_name) 124 { 125 return !m_packages.findOne(["owner": Bson(user_id), "name": Bson(package_name)]).isNull(); 126 } 127 128 void removePackage(string packname, BsonObjectID user) 129 { 130 m_packages.remove(["name": Bson(packname), "owner": Bson(user)]); 131 } 132 133 void setPackageErrors(string packname, string[] error...) 134 { 135 m_packages.update(["name": packname], ["$set": ["errors": error]]); 136 } 137 138 void setPackageCategories(string packname, string[] categories...) 139 { 140 m_packages.update(["name": packname], ["$set": ["categories": categories]]); 141 } 142 143 void setPackageRepository(string packname, DbRepository repo) 144 { 145 m_packages.update(["name": packname], ["$set": ["repository": repo]]); 146 } 147 148 void addVersion(string packname, DbPackageVersion ver) 149 { 150 assert(ver.version_.startsWith("~") || ver.version_.isValidVersion()); 151 152 size_t nretrys = 0; 153 154 while (true) { 155 auto pack = m_packages.findOne(["name": packname], ["versions": true, "updateCounter": true]); 156 auto counter = pack["updateCounter"].get!long; 157 auto versions = deserializeBson!(DbPackageVersion[])(pack["versions"]); 158 auto new_versions = versions ~ ver; 159 new_versions.sort!((a, b) => vcmp(a, b)); 160 161 // remove versions with invalid dependency names to avoid the findAndModify below to fail 162 new_versions = new_versions.filter!( 163 v => !v.info["dependencies"].opt!(Json[string]).byKey.canFind!(k => k.canFind(".")) 164 ).array; 165 166 //assert((cast(Json)bversions).toString() == (cast(Json)serializeToBson(versions)).toString()); 167 168 auto res = m_packages.findAndModify( 169 ["name": Bson(packname), "updateCounter": Bson(counter)], 170 ["$set": ["versions": serializeToBson(new_versions), "updateCounter": Bson(counter+1)]], 171 ["_id": true]); 172 173 if (!res.isNull) return; 174 175 enforce(nretrys++ < 20, format("Failed to store updated version list for %s", packname)); 176 logDebug("Failed to update version list atomically, retrying..."); 177 } 178 } 179 180 void removeVersion(string packname, string ver) 181 { 182 assert(ver.startsWith("~") || ver.isValidVersion()); 183 m_packages.update(["name": packname], ["$pull": ["versions": ["version": ver]]]); 184 } 185 186 void updateVersion(string packname, DbPackageVersion ver) 187 { 188 assert(ver.version_.startsWith("~") || ver.version_.isValidVersion()); 189 m_packages.update(["name": packname, "versions.version": ver.version_], ["$set": ["versions.$": ver]]); 190 } 191 192 bool hasVersion(string packname, string ver) 193 { 194 auto ret = m_packages.findOne(["name": packname, "versions.version" : ver], ["_id": true]); 195 return !ret.isNull(); 196 } 197 198 string getLatestVersion(string packname) 199 { 200 auto slice = serializeToBson(["$slice": -1]); 201 auto pack = m_packages.findOne(["name": packname], ["_id": Bson(true), "versions": slice]); 202 if (pack.isNull() || pack["versions"].isNull() || pack["versions"].length != 1) return null; 203 return deserializeBson!(string)(pack["versions"][0]["version"]); 204 } 205 206 DbPackageVersion getVersionInfo(string packname, string ver) 207 { 208 auto pack = m_packages.findOne(["name": packname, "versions.version": ver], ["versions.$": true]); 209 enforce(!pack.isNull(), "unknown package/version"); 210 assert(pack["versions"].length == 1); 211 return deserializeBson!(DbPackageVersion)(pack["versions"][0]); 212 } 213 214 DbPackage[] searchPackages(string query) 215 { 216 if (!query.strip.length) { 217 return m_packages.find() 218 .sort(["name": 1]) 219 .map!(deserializeBson!DbPackage) 220 .array; 221 } 222 223 return m_packages 224 .find(["$text": ["$search": query]], ["score": ["$meta": "textScore"]]) 225 .sort(["score": ["$meta": "textScore"]]) 226 .map!(deserializeBson!DbPackage) 227 .array; 228 } 229 230 BsonObjectID addDownload(BsonObjectID pack, string ver, string user_agent) 231 { 232 DbPackageDownload download; 233 download._id = BsonObjectID.generate(); 234 download.package_ = pack; 235 download.version_ = ver; 236 download.time = Clock.currTime(UTC()); 237 download.userAgent = user_agent; 238 m_downloads.insert(download); 239 return download._id; 240 } 241 242 auto getDownloadStats(BsonObjectID pack, string ver = null) 243 { 244 static Bson newerThan(SysTime time) 245 { 246 // doc.time >= time ? 1 : 0 247 alias bs = serializeToBson; 248 return bs([ 249 "$cond": [bs(["$gte": [bs("$time"), bs(time)]]), bs(1), bs(0)] 250 ]); 251 } 252 253 auto match = Bson.emptyObject(); 254 match["package"] = Bson(pack); 255 if (ver.length) match["version"] = ver; 256 257 immutable now = Clock.currTime; 258 auto res = m_downloads.aggregate( 259 ["$match": match], 260 ["$project": [ 261 "_id": Bson(false), 262 "total": serializeToBson(["$literal": 1]), 263 "monthly": newerThan(now - 30.days), 264 "weekly": newerThan(now - 7.days), 265 "daily": newerThan(now - 1.days)]], 266 ["$group": [ 267 "_id": Bson(null), // single group 268 "total": Bson(["$sum": Bson("$total")]), 269 "monthly": Bson(["$sum": Bson("$monthly")]), 270 "weekly": Bson(["$sum": Bson("$weekly")]), 271 "daily": Bson(["$sum": Bson("$daily")])]]); 272 assert(res.length <= 1); 273 return res.length ? deserializeBson!DbDownloadStats(res[0]) : DbDownloadStats.init; 274 } 275 276 private void repairVersionOrder() 277 { 278 foreach( bp; m_packages.find() ){ 279 auto p = deserializeBson!DbPackage(bp); 280 auto newversions = p.versions 281 .filter!(v => v.version_.startsWith("~") || v.version_.isValidVersion) 282 .array 283 .sort!((a, b) => vcmp(a, b)) 284 .uniq!((a, b) => a.version_ == b.version_) 285 .array; 286 if (p.versions != newversions) 287 m_packages.update(["_id": p._id], ["$set": ["versions": newversions]]); 288 } 289 } 290 } 291 292 struct DbPackage { 293 BsonObjectID _id; 294 BsonObjectID owner; 295 string name; 296 DbRepository repository; 297 DbPackageVersion[] versions; 298 string[] errors; 299 string[] categories; 300 long updateCounter = 0; // used to implement lockless read-modify-write cycles 301 } 302 303 struct DbRepository { 304 string kind; 305 string owner; 306 string project; 307 } 308 309 struct DbPackageVersion { 310 SysTime date; 311 string version_; 312 @optional string commitID; 313 Json info; 314 @optional string readme; 315 } 316 317 struct DbPackageDownload { 318 BsonObjectID _id; 319 BsonObjectID package_; 320 string version_; 321 SysTime time; 322 string userAgent; 323 } 324 325 struct DbDownloadStats { 326 uint total, monthly, weekly, daily; 327 } 328 329 bool vcmp(DbPackageVersion a, DbPackageVersion b) 330 { 331 return vcmp(a.version_, b.version_); 332 } 333 334 bool vcmp(string va, string vb) 335 { 336 import dub.dependency; 337 return Version(va) < Version(vb); 338 } 339 340 private string[] splitAlphaNumParts(string str) 341 { 342 string[] ret; 343 while (!str.empty) { 344 while (!str.empty && !str.front.isIdentChar()) str.popFront(); 345 if (str.empty) break; 346 size_t i = str.length; 347 foreach (j, dchar ch; str) 348 if (!isIdentChar(ch)) { 349 i = j; 350 break; 351 } 352 if (i > 0) { 353 ret ~= str[0 .. i]; 354 str = str[i .. $]; 355 } 356 if (!str.empty) str.popFront(); // pop non-ident-char 357 } 358 return ret; 359 } 360 361 private bool isIdentChar(dchar ch) 362 { 363 return std.uni.isAlpha(ch) || std.uni.isNumber(ch); 364 }