MergerSitePlugin.py 17 KB

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