Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
at main 396 lines 15 kB view raw
1import re 2import time 3import html 4import os 5 6import gevent 7 8from Plugin import PluginManager 9from Config import config 10from util import helper 11from util.Flag import flag 12from Translate import Translate 13 14 15plugin_dir = os.path.dirname(__file__) 16 17if "_" not in locals(): 18 _ = Translate(plugin_dir + "/languages/") 19 20bigfile_sha512_cache = {} 21 22 23@PluginManager.registerTo("UiWebsocket") 24class UiWebsocketPlugin(object): 25 def __init__(self, *args, **kwargs): 26 self.time_peer_numbers_updated = 0 27 super(UiWebsocketPlugin, self).__init__(*args, **kwargs) 28 29 def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs): 30 # Add file to content.db and set it as pinned 31 content_db = self.site.content_manager.contents.db 32 content_inner_dir = helper.getDirname(inner_path) 33 content_db.my_optional_files[self.site.address + "/" + content_inner_dir] = time.time() 34 if len(content_db.my_optional_files) > 50: # Keep only last 50 35 oldest_key = min( 36 iter(content_db.my_optional_files.keys()), 37 key=(lambda key: content_db.my_optional_files[key]) 38 ) 39 del content_db.my_optional_files[oldest_key] 40 41 return super(UiWebsocketPlugin, self).actionSiteSign(to, privatekey, inner_path, *args, **kwargs) 42 43 def updatePeerNumbers(self): 44 self.site.updateHashfield() 45 content_db = self.site.content_manager.contents.db 46 content_db.updatePeerNumbers() 47 self.site.updateWebsocket(peernumber_updated=True) 48 49 def addBigfileInfo(self, row): 50 global bigfile_sha512_cache 51 52 content_db = self.site.content_manager.contents.db 53 site = content_db.sites[row["address"]] 54 if not site.settings.get("has_bigfile"): 55 return False 56 57 file_key = row["address"] + "/" + row["inner_path"] 58 sha512 = bigfile_sha512_cache.get(file_key) 59 file_info = None 60 if not sha512: 61 file_info = site.content_manager.getFileInfo(row["inner_path"]) 62 if not file_info or not file_info.get("piece_size"): 63 return False 64 sha512 = file_info["sha512"] 65 bigfile_sha512_cache[file_key] = sha512 66 67 if sha512 in site.storage.piecefields: 68 piecefield = site.storage.piecefields[sha512].tobytes() 69 else: 70 piecefield = None 71 72 if piecefield: 73 row["pieces"] = len(piecefield) 74 row["pieces_downloaded"] = piecefield.count(b"\x01") 75 row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"] 76 if row["pieces_downloaded"]: 77 if row["pieces"] == row["pieces_downloaded"]: 78 row["bytes_downloaded"] = row["size"] 79 else: 80 if not file_info: 81 file_info = site.content_manager.getFileInfo(row["inner_path"]) 82 row["bytes_downloaded"] = row["pieces_downloaded"] * file_info.get("piece_size", 0) 83 else: 84 row["bytes_downloaded"] = 0 85 86 row["is_downloading"] = bool(next((inner_path for inner_path in site.bad_files if inner_path.startswith(row["inner_path"])), False)) 87 88 # Add leech / seed stats 89 row["peer_seed"] = 0 90 row["peer_leech"] = 0 91 for peer in site.peers.values(): 92 if not peer.time_piecefields_updated or sha512 not in peer.piecefields: 93 continue 94 peer_piecefield = peer.piecefields[sha512].tobytes() 95 if not peer_piecefield: 96 continue 97 if peer_piecefield == b"\x01" * len(peer_piecefield): 98 row["peer_seed"] += 1 99 else: 100 row["peer_leech"] += 1 101 102 # Add myself 103 if piecefield: 104 if row["pieces_downloaded"] == row["pieces"]: 105 row["peer_seed"] += 1 106 else: 107 row["peer_leech"] += 1 108 109 return True 110 111 # Optional file functions 112 113 def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10, filter="downloaded", filter_inner_path=None): 114 if not address: 115 address = self.site.address 116 117 # Update peer numbers if necessary 118 content_db = self.site.content_manager.contents.db 119 if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5: 120 # Start in new thread to avoid blocking 121 self.time_peer_numbers_updated = time.time() 122 gevent.spawn(self.updatePeerNumbers) 123 124 if address == "all" and "ADMIN" not in self.permissions: 125 return self.response(to, {"error": "Forbidden"}) 126 127 if not self.hasSitePermission(address): 128 return self.response(to, {"error": "Forbidden"}) 129 130 if not all([re.match("^[a-z_*/+-]+( DESC| ASC|)$", part.strip()) for part in orderby.split(",")]): 131 return self.response(to, "Invalid order_by") 132 133 if type(limit) != int: 134 return self.response(to, "Invalid limit") 135 136 back = [] 137 content_db = self.site.content_manager.contents.db 138 139 wheres = {} 140 wheres_raw = [] 141 if "bigfile" in filter: 142 wheres["size >"] = 1024 * 1024 * 1 143 if "downloaded" in filter: 144 wheres_raw.append("(is_downloaded = 1 OR is_pinned = 1)") 145 if "pinned" in filter: 146 wheres["is_pinned"] = 1 147 if filter_inner_path: 148 wheres["inner_path__like"] = filter_inner_path 149 150 if address == "all": 151 join = "LEFT JOIN site USING (site_id)" 152 else: 153 wheres["site_id"] = content_db.site_ids[address] 154 join = "" 155 156 if wheres_raw: 157 query_wheres_raw = "AND" + " AND ".join(wheres_raw) 158 else: 159 query_wheres_raw = "" 160 161 query = "SELECT * FROM file_optional %s WHERE ? %s ORDER BY %s LIMIT %s" % (join, query_wheres_raw, orderby, limit) 162 163 for row in content_db.execute(query, wheres): 164 row = dict(row) 165 if address != "all": 166 row["address"] = address 167 168 if row["size"] > 1024 * 1024: 169 has_bigfile_info = self.addBigfileInfo(row) 170 else: 171 has_bigfile_info = False 172 173 if not has_bigfile_info and "bigfile" in filter: 174 continue 175 176 if not has_bigfile_info: 177 if row["is_downloaded"]: 178 row["bytes_downloaded"] = row["size"] 179 row["downloaded_percent"] = 100 180 else: 181 row["bytes_downloaded"] = 0 182 row["downloaded_percent"] = 0 183 184 back.append(row) 185 self.response(to, back) 186 187 def actionOptionalFileInfo(self, to, inner_path): 188 content_db = self.site.content_manager.contents.db 189 site_id = content_db.site_ids[self.site.address] 190 191 # Update peer numbers if necessary 192 if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5: 193 # Start in new thread to avoid blocking 194 self.time_peer_numbers_updated = time.time() 195 gevent.spawn(self.updatePeerNumbers) 196 197 query = "SELECT * FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1" 198 res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path}) 199 row = next(res, None) 200 if row: 201 row = dict(row) 202 if row["size"] > 1024 * 1024: 203 row["address"] = self.site.address 204 self.addBigfileInfo(row) 205 self.response(to, row) 206 else: 207 self.response(to, None) 208 209 def setPin(self, inner_path, is_pinned, address=None): 210 if not address: 211 address = self.site.address 212 213 if not self.hasSitePermission(address): 214 return {"error": "Forbidden"} 215 216 site = self.server.sites[address] 217 site.content_manager.setPin(inner_path, is_pinned) 218 219 return "ok" 220 221 @flag.no_multiuser 222 def actionOptionalFilePin(self, to, inner_path, address=None): 223 if type(inner_path) is not list: 224 inner_path = [inner_path] 225 back = self.setPin(inner_path, 1, address) 226 num_file = len(inner_path) 227 if back == "ok": 228 if num_file == 1: 229 self.cmd("notification", ["done", _["Pinned %s"] % html.escape(helper.getFilename(inner_path[0])), 5000]) 230 else: 231 self.cmd("notification", ["done", _["Pinned %s files"] % num_file, 5000]) 232 self.response(to, back) 233 234 @flag.no_multiuser 235 def actionOptionalFileUnpin(self, to, inner_path, address=None): 236 if type(inner_path) is not list: 237 inner_path = [inner_path] 238 back = self.setPin(inner_path, 0, address) 239 num_file = len(inner_path) 240 if back == "ok": 241 if num_file == 1: 242 self.cmd("notification", ["done", _["Removed pin from %s"] % html.escape(helper.getFilename(inner_path[0])), 5000]) 243 else: 244 self.cmd("notification", ["done", _["Removed pin from %s files"] % num_file, 5000]) 245 self.response(to, back) 246 247 @flag.no_multiuser 248 def actionOptionalFileDelete(self, to, inner_path, address=None): 249 if not address: 250 address = self.site.address 251 252 if not self.hasSitePermission(address): 253 return self.response(to, {"error": "Forbidden"}) 254 255 site = self.server.sites[address] 256 257 content_db = site.content_manager.contents.db 258 site_id = content_db.site_ids[site.address] 259 260 res = content_db.execute("SELECT * FROM file_optional WHERE ? LIMIT 1", {"site_id": site_id, "inner_path": inner_path, "is_downloaded": 1}) 261 row = next(res, None) 262 263 if not row: 264 return self.response(to, {"error": "Not found in content.db"}) 265 266 removed = site.content_manager.optionalRemoved(inner_path, row["hash_id"], row["size"]) 267 # if not removed: 268 # return self.response(to, {"error": "Not found in hash_id: %s" % row["hash_id"]}) 269 270 content_db.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"site_id": site_id, "inner_path": inner_path}) 271 272 try: 273 site.storage.delete(inner_path) 274 except Exception as err: 275 return self.response(to, {"error": "File delete error: %s" % err}) 276 site.updateWebsocket(file_delete=inner_path) 277 278 if inner_path in site.content_manager.cache_is_pinned: 279 site.content_manager.cache_is_pinned = {} 280 281 self.response(to, "ok") 282 283 # Limit functions 284 285 @flag.admin 286 def actionOptionalLimitStats(self, to): 287 back = {} 288 back["limit"] = config.optional_limit 289 back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes() 290 back["free"] = helper.getFreeSpace() 291 292 self.response(to, back) 293 294 @flag.no_multiuser 295 @flag.admin 296 def actionOptionalLimitSet(self, to, limit): 297 config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end 298 config.saveValue("optional_limit", limit) 299 self.response(to, "ok") 300 301 # Distribute help functions 302 303 def actionOptionalHelpList(self, to, address=None): 304 if not address: 305 address = self.site.address 306 307 if not self.hasSitePermission(address): 308 return self.response(to, {"error": "Forbidden"}) 309 310 site = self.server.sites[address] 311 312 self.response(to, site.settings.get("optional_help", {})) 313 314 @flag.no_multiuser 315 def actionOptionalHelp(self, to, directory, title, address=None): 316 if not address: 317 address = self.site.address 318 319 if not self.hasSitePermission(address): 320 return self.response(to, {"error": "Forbidden"}) 321 322 site = self.server.sites[address] 323 content_db = site.content_manager.contents.db 324 site_id = content_db.site_ids[address] 325 326 if "optional_help" not in site.settings: 327 site.settings["optional_help"] = {} 328 329 stats = content_db.execute( 330 "SELECT COUNT(*) AS num, SUM(size) AS size FROM file_optional WHERE site_id = :site_id AND inner_path LIKE :inner_path", 331 {"site_id": site_id, "inner_path": directory + "%"} 332 ).fetchone() 333 stats = dict(stats) 334 335 if not stats["size"]: 336 stats["size"] = 0 337 if not stats["num"]: 338 stats["num"] = 0 339 340 self.cmd("notification", [ 341 "done", 342 _["You started to help distribute <b>%s</b>.<br><small>Directory: %s</small>"] % 343 (html.escape(title), html.escape(directory)), 344 10000 345 ]) 346 347 site.settings["optional_help"][directory] = title 348 349 self.response(to, dict(stats)) 350 351 @flag.no_multiuser 352 def actionOptionalHelpRemove(self, to, directory, address=None): 353 if not address: 354 address = self.site.address 355 356 if not self.hasSitePermission(address): 357 return self.response(to, {"error": "Forbidden"}) 358 359 site = self.server.sites[address] 360 361 try: 362 del site.settings["optional_help"][directory] 363 self.response(to, "ok") 364 except Exception: 365 self.response(to, {"error": "Not found"}) 366 367 def cbOptionalHelpAll(self, to, site, value): 368 site.settings["autodownloadoptional"] = value 369 self.response(to, value) 370 371 @flag.no_multiuser 372 def actionOptionalHelpAll(self, to, value, address=None): 373 if not address: 374 address = self.site.address 375 376 if not self.hasSitePermission(address): 377 return self.response(to, {"error": "Forbidden"}) 378 379 site = self.server.sites[address] 380 381 if value: 382 if "ADMIN" in self.site.settings["permissions"]: 383 self.cbOptionalHelpAll(to, site, True) 384 else: 385 site_title = site.content_manager.contents["content.json"].get("title", address) 386 self.cmd( 387 "confirm", 388 [ 389 _["Help distribute all new optional files on site <b>%s</b>"] % html.escape(site_title), 390 _["Yes, I want to help!"] 391 ], 392 lambda res: self.cbOptionalHelpAll(to, site, True) 393 ) 394 else: 395 site.settings["autodownloadoptional"] = False 396 self.response(to, False)