mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-04-13 22:03:04 +00:00
CLI: add 'tahoe manifest', which takes a directory and returns a list of things you can reach from it
This commit is contained in:
parent
c9175de0f2
commit
0d93d6244e
15
docs/CLI.txt
15
docs/CLI.txt
@ -325,6 +325,21 @@ tahoe mv tahoe:uploaded.txt fun:uploaded.txt
|
||||
These move a file from your tahoe root directory to the virtual directory
|
||||
set up earlier with "tahoe add-alias fun DIRCAP"
|
||||
|
||||
== Virtual Drive Maintenance ==
|
||||
|
||||
tahoe manifest tahoe:
|
||||
tahoe manifest --storage-index tahoe:
|
||||
|
||||
This performs a recursive walk of the given directory, visiting every file
|
||||
and directory that can be reached from that point. It then emits one line to
|
||||
stdout for each object it encounters.
|
||||
|
||||
The default behavior is to print the access cap string (like URI:CHK:.. or
|
||||
URI:DIR2:..), followed by a space, followed by the full path name.
|
||||
|
||||
If --storage-index is added, each line will instead contain the object's
|
||||
storage index. This (string) value is useful to determine which share files
|
||||
(on the server) are associated with this directory tree.
|
||||
|
||||
|
||||
== Debugging ==
|
||||
|
@ -197,6 +197,18 @@ class WebopenOptions(VDriveOptions):
|
||||
|
||||
longdesc = """Opens a webbrowser to the contents of some portion of the virtual drive."""
|
||||
|
||||
class ManifestOptions(VDriveOptions):
|
||||
optFlags = [
|
||||
("storage-index", "s", "Only print storage index strings, not pathname+cap"),
|
||||
]
|
||||
def parseArgs(self, where=''):
|
||||
self.where = where
|
||||
|
||||
def getSynopsis(self):
|
||||
return "%s manifest [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
|
||||
|
||||
longdesc = """Print a list of all files/directories reachable from the given starting point."""
|
||||
|
||||
subCommands = [
|
||||
["mkdir", None, MakeDirectoryOptions, "Create a new directory"],
|
||||
["add-alias", None, AddAliasOptions, "Add a new alias cap"],
|
||||
@ -210,6 +222,7 @@ subCommands = [
|
||||
["mv", None, MvOptions, "Move a file within the virtual drive."],
|
||||
["ln", None, LnOptions, "Make an additional link to an existing file."],
|
||||
["webopen", None, WebopenOptions, "Open a webbrowser to the root_dir"],
|
||||
["manifest", None, ManifestOptions, "List all files/dirs in a subtree"],
|
||||
]
|
||||
|
||||
def mkdir(options):
|
||||
@ -281,6 +294,11 @@ def webopen(options, opener=None):
|
||||
rc = tahoe_webopen.webopen(options, opener=opener)
|
||||
return rc
|
||||
|
||||
def manifest(options):
|
||||
from allmydata.scripts import tahoe_manifest
|
||||
rc = tahoe_manifest.manifest(options)
|
||||
return rc
|
||||
|
||||
dispatch = {
|
||||
"mkdir": mkdir,
|
||||
"add-alias": add_alias,
|
||||
@ -294,5 +312,6 @@ dispatch = {
|
||||
"mv": mv,
|
||||
"ln": ln,
|
||||
"webopen": webopen,
|
||||
"manifest": manifest,
|
||||
}
|
||||
|
||||
|
91
src/allmydata/scripts/tahoe_manifest.py
Normal file
91
src/allmydata/scripts/tahoe_manifest.py
Normal file
@ -0,0 +1,91 @@
|
||||
|
||||
import os, time
|
||||
from allmydata.scripts.common import get_alias, DEFAULT_ALIAS, escape_path
|
||||
from allmydata.scripts.common_http import do_http
|
||||
from allmydata.util import base32
|
||||
from allmydata import uri
|
||||
import urllib
|
||||
import simplejson
|
||||
|
||||
class ManifestGrabber:
|
||||
def run(self, options):
|
||||
stderr = options.stderr
|
||||
self.options = options
|
||||
self.ophandle = ophandle = base32.b2a(os.urandom(16))
|
||||
nodeurl = options['node-url']
|
||||
if not nodeurl.endswith("/"):
|
||||
nodeurl += "/"
|
||||
self.nodeurl = nodeurl
|
||||
where = options.where
|
||||
rootcap, path = get_alias(options.aliases, where, DEFAULT_ALIAS)
|
||||
if path == '/':
|
||||
path = ''
|
||||
url = nodeurl + "uri/%s" % urllib.quote(rootcap)
|
||||
if path:
|
||||
url += "/" + escape_path(path)
|
||||
# todo: should it end with a slash?
|
||||
url += "?t=start-manifest&ophandle=" + ophandle
|
||||
resp = do_http("POST", url)
|
||||
if resp.status not in (200, 302):
|
||||
print >>stderr, "ERROR", resp.status, resp.reason, resp.read()
|
||||
return 1
|
||||
# now we poll for results. We nominally poll at t=1, 5, 10, 30, 60,
|
||||
# 90, k*120 seconds, but if the poll takes non-zero time, that will
|
||||
# be slightly longer. I'm not worried about trying to make up for
|
||||
# that time.
|
||||
|
||||
return self.wait_for_results()
|
||||
|
||||
def poll_times(self):
|
||||
for i in (1,5,10,30,60,90):
|
||||
yield i
|
||||
i = 120
|
||||
while True:
|
||||
yield i
|
||||
i += 120
|
||||
|
||||
def wait_for_results(self):
|
||||
last = 0
|
||||
for next in self.poll_times():
|
||||
delay = next - last
|
||||
time.sleep(delay)
|
||||
last = next
|
||||
if self.poll():
|
||||
return 0
|
||||
|
||||
def poll(self):
|
||||
url = self.nodeurl + "operations/" + self.ophandle
|
||||
url += "?t=status&output=JSON&release-after-complete=true"
|
||||
stdout = self.options.stdout
|
||||
stderr = self.options.stderr
|
||||
resp = do_http("GET", url)
|
||||
if resp.status != 200:
|
||||
print >>stderr, "ERROR", resp.status, resp.reason, resp.read()
|
||||
return True
|
||||
data = simplejson.loads(resp.read())
|
||||
if not data["finished"]:
|
||||
return False
|
||||
self.write_results(data)
|
||||
return True
|
||||
|
||||
def write_results(self, data):
|
||||
stdout = self.options.stdout
|
||||
stderr = self.options.stderr
|
||||
if self.options["storage-index"]:
|
||||
for (path, cap) in data["manifest"]:
|
||||
u = uri.from_string(str(cap))
|
||||
si = u.get_storage_index()
|
||||
if si is not None:
|
||||
print >>stdout, base32.b2a(si)
|
||||
else:
|
||||
for (path, cap) in data["manifest"]:
|
||||
try:
|
||||
print >>stdout, cap, "/".join(path)
|
||||
except UnicodeEncodeError:
|
||||
print >>stdout, cap, "/".join([p.encode("utf-8")
|
||||
for p in path])
|
||||
|
||||
|
||||
|
||||
def manifest(options):
|
||||
return ManifestGrabber().run(options)
|
@ -2068,6 +2068,7 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
|
||||
d.addCallback(self.do_stats)
|
||||
d.addCallback(self.do_test_check_good)
|
||||
d.addCallback(self.do_test_web_good)
|
||||
d.addCallback(self.do_test_cli_good)
|
||||
d.addErrback(self.explain_web_error)
|
||||
d.addErrback(self.explain_error)
|
||||
return d
|
||||
@ -2345,6 +2346,50 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
|
||||
|
||||
return d
|
||||
|
||||
def _run_cli(self, argv, stdin=""):
|
||||
#print "CLI:", argv
|
||||
stdout, stderr = StringIO(), StringIO()
|
||||
d = threads.deferToThread(runner.runner, argv, run_by_human=False,
|
||||
stdin=StringIO(stdin),
|
||||
stdout=stdout, stderr=stderr)
|
||||
def _done(res):
|
||||
return stdout.getvalue(), stderr.getvalue()
|
||||
d.addCallback(_done)
|
||||
return d
|
||||
|
||||
def do_test_cli_good(self, ignored):
|
||||
d = self._run_cli(["manifest", "-u", self.webish_url, self.root_uri])
|
||||
def _check((out,err)):
|
||||
lines = [l for l in out.split("\n") if l]
|
||||
self.failUnlessEqual(len(lines), 4)
|
||||
caps = {}
|
||||
for l in lines:
|
||||
try:
|
||||
cap, path = l.split(None, 1)
|
||||
except ValueError:
|
||||
cap = l.strip()
|
||||
path = ""
|
||||
caps[cap] = path
|
||||
self.failUnless(self.root.get_uri() in caps)
|
||||
self.failUnlessEqual(caps[self.root.get_uri()], "")
|
||||
self.failUnlessEqual(caps[self.mutable.get_uri()], "mutable")
|
||||
self.failUnlessEqual(caps[self.large.get_uri()], "large")
|
||||
self.failUnlessEqual(caps[self.small.get_uri()], "small")
|
||||
d.addCallback(_check)
|
||||
|
||||
d.addCallback(lambda res:
|
||||
self._run_cli(["manifest", "-u", self.webish_url,
|
||||
"--storage-index", self.root_uri]))
|
||||
def _check2((out,err)):
|
||||
lines = [l for l in out.split("\n") if l]
|
||||
self.failUnlessEqual(len(lines), 3)
|
||||
self.failUnless(base32.b2a(self.root.get_storage_index()) in lines)
|
||||
self.failUnless(base32.b2a(self.mutable.get_storage_index()) in lines)
|
||||
self.failUnless(base32.b2a(self.large.get_storage_index()) in lines)
|
||||
d.addCallback(_check2)
|
||||
return d
|
||||
|
||||
|
||||
class DeepCheckWebBad(DeepCheckBase, unittest.TestCase):
|
||||
|
||||
def test_bad(self):
|
||||
|
@ -38,6 +38,9 @@ class _BaseURI:
|
||||
def to_human_encoding(self):
|
||||
return 'http://127.0.0.1:8123/uri/'+self.to_string()
|
||||
|
||||
def get_storage_index(self):
|
||||
return self.storage_index
|
||||
|
||||
class CHKFileURI(_BaseURI):
|
||||
implements(IURI, IFileURI)
|
||||
|
||||
@ -179,6 +182,8 @@ class LiteralFileURI(_BaseURI):
|
||||
return False
|
||||
def get_readonly(self):
|
||||
return self
|
||||
def get_storage_index(self):
|
||||
return None
|
||||
|
||||
def get_verifier(self):
|
||||
# LIT files need no verification, all the data is present in the URI
|
||||
@ -359,6 +364,9 @@ class _NewDirectoryBaseURI(_BaseURI):
|
||||
def get_verifier(self):
|
||||
return NewDirectoryURIVerifier(self._filenode_uri.get_verifier())
|
||||
|
||||
def get_storage_index(self):
|
||||
return self._filenode_uri.get_storage_index()
|
||||
|
||||
class NewDirectoryURI(_NewDirectoryBaseURI):
|
||||
implements(INewDirectoryURI)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user