MergerSitePlugin.py 15 KB

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