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