MergerSitePlugin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import re
  2. import time
  3. import copy
  4. import os
  5. from Plugin import PluginManager
  6. from Translate import Translate
  7. from util import RateLimit
  8. from util import helper
  9. from util.Flag import flag
  10. from Debug import Debug
  11. try:
  12. import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible
  13. except Exception:
  14. pass
  15. if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads
  16. merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]}
  17. merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...}
  18. merged_to_merger = {} # {address: [site1, site2, ...]} cache
  19. site_manager = None # Site manager for merger sites
  20. plugin_dir = os.path.dirname(__file__)
  21. if "_" not in locals():
  22. _ = Translate(plugin_dir + "/languages/")
  23. # Check if the site has permission to this merger site
  24. def checkMergerPath(address, inner_path):
  25. merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path)
  26. if merged_match:
  27. merger_type = merged_match.group(1)
  28. # Check if merged site is allowed to include other sites
  29. if merger_type in merger_db.get(address, []):
  30. # Check if included site allows to include
  31. merged_address = merged_match.group(2)
  32. if merged_db.get(merged_address) == merger_type:
  33. inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path)
  34. return merged_address, inner_path
  35. else:
  36. raise Exception(
  37. "Merger site (%s) does not have permission for merged site: %s (%s)" %
  38. (merger_type, merged_address, merged_db.get(merged_address))
  39. )
  40. else:
  41. raise Exception("No merger (%s) permission to load: <br>%s (%s not in %s)" % (
  42. address, inner_path, merger_type, merger_db.get(address, []))
  43. )
  44. else:
  45. raise Exception("Invalid merger path: %s" % inner_path)
  46. @PluginManager.registerTo("UiWebsocket")
  47. class UiWebsocketPlugin(object):
  48. # Download new site
  49. def actionMergerSiteAdd(self, to, addresses):
  50. if type(addresses) != list:
  51. # Single site add
  52. addresses = [addresses]
  53. # Check if the site has merger permission
  54. merger_types = merger_db.get(self.site.address)
  55. if not merger_types:
  56. return self.response(to, {"error": "Not a merger site"})
  57. if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1:
  58. # Without confirmation if only one site address and not called in last 10 sec
  59. self.cbMergerSiteAdd(to, addresses)
  60. else:
  61. self.cmd(
  62. "confirm",
  63. [_["Add <b>%s</b> new site?"] % len(addresses), "Add"],
  64. lambda res: self.cbMergerSiteAdd(to, addresses)
  65. )
  66. self.response(to, "ok")
  67. # Callback of adding new site confirmation
  68. def cbMergerSiteAdd(self, to, addresses):
  69. added = 0
  70. for address in addresses:
  71. try:
  72. site_manager.need(address)
  73. added += 1
  74. except Exception as err:
  75. self.cmd("notification", ["error", _["Adding <b>%s</b> failed: %s"] % (address, err)])
  76. if added:
  77. self.cmd("notification", ["done", _["Added <b>%s</b> new site"] % added, 5000])
  78. RateLimit.called(self.site.address + "-MergerSiteAdd")
  79. site_manager.updateMergerSites()
  80. # Delete a merged site
  81. @flag.no_multiuser
  82. def actionMergerSiteDelete(self, to, address):
  83. site = self.server.sites.get(address)
  84. if not site:
  85. return self.response(to, {"error": "No site found: %s" % address})
  86. merger_types = merger_db.get(self.site.address)
  87. if not merger_types:
  88. return self.response(to, {"error": "Not a merger site"})
  89. if merged_db.get(address) not in merger_types:
  90. return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)})
  91. self.cmd("notification", ["done", _["Site deleted: <b>%s</b>"] % address, 5000])
  92. self.response(to, "ok")
  93. # Lists merged sites
  94. def actionMergerSiteList(self, to, query_site_info=False):
  95. merger_types = merger_db.get(self.site.address)
  96. ret = {}
  97. if not merger_types:
  98. return self.response(to, {"error": "Not a merger site"})
  99. for address, merged_type in merged_db.items():
  100. if merged_type not in merger_types:
  101. continue # Site not for us
  102. if query_site_info:
  103. site = self.server.sites.get(address)
  104. ret[address] = self.formatSiteInfo(site, create_user=False)
  105. else:
  106. ret[address] = merged_type
  107. self.response(to, ret)
  108. def hasSitePermission(self, address, *args, **kwargs):
  109. if super(UiWebsocketPlugin, self).hasSitePermission(address, *args, **kwargs):
  110. return True
  111. else:
  112. if self.site.address in [merger_site.address for merger_site in merged_to_merger.get(address, [])]:
  113. return True
  114. else:
  115. return False
  116. # Add support merger sites for file commands
  117. def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs):
  118. if inner_path.startswith("merged-"):
  119. merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
  120. # Set the same cert for merged site
  121. merger_cert = self.user.getSiteData(self.site.address).get("cert")
  122. if merger_cert and self.user.getSiteData(merged_address).get("cert") != merger_cert:
  123. self.user.setCert(merged_address, merger_cert)
  124. req_self = copy.copy(self)
  125. req_self.site = self.server.sites.get(merged_address) # Change the site to the merged one
  126. func = getattr(super(UiWebsocketPlugin, req_self), func_name)
  127. return func(to, merged_inner_path, *args, **kwargs)
  128. else:
  129. func = getattr(super(UiWebsocketPlugin, self), func_name)
  130. return func(to, inner_path, *args, **kwargs)
  131. def actionFileList(self, to, inner_path, *args, **kwargs):
  132. return self.mergerFuncWrapper("actionFileList", to, inner_path, *args, **kwargs)
  133. def actionDirList(self, to, inner_path, *args, **kwargs):
  134. return self.mergerFuncWrapper("actionDirList", to, inner_path, *args, **kwargs)
  135. def actionFileGet(self, to, inner_path, *args, **kwargs):
  136. return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs)
  137. def actionFileWrite(self, to, inner_path, *args, **kwargs):
  138. return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs)
  139. def actionFileDelete(self, to, inner_path, *args, **kwargs):
  140. return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs)
  141. def actionFileRules(self, to, inner_path, *args, **kwargs):
  142. return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs)
  143. def actionFileNeed(self, to, inner_path, *args, **kwargs):
  144. return self.mergerFuncWrapper("actionFileNeed", to, inner_path, *args, **kwargs)
  145. def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs):
  146. return self.mergerFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs)
  147. def actionOptionalFileDelete(self, to, inner_path, *args, **kwargs):
  148. return self.mergerFuncWrapper("actionOptionalFileDelete", to, inner_path, *args, **kwargs)
  149. def actionBigfileUploadInit(self, to, inner_path, *args, **kwargs):
  150. back = self.mergerFuncWrapper("actionBigfileUploadInit", to, inner_path, *args, **kwargs)
  151. if inner_path.startswith("merged-"):
  152. merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
  153. back["inner_path"] = "merged-%s/%s/%s" % (merged_db[merged_address], merged_address, back["inner_path"])
  154. return back
  155. # Add support merger sites for file commands with privatekey parameter
  156. def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs):
  157. func = getattr(super(UiWebsocketPlugin, self), func_name)
  158. if inner_path.startswith("merged-"):
  159. merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path)
  160. merged_site = self.server.sites.get(merged_address)
  161. # Set the same cert for merged site
  162. merger_cert = self.user.getSiteData(self.site.address).get("cert")
  163. if merger_cert:
  164. self.user.setCert(merged_address, merger_cert)
  165. site_before = self.site # Save to be able to change it back after we ran the command
  166. self.site = merged_site # Change the site to the merged one
  167. try:
  168. back = func(to, privatekey, merged_inner_path, *args, **kwargs)
  169. finally:
  170. self.site = site_before # Change back to original site
  171. return back
  172. else:
  173. return func(to, privatekey, inner_path, *args, **kwargs)
  174. def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
  175. return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs)
  176. def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs):
  177. return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs)
  178. def actionPermissionAdd(self, to, permission):
  179. super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
  180. if permission.startswith("Merger"):
  181. self.site.storage.rebuildDb()
  182. def actionPermissionDetails(self, to, permission):
  183. if not permission.startswith("Merger"):
  184. return super(UiWebsocketPlugin, self).actionPermissionDetails(to, permission)
  185. merger_type = permission.replace("Merger:", "")
  186. if not re.match("^[A-Za-z0-9-]+$", merger_type):
  187. raise Exception("Invalid merger_type: %s" % merger_type)
  188. merged_sites = []
  189. for address, merged_type in merged_db.items():
  190. if merged_type != merger_type:
  191. continue
  192. site = self.server.sites.get(address)
  193. try:
  194. merged_sites.append(site.content_manager.contents.get("content.json").get("title", address))
  195. except Exception:
  196. merged_sites.append(address)
  197. details = _["Read and write permissions to sites with merged type of <b>%s</b> "] % merger_type
  198. details += _["(%s sites)"] % len(merged_sites)
  199. details += "<div style='white-space: normal; max-width: 400px'>%s</div>" % ", ".join(merged_sites)
  200. self.response(to, details)
  201. @PluginManager.registerTo("UiRequest")
  202. class UiRequestPlugin(object):
  203. # Allow to load merged site files using /merged-ZeroMe/address/file.jpg
  204. def parsePath(self, path):
  205. path_parts = super(UiRequestPlugin, self).parsePath(path)
  206. if "merged-" not in path: # Optimization
  207. return path_parts
  208. path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"])
  209. return path_parts
  210. @PluginManager.registerTo("SiteStorage")
  211. class SiteStoragePlugin(object):
  212. # Also rebuild from merged sites
  213. def getDbFiles(self):
  214. merger_types = merger_db.get(self.site.address)
  215. # First return the site's own db files
  216. for item in super(SiteStoragePlugin, self).getDbFiles():
  217. yield item
  218. # Not a merger site, that's all
  219. if not merger_types:
  220. return
  221. merged_sites = [
  222. site_manager.sites[address]
  223. for address, merged_type in merged_db.items()
  224. if merged_type in merger_types
  225. ]
  226. found = 0
  227. for merged_site in merged_sites:
  228. self.log.debug("Loading merged site: %s" % merged_site)
  229. merged_type = merged_db[merged_site.address]
  230. for content_inner_path, content in merged_site.content_manager.contents.items():
  231. # content.json file itself
  232. if merged_site.storage.isFile(content_inner_path): # Missing content.json file
  233. merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path)
  234. yield merged_inner_path, merged_site.storage.getPath(content_inner_path)
  235. else:
  236. merged_site.log.error("[MISSING] %s" % content_inner_path)
  237. # Data files in content.json
  238. content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site
  239. for file_relative_path in list(content.get("files", {}).keys()) + list(content.get("files_optional", {}).keys()):
  240. if not file_relative_path.endswith(".json"):
  241. continue # We only interesed in json files
  242. file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir
  243. file_inner_path = file_inner_path.strip("/") # Strip leading /
  244. if merged_site.storage.isFile(file_inner_path):
  245. merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path)
  246. yield merged_inner_path, merged_site.storage.getPath(file_inner_path)
  247. else:
  248. merged_site.log.error("[MISSING] %s" % file_inner_path)
  249. found += 1
  250. if found % 100 == 0:
  251. time.sleep(0.001) # Context switch to avoid UI block
  252. # Also notice merger sites on a merged site file change
  253. def onUpdated(self, inner_path, file=None):
  254. super(SiteStoragePlugin, self).onUpdated(inner_path, file)
  255. merged_type = merged_db.get(self.site.address)
  256. for merger_site in merged_to_merger.get(self.site.address, []):
  257. if merger_site.address == self.site.address: # Avoid infinite loop
  258. continue
  259. virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path)
  260. if inner_path.endswith(".json"):
  261. if file is not None:
  262. merger_site.storage.onUpdated(virtual_path, file=file)
  263. else:
  264. merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path))
  265. else:
  266. merger_site.storage.onUpdated(virtual_path)
  267. @PluginManager.registerTo("Site")
  268. class SitePlugin(object):
  269. def fileDone(self, inner_path):
  270. super(SitePlugin, self).fileDone(inner_path)
  271. for merger_site in merged_to_merger.get(self.address, []):
  272. if merger_site.address == self.address:
  273. continue
  274. for ws in merger_site.websockets:
  275. ws.event("siteChanged", self, {"event": ["file_done", inner_path]})
  276. def fileFailed(self, inner_path):
  277. super(SitePlugin, self).fileFailed(inner_path)
  278. for merger_site in merged_to_merger.get(self.address, []):
  279. if merger_site.address == self.address:
  280. continue
  281. for ws in merger_site.websockets:
  282. ws.event("siteChanged", self, {"event": ["file_failed", inner_path]})
  283. @PluginManager.registerTo("SiteManager")
  284. class SiteManagerPlugin(object):
  285. # Update merger site for site types
  286. def updateMergerSites(self):
  287. global merger_db, merged_db, merged_to_merger, site_manager
  288. s = time.time()
  289. merger_db_new = {}
  290. merged_db_new = {}
  291. merged_to_merger_new = {}
  292. site_manager = self
  293. if not self.sites:
  294. return
  295. for site in self.sites.values():
  296. # Update merged sites
  297. try:
  298. merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type")
  299. except Exception as err:
  300. self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err)))
  301. continue
  302. if merged_type:
  303. merged_db_new[site.address] = merged_type
  304. # Update merger sites
  305. for permission in site.settings["permissions"]:
  306. if not permission.startswith("Merger:"):
  307. continue
  308. if merged_type:
  309. self.log.error(
  310. "Removing permission %s from %s: Merger and merged at the same time." %
  311. (permission, site.address)
  312. )
  313. site.settings["permissions"].remove(permission)
  314. continue
  315. merger_type = permission.replace("Merger:", "")
  316. if site.address not in merger_db_new:
  317. merger_db_new[site.address] = []
  318. merger_db_new[site.address].append(merger_type)
  319. site_manager.sites[site.address] = site
  320. # Update merged to merger
  321. if merged_type:
  322. for merger_site in self.sites.values():
  323. if "Merger:" + merged_type in merger_site.settings["permissions"]:
  324. if site.address not in merged_to_merger_new:
  325. merged_to_merger_new[site.address] = []
  326. merged_to_merger_new[site.address].append(merger_site)
  327. # Update globals
  328. merger_db = merger_db_new
  329. merged_db = merged_db_new
  330. merged_to_merger = merged_to_merger_new
  331. self.log.debug("Updated merger sites in %.3fs" % (time.time() - s))
  332. def load(self, *args, **kwags):
  333. super(SiteManagerPlugin, self).load(*args, **kwags)
  334. self.updateMergerSites()
  335. def saveDelayed(self, *args, **kwags):
  336. super(SiteManagerPlugin, self).saveDelayed(*args, **kwags)
  337. self.updateMergerSites()