UiWebsocketPlugin.py 14 KB

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