source file: /home/buildslave/tahoe/edgy/build/src/allmydata/web/directory.py
file stats: 759 lines, 731 executed: 96.3% covered
coverage versus previous test: 0 lines added, 0 lines removed
1.
2. import simplejson
3. import urllib
4.
5. from zope.interface import implements
6. from twisted.internet import defer
7. from twisted.internet.interfaces import IPushProducer
8. from twisted.python.failure import Failure
9. from twisted.web import http, html
10. from nevow import url, rend, inevow, tags as T
11. from nevow.inevow import IRequest
12.
13. from foolscap.api import fireEventually
14.
15. from allmydata.util import base32, time_format
16. from allmydata.uri import from_string_dirnode
17. from allmydata.interfaces import IDirectoryNode, IFileNode, IMutableFileNode, \
18. IFilesystemNode, ExistingChildError, NoSuchChildError
19. from allmydata.monitor import Monitor, OperationCancelledError
20. from allmydata import dirnode
21. from allmydata.web.common import text_plain, WebError, \
22. IOpHandleTable, NeedOperationHandleError, \
23. boolean_of_arg, get_arg, get_root, parse_replace_arg, \
24. should_create_intermediate_directories, \
25. getxmlfile, RenderMixin, humanize_failure
26. from allmydata.web.filenode import ReplaceMeMixin, \
27. FileNodeHandler, PlaceHolderNodeHandler
28. from allmydata.web.check_results import CheckResults, \
29. CheckAndRepairResults, DeepCheckResults, DeepCheckAndRepairResults
30. from allmydata.web.info import MoreInfo
31. from allmydata.web.operations import ReloadMixin
32. from allmydata.web.check_results import json_check_results, \
33. json_check_and_repair_results
34.
35. class BlockingFileError(Exception):
36. # TODO: catch and transform
37. """We cannot auto-create a parent directory, because there is a file in
38. the way"""
39.
40. def make_handler_for(node, client, parentnode=None, name=None):
41. if parentnode:
42. assert IDirectoryNode.providedBy(parentnode)
43. if IMutableFileNode.providedBy(node):
44. return FileNodeHandler(client, node, parentnode, name)
45. if IFileNode.providedBy(node):
46. return FileNodeHandler(client, node, parentnode, name)
47. if IDirectoryNode.providedBy(node):
48. return DirectoryNodeHandler(client, node, parentnode, name)
49. return UnknownNodeHandler(client, node, parentnode, name)
50.
51. class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin):
52. addSlash = True
53.
54. def __init__(self, client, node, parentnode=None, name=None):
55. rend.Page.__init__(self)
56. self.client = client
57. assert node
58. self.node = node
59. self.parentnode = parentnode
60. self.name = name
61.
62. def childFactory(self, ctx, name):
63. req = IRequest(ctx)
64. name = name.decode("utf-8")
65. d = self.node.get(name)
66. d.addBoth(self.got_child, ctx, name)
67. # got_child returns a handler resource: FileNodeHandler or
68. # DirectoryNodeHandler
69. return d
70.
71. def got_child(self, node_or_failure, ctx, name):
72. DEBUG = False
73. if DEBUG: print "GOT_CHILD", name, node_or_failure
74. req = IRequest(ctx)
75. method = req.method
76. nonterminal = len(req.postpath) > 1
77. t = get_arg(req, "t", "").strip()
78. if isinstance(node_or_failure, Failure):
79. f = node_or_failure
80. f.trap(NoSuchChildError)
81. # No child by this name. What should we do about it?
82. if DEBUG: print "no child", name
83. if DEBUG: print "postpath", req.postpath
84. if nonterminal:
85. if DEBUG: print " intermediate"
86. if should_create_intermediate_directories(req):
87. # create intermediate directories
88. if DEBUG: print " making intermediate directory"
89. d = self.node.create_empty_directory(name)
90. d.addCallback(make_handler_for,
91. self.client, self.node, name)
92. return d
93. else:
94. if DEBUG: print " terminal"
95. # terminal node
96. if (method,t) in [ ("POST","mkdir"), ("PUT","mkdir") ]:
97. if DEBUG: print " making final directory"
98. # final directory
99. d = self.node.create_empty_directory(name)
100. d.addCallback(make_handler_for,
101. self.client, self.node, name)
102. return d
103. if (method,t) in ( ("PUT",""), ("PUT","uri"), ):
104. if DEBUG: print " PUT, making leaf placeholder"
105. # we were trying to find the leaf filenode (to put a new
106. # file in its place), and it didn't exist. That's ok,
107. # since that's the leaf node that we're about to create.
108. # We make a dummy one, which will respond to the PUT
109. # request by replacing itself.
110. return PlaceHolderNodeHandler(self.client, self.node, name)
111. if DEBUG: print " 404"
112. # otherwise, we just return a no-such-child error
113. return f
114.
115. node = node_or_failure
116. if nonterminal and should_create_intermediate_directories(req):
117. if not IDirectoryNode.providedBy(node):
118. # we would have put a new directory here, but there was a
119. # file in the way.
120. if DEBUG: print "blocking"
121. raise WebError("Unable to create directory '%s': "
122. "a file was in the way" % name,
123. http.CONFLICT)
124. if DEBUG: print "good child"
125. return make_handler_for(node, self.client, self.node, name)
126.
127. def render_DELETE(self, ctx):
128. assert self.parentnode and self.name
129. d = self.parentnode.delete(self.name)
130. d.addCallback(lambda res: self.node.get_uri())
131. return d
132.
133. def render_GET(self, ctx):
134. req = IRequest(ctx)
135. # This is where all of the directory-related ?t=* code goes.
136. t = get_arg(req, "t", "").strip()
137. if not t:
138. # render the directory as HTML, using the docFactory and Nevow's
139. # whole templating thing.
140. return DirectoryAsHTML(self.node)
141.
142. if t == "json":
143. return DirectoryJSONMetadata(ctx, self.node)
144. if t == "info":
145. return MoreInfo(self.node)
146. if t == "uri":
147. return DirectoryURI(ctx, self.node)
148. if t == "readonly-uri":
149. return DirectoryReadonlyURI(ctx, self.node)
150. if t == 'rename-form':
151. return RenameForm(self.node)
152.
153. raise WebError("GET directory: bad t=%s" % t)
154.
155. def render_PUT(self, ctx):
156. req = IRequest(ctx)
157. t = get_arg(req, "t", "").strip()
158. replace = parse_replace_arg(get_arg(req, "replace", "true"))
159.
160. if t == "mkdir":
161. # our job was done by the traversal/create-intermediate-directory
162. # process that got us here.
163. return text_plain(self.node.get_uri(), ctx) # TODO: urlencode
164. if t == "uri":
165. if not replace:
166. # they're trying to set_uri and that name is already occupied
167. # (by us).
168. raise ExistingChildError()
169. d = self.replace_me_with_a_childcap(req, self.client, replace)
170. # TODO: results
171. return d
172.
173. raise WebError("PUT to a directory")
174.
175. def render_POST(self, ctx):
176. req = IRequest(ctx)
177. t = get_arg(req, "t", "").strip()
178.
179. if t == "mkdir":
180. d = self._POST_mkdir(req)
181. elif t == "mkdir-p":
182. # TODO: docs, tests
183. d = self._POST_mkdir_p(req)
184. elif t == "upload":
185. d = self._POST_upload(ctx) # this one needs the context
186. elif t == "uri":
187. d = self._POST_uri(req)
188. elif t == "delete":
189. d = self._POST_delete(req)
190. elif t == "rename":
191. d = self._POST_rename(req)
192. elif t == "check":
193. d = self._POST_check(req)
194. elif t == "start-deep-check":
195. d = self._POST_start_deep_check(ctx)
196. elif t == "stream-deep-check":
197. d = self._POST_stream_deep_check(ctx)
198. elif t == "start-manifest":
199. d = self._POST_start_manifest(ctx)
200. elif t == "start-deep-size":
201. d = self._POST_start_deep_size(ctx)
202. elif t == "start-deep-stats":
203. d = self._POST_start_deep_stats(ctx)
204. elif t == "stream-manifest":
205. d = self._POST_stream_manifest(ctx)
206. elif t == "set_children":
207. # TODO: docs
208. d = self._POST_set_children(req)
209. else:
210. raise WebError("POST to a directory with bad t=%s" % t)
211.
212. when_done = get_arg(req, "when_done", None)
213. if when_done:
214. d.addCallback(lambda res: url.URL.fromString(when_done))
215. return d
216.
217. def _POST_mkdir(self, req):
218. name = get_arg(req, "name", "")
219. if not name:
220. # our job is done, it was handled by the code in got_child
221. # which created the final directory (i.e. us)
222. return defer.succeed(self.node.get_uri()) # TODO: urlencode
223. name = name.decode("utf-8")
224. replace = boolean_of_arg(get_arg(req, "replace", "true"))
225. d = self.node.create_empty_directory(name, overwrite=replace)
226. d.addCallback(lambda child: child.get_uri()) # TODO: urlencode
227. return d
228.
229. def _POST_mkdir_p(self, req):
230. path = get_arg(req, "path")
231. if not path:
232. raise WebError("mkdir-p requires a path")
233. path_ = tuple([seg.decode("utf-8") for seg in path.split('/') if seg ])
234. # TODO: replace
235. d = self._get_or_create_directories(self.node, path_)
236. d.addCallback(lambda node: node.get_uri())
237. return d
238.
239. def _get_or_create_directories(self, node, path):
240. if not IDirectoryNode.providedBy(node):
241. # unfortunately it is too late to provide the name of the
242. # blocking directory in the error message.
243. raise BlockingFileError("cannot create directory because there "
244. "is a file in the way")
245. if not path:
246. return defer.succeed(node)
247. d = node.get(path[0])
248. def _maybe_create(f):
249. f.trap(NoSuchChildError)
250. return node.create_empty_directory(path[0])
251. d.addErrback(_maybe_create)
252. d.addCallback(self._get_or_create_directories, path[1:])
253. return d
254.
255. def _POST_upload(self, ctx):
256. req = IRequest(ctx)
257. charset = get_arg(req, "_charset", "utf-8")
258. contents = req.fields["file"]
259. assert contents.filename is None or isinstance(contents.filename, str)
260. name = get_arg(req, "name")
261. name = name or contents.filename
262. if name is not None:
263. name = name.strip()
264. if not name:
265. # this prohibts empty, missing, and all-whitespace filenames
266. raise WebError("upload requires a name")
267. assert isinstance(name, str)
268. name = name.decode(charset)
269. if "/" in name:
270. raise WebError("name= may not contain a slash", http.BAD_REQUEST)
271. assert isinstance(name, unicode)
272.
273. # since POST /uri/path/file?t=upload is equivalent to
274. # POST /uri/path/dir?t=upload&name=foo, just do the same thing that
275. # childFactory would do. Things are cleaner if we only do a subset of
276. # them, though, so we don't do: d = self.childFactory(ctx, name)
277.
278. d = self.node.get(name)
279. def _maybe_got_node(node_or_failure):
280. if isinstance(node_or_failure, Failure):
281. f = node_or_failure
282. f.trap(NoSuchChildError)
283. # create a placeholder which will see POST t=upload
284. return PlaceHolderNodeHandler(self.client, self.node, name)
285. else:
286. node = node_or_failure
287. return make_handler_for(node, self.client, self.node, name)
288. d.addBoth(_maybe_got_node)
289. # now we have a placeholder or a filenodehandler, and we can just
290. # delegate to it. We could return the resource back out of
291. # DirectoryNodeHandler.renderHTTP, and nevow would recurse into it,
292. # but the addCallback() that handles when_done= would break.
293. d.addCallback(lambda child: child.renderHTTP(ctx))
294. return d
295.
296. def _POST_uri(self, req):
297. childcap = get_arg(req, "uri")
298. if not childcap:
299. raise WebError("set-uri requires a uri")
300. name = get_arg(req, "name")
301. if not name:
302. raise WebError("set-uri requires a name")
303. charset = get_arg(req, "_charset", "utf-8")
304. name = name.decode(charset)
305. replace = boolean_of_arg(get_arg(req, "replace", "true"))
306. d = self.node.set_uri(name, childcap, overwrite=replace)
307. d.addCallback(lambda res: childcap)
308. return d
309.
310. def _POST_delete(self, req):
311. name = get_arg(req, "name")
312. if name is None:
313. # apparently an <input type="hidden" name="name" value="">
314. # won't show up in the resulting encoded form.. the 'name'
315. # field is completely missing. So to allow deletion of an
316. # empty file, we have to pretend that None means ''. The only
317. # downide of this is a slightly confusing error message if
318. # someone does a POST without a name= field. For our own HTML
319. # thisn't a big deal, because we create the 'delete' POST
320. # buttons ourselves.
321. name = ''
322. charset = get_arg(req, "_charset", "utf-8")
323. name = name.decode(charset)
324. d = self.node.delete(name)
325. d.addCallback(lambda res: "thing deleted")
326. return d
327.
328. def _POST_rename(self, req):
329. charset = get_arg(req, "_charset", "utf-8")
330. from_name = get_arg(req, "from_name")
331. if from_name is not None:
332. from_name = from_name.strip()
333. from_name = from_name.decode(charset)
334. assert isinstance(from_name, unicode)
335. to_name = get_arg(req, "to_name")
336. if to_name is not None:
337. to_name = to_name.strip()
338. to_name = to_name.decode(charset)
339. assert isinstance(to_name, unicode)
340. if not from_name or not to_name:
341. raise WebError("rename requires from_name and to_name")
342. if from_name == to_name:
343. return defer.succeed("redundant rename")
344.
345. # allow from_name to contain slashes, so they can fix names that were
346. # accidentally created with them. But disallow them in to_name, to
347. # discourage the practice.
348. if "/" in to_name:
349. raise WebError("to_name= may not contain a slash", http.BAD_REQUEST)
350.
351. replace = boolean_of_arg(get_arg(req, "replace", "true"))
352. d = self.node.move_child_to(from_name, self.node, to_name, replace)
353. d.addCallback(lambda res: "thing renamed")
354. return d
355.
356. def _POST_check(self, req):
357. # check this directory
358. verify = boolean_of_arg(get_arg(req, "verify", "false"))
359. repair = boolean_of_arg(get_arg(req, "repair", "false"))
360. add_lease = boolean_of_arg(get_arg(req, "add-lease", "false"))
361. if repair:
362. d = self.node.check_and_repair(Monitor(), verify, add_lease)
363. d.addCallback(lambda res: CheckAndRepairResults(self.client, res))
364. else:
365. d = self.node.check(Monitor(), verify, add_lease)
366. d.addCallback(lambda res: CheckResults(self.client, res))
367. return d
368.
369. def _start_operation(self, monitor, renderer, ctx):
370. table = IOpHandleTable(ctx)
371. table.add_monitor(ctx, monitor, renderer)
372. return table.redirect_to(ctx)
373.
374. def _POST_start_deep_check(self, ctx):
375. # check this directory and everything reachable from it
376. if not get_arg(ctx, "ophandle"):
377. raise NeedOperationHandleError("slow operation requires ophandle=")
378. verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
379. repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
380. add_lease = boolean_of_arg(get_arg(ctx, "add-lease", "false"))
381. if repair:
382. monitor = self.node.start_deep_check_and_repair(verify, add_lease)
383. renderer = DeepCheckAndRepairResults(self.client, monitor)
384. else:
385. monitor = self.node.start_deep_check(verify, add_lease)
386. renderer = DeepCheckResults(self.client, monitor)
387. return self._start_operation(monitor, renderer, ctx)
388.
389. def _POST_stream_deep_check(self, ctx):
390. verify = boolean_of_arg(get_arg(ctx, "verify", "false"))
391. repair = boolean_of_arg(get_arg(ctx, "repair", "false"))
392. add_lease = boolean_of_arg(get_arg(ctx, "add-lease", "false"))
393. walker = DeepCheckStreamer(ctx, self.node, verify, repair, add_lease)
394. monitor = self.node.deep_traverse(walker)
395. walker.setMonitor(monitor)
396. # register to hear stopProducing. The walker ignores pauseProducing.
397. IRequest(ctx).registerProducer(walker, True)
398. d = monitor.when_done()
399. def _done(res):
400. IRequest(ctx).unregisterProducer()
401. return res
402. d.addBoth(_done)
403. def _cancelled(f):
404. f.trap(OperationCancelledError)
405. return "Operation Cancelled"
406. d.addErrback(_cancelled)
407. def _error(f):
408. # signal the error as a non-JSON "ERROR:" line, plus exception
409. msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
410. ", ".join([str(a) for a in f.value.args]))
411. msg += str(f)
412. return msg
413. d.addErrback(_error)
414. return d
415.
416. def _POST_start_manifest(self, ctx):
417. if not get_arg(ctx, "ophandle"):
418. raise NeedOperationHandleError("slow operation requires ophandle=")
419. monitor = self.node.build_manifest()
420. renderer = ManifestResults(self.client, monitor)
421. return self._start_operation(monitor, renderer, ctx)
422.
423. def _POST_start_deep_size(self, ctx):
424. if not get_arg(ctx, "ophandle"):
425. raise NeedOperationHandleError("slow operation requires ophandle=")
426. monitor = self.node.start_deep_stats()
427. renderer = DeepSizeResults(self.client, monitor)
428. return self._start_operation(monitor, renderer, ctx)
429.
430. def _POST_start_deep_stats(self, ctx):
431. if not get_arg(ctx, "ophandle"):
432. raise NeedOperationHandleError("slow operation requires ophandle=")
433. monitor = self.node.start_deep_stats()
434. renderer = DeepStatsResults(self.client, monitor)
435. return self._start_operation(monitor, renderer, ctx)
436.
437. def _POST_stream_manifest(self, ctx):
438. walker = ManifestStreamer(ctx, self.node)
439. monitor = self.node.deep_traverse(walker)
440. walker.setMonitor(monitor)
441. # register to hear stopProducing. The walker ignores pauseProducing.
442. IRequest(ctx).registerProducer(walker, True)
443. d = monitor.when_done()
444. def _done(res):
445. IRequest(ctx).unregisterProducer()
446. return res
447. d.addBoth(_done)
448. def _cancelled(f):
449. f.trap(OperationCancelledError)
450. return "Operation Cancelled"
451. d.addErrback(_cancelled)
452. def _error(f):
453. # signal the error as a non-JSON "ERROR:" line, plus exception
454. msg = "ERROR: %s(%s)\n" % (f.value.__class__.__name__,
455. ", ".join([str(a) for a in f.value.args]))
456. msg += str(f)
457. return msg
458. d.addErrback(_error)
459. return d
460.
461. def _POST_set_children(self, req):
462. replace = boolean_of_arg(get_arg(req, "replace", "true"))
463. req.content.seek(0)
464. body = req.content.read()
465. try:
466. children = simplejson.loads(body)
467. except ValueError, le:
468. le.args = tuple(le.args + (body,))
469. # TODO test handling of bad JSON
470. raise
471. cs = []
472. for name, (file_or_dir, mddict) in children.iteritems():
473. name = unicode(name) # simplejson-2.0.1 returns str *or* unicode
474. cap = str(mddict.get('rw_uri') or mddict.get('ro_uri'))
475. cs.append((name, cap, mddict.get('metadata')))
476. d = self.node.set_children(cs, replace)
477. d.addCallback(lambda res: "Okay so I did it.")
478. # TODO: results
479. return d
480.
481. def abbreviated_dirnode(dirnode):
482. u = from_string_dirnode(dirnode.get_uri())
483. return u.abbrev_si()
484.
485. class DirectoryAsHTML(rend.Page):
486. # The remainder of this class is to render the directory into
487. # human+browser -oriented HTML.
488. docFactory = getxmlfile("directory.xhtml")
489. addSlash = True
490.
491. def __init__(self, node):
492. rend.Page.__init__(self)
493. self.node = node
494.
495. def beforeRender(self, ctx):
496. # attempt to get the dirnode's children, stashing them (or the
497. # failure that results) for later use
498. d = self.node.list()
499. def _good(children):
500. # Deferreds don't optimize out tail recursion, and the way
501. # Nevow's flattener handles Deferreds doesn't take this into
502. # account. As a result, large lists of Deferreds that fire in the
503. # same turn (i.e. the output of defer.succeed) will cause a stack
504. # overflow. To work around this, we insert a turn break after
505. # every 100 items, using foolscap's fireEventually(). This gives
506. # the stack a chance to be popped. It would also work to put
507. # every item in its own turn, but that'd be a lot more
508. # inefficient. This addresses ticket #237, for which I was never
509. # able to create a failing unit test.
510. output = []
511. for i,item in enumerate(sorted(children.items())):
512. if i % 100 == 0:
513. output.append(fireEventually(item))
514. else:
515. output.append(item)
516. self.dirnode_children = output
517. return ctx
518. def _bad(f):
519. text, code = humanize_failure(f)
520. self.dirnode_children = None
521. self.dirnode_children_error = text
522. return ctx
523. d.addCallbacks(_good, _bad)
524. return d
525.
526. def render_title(self, ctx, data):
527. si_s = abbreviated_dirnode(self.node)
528. header = ["TahoeLAFS - Directory SI=%s" % si_s]
529. if self.node.is_readonly():
530. header.append(" (read-only)")
531. else:
532. header.append(" (modifiable)")
533. return ctx.tag[header]
534.
535. def render_header(self, ctx, data):
536. si_s = abbreviated_dirnode(self.node)
537. header = ["TahoeLAFS Directory SI=", T.span(class_="data-chars")[si_s]]
538. if self.node.is_readonly():
539. header.append(" (read-only)")
540. return ctx.tag[header]
541.
542. def render_welcome(self, ctx, data):
543. link = get_root(ctx)
544. return T.div[T.a(href=link)["Return to Welcome page"]]
545.
546. def render_show_readonly(self, ctx, data):
547. if self.node.is_readonly():
548. return ""
549. rocap = self.node.get_readonly_uri()
550. root = get_root(ctx)
551. uri_link = "%s/uri/%s/" % (root, urllib.quote(rocap))
552. return ctx.tag[T.a(href=uri_link)["Read-Only Version"]]
553.
554. def render_try_children(self, ctx, data):
555. # if the dirnode can be retrived, render a table of children.
556. # Otherwise, render an apologetic error message.
557. if self.dirnode_children is not None:
558. return ctx.tag
559. else:
560. return T.div[T.p["Error reading directory:"],
561. T.p[self.dirnode_children_error]]
562.
563. def data_children(self, ctx, data):
564. return self.dirnode_children
565.
566. def render_row(self, ctx, data):
567. name, (target, metadata) = data
568. name = name.encode("utf-8")
569. assert not isinstance(name, unicode)
570. nameurl = urllib.quote(name, safe="") # encode any slashes too
571.
572. root = get_root(ctx)
573. here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri()))
574. if self.node.is_readonly():
575. delete = "-"
576. rename = "-"
577. else:
578. # this creates a button which will cause our child__delete method
579. # to be invoked, which deletes the file and then redirects the
580. # browser back to this directory
581. delete = T.form(action=here, method="post")[
582. T.input(type='hidden', name='t', value='delete'),
583. T.input(type='hidden', name='name', value=name),
584. T.input(type='hidden', name='when_done', value="."),
585. T.input(type='submit', value='del', name="del"),
586. ]
587.
588. rename = T.form(action=here, method="get")[
589. T.input(type='hidden', name='t', value='rename-form'),
590. T.input(type='hidden', name='name', value=name),
591. T.input(type='hidden', name='when_done', value="."),
592. T.input(type='submit', value='rename', name="rename"),
593. ]
594.
595. ctx.fillSlots("delete", delete)
596. ctx.fillSlots("rename", rename)
597.
598. times = []
599. linkcrtime = metadata.get('tahoe', {}).get("linkcrtime")
600. if linkcrtime is not None:
601. times.append("lcr: " + time_format.iso_local(linkcrtime))
602. else:
603. # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
604. if "ctime" in metadata:
605. ctime = time_format.iso_local(metadata["ctime"])
606. times.append("c: " + ctime)
607. linkmotime = metadata.get('tahoe', {}).get("linkmotime")
608. if linkmotime is not None:
609. if times:
610. times.append(T.br())
611. times.append("lmo: " + time_format.iso_local(linkmotime))
612. else:
613. # For backwards-compatibility with links last modified by Tahoe < 1.4.0:
614. if "mtime" in metadata:
615. mtime = time_format.iso_local(metadata["mtime"])
616. if times:
617. times.append(T.br())
618. times.append("m: " + mtime)
619. ctx.fillSlots("times", times)
620.
621. assert IFilesystemNode.providedBy(target), target
622. writecap = target.get_uri() or ""
623. quoted_uri = urllib.quote(writecap, safe="") # escape slashes too
624.
625. if IMutableFileNode.providedBy(target):
626. # to prevent javascript in displayed .html files from stealing a
627. # secret directory URI from the URL, send the browser to a URI-based
628. # page that doesn't know about the directory at all
629. dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
630.
631. ctx.fillSlots("filename",
632. T.a(href=dlurl)[html.escape(name)])
633. ctx.fillSlots("type", "SSK")
634.
635. ctx.fillSlots("size", "?")
636.
637. info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
638.
639. elif IFileNode.providedBy(target):
640. dlurl = "%s/file/%s/@@named=/%s" % (root, quoted_uri, nameurl)
641.
642. ctx.fillSlots("filename",
643. T.a(href=dlurl)[html.escape(name)])
644. ctx.fillSlots("type", "FILE")
645.
646. ctx.fillSlots("size", target.get_size())
647.
648. info_link = "%s/uri/%s?t=info" % (root, quoted_uri)
649.
650. elif IDirectoryNode.providedBy(target):
651. # directory
652. uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap))
653. ctx.fillSlots("filename",
654. T.a(href=uri_link)[html.escape(name)])
655. if target.is_readonly():
656. dirtype = "DIR-RO"
657. else:
658. dirtype = "DIR"
659. ctx.fillSlots("type", dirtype)
660. ctx.fillSlots("size", "-")
661. info_link = "%s/uri/%s/?t=info" % (root, quoted_uri)
662.
663. else:
664. # unknown
665. ctx.fillSlots("filename", html.escape(name))
666. ctx.fillSlots("type", "?")
667. ctx.fillSlots("size", "-")
668. # use a directory-relative info link, so we can extract both the
669. # writecap and the readcap
670. info_link = "%s?t=info" % urllib.quote(name)
671.
672. ctx.fillSlots("info", T.a(href=info_link)["More Info"])
673.
674. return ctx.tag
675.
676. def render_forms(self, ctx, data):
677. forms = []
678.
679. if self.node.is_readonly():
680. return T.div["No upload forms: directory is read-only"]
681. if self.dirnode_children is None:
682. return T.div["No upload forms: directory is unreadable"]
683.
684. mkdir = T.form(action=".", method="post",
685. enctype="multipart/form-data")[
686. T.fieldset[
687. T.input(type="hidden", name="t", value="mkdir"),
688. T.input(type="hidden", name="when_done", value="."),
689. T.legend(class_="freeform-form-label")["Create a new directory in this directory"],
690. "New directory name: ",
691. T.input(type="text", name="name"), " ",
692. T.input(type="submit", value="Create"),
693. ]]
694. forms.append(T.div(class_="freeform-form")[mkdir])
695.
696. upload = T.form(action=".", method="post",
697. enctype="multipart/form-data")[
698. T.fieldset[
699. T.input(type="hidden", name="t", value="upload"),
700. T.input(type="hidden", name="when_done", value="."),
701. T.legend(class_="freeform-form-label")["Upload a file to this directory"],
702. "Choose a file to upload: ",
703. T.input(type="file", name="file", class_="freeform-input-file"),
704. " ",
705. T.input(type="submit", value="Upload"),
706. " Mutable?:",
707. T.input(type="checkbox", name="mutable"),
708. ]]
709. forms.append(T.div(class_="freeform-form")[upload])
710.
711. mount = T.form(action=".", method="post",
712. enctype="multipart/form-data")[
713. T.fieldset[
714. T.input(type="hidden", name="t", value="uri"),
715. T.input(type="hidden", name="when_done", value="."),
716. T.legend(class_="freeform-form-label")["Add a link to a file or directory which is already in TahoeLAFS."],
717. "New child name: ",
718. T.input(type="text", name="name"), " ",
719. "URI of new child: ",
720. T.input(type="text", name="uri"), " ",
721. T.input(type="submit", value="Attach"),
722. ]]
723. forms.append(T.div(class_="freeform-form")[mount])
724. return forms
725.
726. def render_results(self, ctx, data):
727. req = IRequest(ctx)
728. return get_arg(req, "results", "")
729.
730.
731. def DirectoryJSONMetadata(ctx, dirnode):
732. d = dirnode.list()
733. def _got(children):
734. kids = {}
735. for name, (childnode, metadata) in children.iteritems():
736. assert IFilesystemNode.providedBy(childnode), childnode
737. rw_uri = childnode.get_uri()
738. ro_uri = childnode.get_readonly_uri()
739. if (IDirectoryNode.providedBy(childnode)
740. or IFileNode.providedBy(childnode)):
741. if childnode.is_readonly():
742. rw_uri = None
743. if IFileNode.providedBy(childnode):
744. kiddata = ("filenode", {'size': childnode.get_size(),
745. 'mutable': childnode.is_mutable(),
746. })
747. elif IDirectoryNode.providedBy(childnode):
748. kiddata = ("dirnode", {'mutable': childnode.is_mutable(),
749. })
750. else:
751. kiddata = ("unknown", {})
752. kiddata[1]["metadata"] = metadata
753. if ro_uri:
754. kiddata[1]["ro_uri"] = ro_uri
755. if rw_uri:
756. kiddata[1]["rw_uri"] = rw_uri
757. verifycap = childnode.get_verify_cap()
758. if verifycap:
759. kiddata[1]['verify_uri'] = verifycap.to_string()
760. kids[name] = kiddata
761. if dirnode.is_readonly():
762. drw_uri = None
763. dro_uri = dirnode.get_uri()
764. else:
765. drw_uri = dirnode.get_uri()
766. dro_uri = dirnode.get_readonly_uri()
767. contents = { 'children': kids }
768. if dro_uri:
769. contents['ro_uri'] = dro_uri
770. if drw_uri:
771. contents['rw_uri'] = drw_uri
772. verifycap = dirnode.get_verify_cap()
773. if verifycap:
774. contents['verify_uri'] = verifycap.to_string()
775. contents['mutable'] = dirnode.is_mutable()
776. data = ("dirnode", contents)
777. return simplejson.dumps(data, indent=1) + "\n"
778. d.addCallback(_got)
779. d.addCallback(text_plain, ctx)
780. return d
781.
782.
783.
784. def DirectoryURI(ctx, dirnode):
785. return text_plain(dirnode.get_uri(), ctx)
786.
787. def DirectoryReadonlyURI(ctx, dirnode):
788. return text_plain(dirnode.get_readonly_uri(), ctx)
789.
790. class RenameForm(rend.Page):
791. addSlash = True
792. docFactory = getxmlfile("rename-form.xhtml")
793.
794. def render_title(self, ctx, data):
795. return ctx.tag["Directory SI=%s" % abbreviated_dirnode(self.original)]
796.
797. def render_header(self, ctx, data):
798. header = ["Rename "
799. "in directory SI=%s" % abbreviated_dirnode(self.original),
800. ]
801.
802. if self.original.is_readonly():
803. header.append(" (readonly!)")
804. header.append(":")
805. return ctx.tag[header]
806.
807. def render_when_done(self, ctx, data):
808. return T.input(type="hidden", name="when_done", value=".")
809.
810. def render_get_name(self, ctx, data):
811. req = IRequest(ctx)
812. name = get_arg(req, "name", "")
813. ctx.tag.attributes['value'] = name
814. return ctx.tag
815.
816.
817. class ManifestResults(rend.Page, ReloadMixin):
818. docFactory = getxmlfile("manifest.xhtml")
819.
820. def __init__(self, client, monitor):
821. self.client = client
822. self.monitor = monitor
823.
824. def renderHTTP(self, ctx):
825. req = inevow.IRequest(ctx)
826. output = get_arg(req, "output", "html").lower()
827. if output == "text":
828. return self.text(req)
829. if output == "json":
830. return self.json(req)
831. return rend.Page.renderHTTP(self, ctx)
832.
833. def slashify_path(self, path):
834. if not path:
835. return ""
836. return "/".join([p.encode("utf-8") for p in path])
837.
838. def text(self, req):
839. req.setHeader("content-type", "text/plain")
840. lines = []
841. is_finished = self.monitor.is_finished()
842. lines.append("finished: " + {True: "yes", False: "no"}[is_finished])
843. for (path, cap) in self.monitor.get_status()["manifest"]:
844. lines.append(self.slashify_path(path) + " " + cap)
845. return "\n".join(lines) + "\n"
846.
847. def json(self, req):
848. req.setHeader("content-type", "text/plain")
849. m = self.monitor
850. s = m.get_status()
851.
852. status = { "stats": s["stats"],
853. "finished": m.is_finished(),
854. "origin": base32.b2a(m.origin_si),
855. }
856. if m.is_finished():
857. # don't return manifest/verifycaps/SIs unless the operation is
858. # done, to save on CPU/memory (both here and in the HTTP client
859. # who has to unpack the JSON). Tests show that the ManifestWalker
860. # needs about 1092 bytes per item, the JSON we generate here
861. # requires about 503 bytes per item, and some internal overhead
862. # (perhaps transport-layer buffers in twisted.web?) requires an
863. # additional 1047 bytes per item.
864. status.update({ "manifest": s["manifest"],
865. "verifycaps": [i for i in s["verifycaps"]],
866. "storage-index": [i for i in s["storage-index"]],
867. })
868. # simplejson doesn't know how to serialize a set. We use a
869. # generator that walks the set rather than list(setofthing) to
870. # save a small amount of memory (4B*len) and a moderate amount of
871. # CPU.
872. return simplejson.dumps(status, indent=1)
873.
874. def _si_abbrev(self):
875. return base32.b2a(self.monitor.origin_si)[:6]
876.
877. def render_title(self, ctx):
878. return T.title["Manifest of SI=%s" % self._si_abbrev()]
879.
880. def render_header(self, ctx):
881. return T.p["Manifest of SI=%s" % self._si_abbrev()]
882.
883. def data_items(self, ctx, data):
884. return self.monitor.get_status()["manifest"]
885.
886. def render_row(self, ctx, (path, cap)):
887. ctx.fillSlots("path", self.slashify_path(path))
888. root = get_root(ctx)
889. # TODO: we need a clean consistent way to get the type of a cap string
890. if cap:
891. if cap.startswith("URI:CHK") or cap.startswith("URI:SSK"):
892. nameurl = urllib.quote(path[-1].encode("utf-8"))
893. uri_link = "%s/file/%s/@@named=/%s" % (root, urllib.quote(cap),
894. nameurl)
895. else:
896. uri_link = "%s/uri/%s" % (root, urllib.quote(cap, safe=""))
897. ctx.fillSlots("cap", T.a(href=uri_link)[cap])
898. else:
899. ctx.fillSlots("cap", "")
900. return ctx.tag
901.
902. class DeepSizeResults(rend.Page):
903. def __init__(self, client, monitor):
904. self.client = client
905. self.monitor = monitor
906.
907. def renderHTTP(self, ctx):
908. req = inevow.IRequest(ctx)
909. output = get_arg(req, "output", "html").lower()
910. req.setHeader("content-type", "text/plain")
911. if output == "json":
912. return self.json(req)
913. # plain text
914. is_finished = self.monitor.is_finished()
915. output = "finished: " + {True: "yes", False: "no"}[is_finished] + "\n"
916. if is_finished:
917. stats = self.monitor.get_status()
918. total = (stats.get("size-immutable-files", 0)
919. + stats.get("size-mutable-files", 0)
920. + stats.get("size-directories", 0))
921. output += "size: %d\n" % total
922. return output
923.
924. def json(self, req):
925. status = {"finished": self.monitor.is_finished(),
926. "size": self.monitor.get_status(),
927. }
928. return simplejson.dumps(status)
929.
930. class DeepStatsResults(rend.Page):
931. def __init__(self, client, monitor):
932. self.client = client
933. self.monitor = monitor
934.
935. def renderHTTP(self, ctx):
936. # JSON only
937. inevow.IRequest(ctx).setHeader("content-type", "text/plain")
938. s = self.monitor.get_status().copy()
939. s["finished"] = self.monitor.is_finished()
940. return simplejson.dumps(s, indent=1)
941.
942. class ManifestStreamer(dirnode.DeepStats):
943. implements(IPushProducer)
944.
945. def __init__(self, ctx, origin):
946. dirnode.DeepStats.__init__(self, origin)
947. self.req = IRequest(ctx)
948.
949. def setMonitor(self, monitor):
950. self.monitor = monitor
951. def pauseProducing(self):
952. pass
953. def resumeProducing(self):
954. pass
955. def stopProducing(self):
956. self.monitor.cancel()
957.
958. def add_node(self, node, path):
959. dirnode.DeepStats.add_node(self, node, path)
960. d = {"path": path,
961. "cap": node.get_uri()}
962.
963. if IDirectoryNode.providedBy(node):
964. d["type"] = "directory"
965. elif IFileNode.providedBy(node):
966. d["type"] = "file"
967. else:
968. d["type"] = "unknown"
969.
970. v = node.get_verify_cap()
971. if v:
972. v = v.to_string()
973. d["verifycap"] = v
974.
975. r = node.get_repair_cap()
976. if r:
977. r = r.to_string()
978. d["repaircap"] = r
979.
980. si = node.get_storage_index()
981. if si:
982. si = base32.b2a(si)
983. d["storage-index"] = si
984.
985. j = simplejson.dumps(d, ensure_ascii=True)
986. assert "\n" not in j
987. self.req.write(j+"\n")
988.
989. def finish(self):
990. stats = dirnode.DeepStats.get_results(self)
991. d = {"type": "stats",
992. "stats": stats,
993. }
994. j = simplejson.dumps(d, ensure_ascii=True)
995. assert "\n" not in j
996. self.req.write(j+"\n")
997. return ""
998.
999. class DeepCheckStreamer(dirnode.DeepStats):
1000. implements(IPushProducer)
1001.
1002. def __init__(self, ctx, origin, verify, repair, add_lease):
1003. dirnode.DeepStats.__init__(self, origin)
1004. self.req = IRequest(ctx)
1005. self.verify = verify
1006. self.repair = repair
1007. self.add_lease = add_lease
1008.
1009. def setMonitor(self, monitor):
1010. self.monitor = monitor
1011. def pauseProducing(self):
1012. pass
1013. def resumeProducing(self):
1014. pass
1015. def stopProducing(self):
1016. self.monitor.cancel()
1017.
1018. def add_node(self, node, path):
1019. dirnode.DeepStats.add_node(self, node, path)
1020. data = {"path": path,
1021. "cap": node.get_uri()}
1022.
1023. if IDirectoryNode.providedBy(node):
1024. data["type"] = "directory"
1025. else:
1026. data["type"] = "file"
1027.
1028. v = node.get_verify_cap()
1029. if v:
1030. v = v.to_string()
1031. data["verifycap"] = v
1032.
1033. r = node.get_repair_cap()
1034. if r:
1035. r = r.to_string()
1036. data["repaircap"] = r
1037.
1038. si = node.get_storage_index()
1039. if si:
1040. si = base32.b2a(si)
1041. data["storage-index"] = si
1042.
1043. if self.repair:
1044. d = node.check_and_repair(self.monitor, self.verify, self.add_lease)
1045. d.addCallback(self.add_check_and_repair, data)
1046. else:
1047. d = node.check(self.monitor, self.verify, self.add_lease)
1048. d.addCallback(self.add_check, data)
1049. d.addCallback(self.write_line)
1050. return d
1051.
1052. def add_check_and_repair(self, crr, data):
1053. data["check-and-repair-results"] = json_check_and_repair_results(crr)
1054. return data
1055.
1056. def add_check(self, cr, data):
1057. data["check-results"] = json_check_results(cr)
1058. return data
1059.
1060. def write_line(self, data):
1061. j = simplejson.dumps(data, ensure_ascii=True)
1062. assert "\n" not in j
1063. self.req.write(j+"\n")
1064.
1065. def finish(self):
1066. stats = dirnode.DeepStats.get_results(self)
1067. d = {"type": "stats",
1068. "stats": stats,
1069. }
1070. j = simplejson.dumps(d, ensure_ascii=True)
1071. assert "\n" not in j
1072. self.req.write(j+"\n")
1073. return ""
1074.
1075. class UnknownNodeHandler(RenderMixin, rend.Page):
1076.
1077. def __init__(self, client, node, parentnode=None, name=None):
1078. rend.Page.__init__(self)
1079. assert node
1080. self.node = node
1081.
1082. def render_GET(self, ctx):
1083. req = IRequest(ctx)
1084. t = get_arg(req, "t", "").strip()
1085. if t == "info":
1086. return MoreInfo(self.node)
1087. raise WebError("GET unknown URI type: can only do t=info, not t=%s" % t)
1088.
1089.