UiWebsocketPlugin.py 13 KB

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