source file: /home/buildslave/tahoe/edgy/build/src/allmydata/scripts/cli.py
file stats: 256 lines, 252 executed: 98.4% covered
coverage versus previous test: 0 lines added, 0 lines removed
1. import os.path, re, sys, fnmatch
2. from twisted.python import usage
3. from allmydata.scripts.common import BaseOptions, get_aliases
4.
5. NODEURL_RE=re.compile("http://([^:]*)(:([1-9][0-9]*))?")
6.
7. class VDriveOptions(BaseOptions, usage.Options):
8. optParameters = [
9. ["node-directory", "d", "~/.tahoe",
10. "Look here to find out which Tahoe node should be used for all "
11. "operations. The directory should either contain a full Tahoe node, "
12. "or a file named node.url which points to some other Tahoe node. "
13. "It should also contain a file named private/aliases which contains "
14. "the mapping from alias name to root dirnode URI."
15. ],
16. ["node-url", "u", None,
17. "URL of the tahoe node to use, a URL like \"http://127.0.0.1:3456\". "
18. "This overrides the URL found in the --node-directory ."],
19. ["dir-cap", None, None,
20. "Which dirnode URI should be used as the 'tahoe' alias."]
21. ]
22.
23. def postOptions(self):
24. # compute a node-url from the existing options, put in self['node-url']
25. if self['node-directory']:
26. if sys.platform == 'win32' and self['node-directory'] == '~/.tahoe':
27. from allmydata.windows import registry
28. self['node-directory'] = registry.get_base_dir_path()
29. else:
30. self['node-directory'] = os.path.expanduser(self['node-directory'])
31. if self['node-url']:
32. if (not isinstance(self['node-url'], basestring)
33. or not NODEURL_RE.match(self['node-url'])):
34. msg = ("--node-url is required to be a string and look like "
35. "\"http://HOSTNAMEORADDR:PORT\", not: %r" %
36. (self['node-url'],))
37. raise usage.UsageError(msg)
38. else:
39. node_url_file = os.path.join(self['node-directory'], "node.url")
40. self['node-url'] = open(node_url_file, "r").read().strip()
41. if self['node-url'][-1] != "/":
42. self['node-url'] += "/"
43.
44. aliases = get_aliases(self['node-directory'])
45. if self['dir-cap']:
46. aliases["tahoe"] = self['dir-cap']
47. self.aliases = aliases # maps alias name to dircap
48.
49.
50. class MakeDirectoryOptions(VDriveOptions):
51. def parseArgs(self, where=""):
52. self.where = where
53. longdesc = """Create a new directory, either unlinked or as a subdirectory."""
54.
55. class AddAliasOptions(VDriveOptions):
56. def parseArgs(self, alias, cap):
57. self.alias = alias
58. self.cap = cap
59.
60. def getSynopsis(self):
61. return "%s add-alias ALIAS DIRCAP" % (os.path.basename(sys.argv[0]),)
62.
63. longdesc = """Add a new alias for an existing directory."""
64.
65. class CreateAliasOptions(VDriveOptions):
66. def parseArgs(self, alias):
67. self.alias = alias
68.
69. def getSynopsis(self):
70. return "%s create-alias ALIAS" % (os.path.basename(sys.argv[0]),)
71.
72. longdesc = """Creates a new directory and adds an alias for it."""
73.
74. class ListAliasOptions(VDriveOptions):
75. longdesc = """Displays a table of all configured aliases."""
76.
77. class ListOptions(VDriveOptions):
78. optFlags = [
79. ("long", "l", "Use long format: show file sizes, and timestamps"),
80. ("uri", "u", "Show file/directory URIs"),
81. ("readonly-uri", None, "Show readonly file/directory URIs"),
82. ("classify", "F", "Append '/' to directory names, and '*' to mutable"),
83. ("json", None, "Show the raw JSON output"),
84. ]
85. def parseArgs(self, where=""):
86. self.where = where
87.
88. longdesc = """List the contents of some portion of the virtual drive."""
89.
90. class GetOptions(VDriveOptions):
91. def parseArgs(self, arg1, arg2=None):
92. # tahoe get FOO |less # write to stdout
93. # tahoe get tahoe:FOO |less # same
94. # tahoe get FOO bar # write to local file
95. # tahoe get tahoe:FOO bar # same
96.
97. self.from_file = arg1
98. self.to_file = arg2
99. if self.to_file == "-":
100. self.to_file = None
101.
102. def getSynopsis(self):
103. return "%s get VDRIVE_FILE LOCAL_FILE" % (os.path.basename(sys.argv[0]),)
104.
105. longdesc = """Retrieve a file from the virtual drive and write it to the
106. local filesystem. If LOCAL_FILE is omitted or '-', the contents of the file
107. will be written to stdout."""
108.
109. def getUsage(self, width=None):
110. t = VDriveOptions.getUsage(self, width)
111. t += """
112. Examples:
113. % tahoe get FOO |less # write to stdout
114. % tahoe get tahoe:FOO |less # same
115. % tahoe get FOO bar # write to local file
116. % tahoe get tahoe:FOO bar # same
117. """
118. return t
119.
120. class PutOptions(VDriveOptions):
121. optFlags = [
122. ("mutable", "m", "Create a mutable file instead of an immutable one."),
123. ]
124.
125. def parseArgs(self, arg1=None, arg2=None):
126. # cat FILE | tahoe put # create unlinked file from stdin
127. # cat FILE | tahoe put - # same
128. # tahoe put bar # create unlinked file from local 'bar'
129. # cat FILE | tahoe put - FOO # create tahoe:FOO from stdin
130. # tahoe put bar FOO # copy local 'bar' to tahoe:FOO
131. # tahoe put bar tahoe:FOO # same
132.
133. if arg1 is not None and arg2 is not None:
134. self.from_file = arg1
135. self.to_file = arg2
136. elif arg1 is not None and arg2 is None:
137. self.from_file = arg1 # might be "-"
138. self.to_file = None
139. else:
140. self.from_file = None
141. self.to_file = None
142. if self.from_file == "-":
143. self.from_file = None
144.
145. def getSynopsis(self):
146. return "%s put LOCAL_FILE VDRIVE_FILE" % (os.path.basename(sys.argv[0]),)
147.
148. longdesc = """Put a file into the virtual drive (copying the file's
149. contents from the local filesystem). If VDRIVE_FILE is missing, upload
150. the file but do not link it into a directory: prints the new filecap to
151. stdout. If LOCAL_FILE is missing or '-', data will be copied from stdin.
152. VDRIVE_FILE is assumed to start with tahoe: unless otherwise specified."""
153.
154. def getUsage(self, width=None):
155. t = VDriveOptions.getUsage(self, width)
156. t += """
157. Examples:
158. % cat FILE | tahoe put # create unlinked file from stdin
159. % cat FILE | tahoe - # same
160. % tahoe put bar # create unlinked file from local 'bar'
161. % cat FILE | tahoe put - FOO # create tahoe:FOO from stdin
162. % tahoe put bar FOO # copy local 'bar' to tahoe:FOO
163. % tahoe put bar tahoe:FOO # same
164. % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
165. """
166. return t
167.
168. class CpOptions(VDriveOptions):
169. optFlags = [
170. ("recursive", "r", "Copy source directory recursively."),
171. ("verbose", "v", "Be noisy about what is happening."),
172. ("caps-only", None,
173. "When copying to local files, write out filecaps instead of actual "
174. "data. (only useful for debugging and tree-comparison purposes)"),
175. ]
176. def parseArgs(self, *args):
177. if len(args) < 2:
178. raise usage.UsageError("cp requires at least two arguments")
179. self.sources = args[:-1]
180. self.destination = args[-1]
181. def getSynopsis(self):
182. return "Usage: tahoe [options] cp FROM.. TO"
183. longdesc = """
184. Use 'tahoe cp' to copy files between a local filesystem and a Tahoe
185. virtual filesystem. Any FROM/TO arguments that begin with an alias
186. indicate Tahoe-side files, and arguments which do not indicate local
187. files. Directories will be copied recursively. New Tahoe-side directories
188. will be created when necessary. Assuming that you have previously set up
189. an alias 'home' with 'tahoe create-alias home', here are some examples:
190.
191. tahoe cp ~/foo.txt home: # creates tahoe-side home:foo.txt
192.
193. tahoe cp ~/foo.txt /tmp/bar.txt home: # copies two files to home:
194.
195. tahoe cp ~/Pictures home:stuff/my-pictures # copies directory recursively
196.
197. You can also use a dircap as either FROM or TO target:
198.
199. tahoe cp URI:DIR2-RO:j74uhg25nwdpjpacl6rkat2yhm:kav7ijeft5h7r7rxdp5bgtlt3viv32yabqajkrdykozia5544jqa/wiki.html ./ # copy Zooko's wiki page to a local file
200.
201. This command still has some limitations: symlinks, special files (device
202. nodes, named pipes), and non-ASCII filenames are not handled very well.
203. Arguments should probably not have trailing slashes. 'tahoe cp' does not
204. behave as much like /bin/cp as you would wish, especially with respect to
205. trailing slashes.
206. """
207.
208. class RmOptions(VDriveOptions):
209. def parseArgs(self, where):
210. self.where = where
211.
212. def getSynopsis(self):
213. return "%s rm VDRIVE_FILE" % (os.path.basename(sys.argv[0]),)
214.
215. class MvOptions(VDriveOptions):
216. def parseArgs(self, frompath, topath):
217. self.from_file = frompath
218. self.to_file = topath
219.
220. def getSynopsis(self):
221. return "%s mv FROM TO" % (os.path.basename(sys.argv[0]),)
222. longdesc = """
223. Use 'tahoe mv' to move files that are already on the grid elsewhere on the grid, e.g., 'tahoe mv alias:some_file alias:new_file'.
224.
225. If moving a remote file into a remote directory, you'll need to append a '/' to the name of the remote directory, e.g., 'tahoe mv tahoe:file1 tahoe:dir/', not 'tahoe mv tahoe:file1 tahoe:dir'.
226.
227. Note that it is not possible to use this command to move local files to the grid -- use 'tahoe cp' for that.
228. """
229.
230. class LnOptions(VDriveOptions):
231. def parseArgs(self, frompath, topath):
232. self.from_file = frompath
233. self.to_file = topath
234.
235. def getSynopsis(self):
236. return "%s ln FROM TO" % (os.path.basename(sys.argv[0]),)
237.
238. class BackupConfigurationError(Exception):
239. pass
240.
241. class BackupOptions(VDriveOptions):
242. optFlags = [
243. ("verbose", "v", "Be noisy about what is happening."),
244. ("ignore-timestamps", None, "Do not use backupdb timestamps to decide if a local file is unchanged."),
245. ]
246.
247. vcs_patterns = ('CVS', 'RCS', 'SCCS', '.git', '.gitignore', '.cvsignore', '.svn',
248. '.arch-ids','{arch}', '=RELEASE-ID', '=meta-update', '=update',
249. '.bzr', '.bzrignore', '.bzrtags', '.hg', '.hgignore', '_darcs')
250.
251. def __init__(self):
252. super(BackupOptions, self).__init__()
253. self['exclude'] = set()
254.
255. def parseArgs(self, localdir, topath):
256. self.from_dir = localdir
257. self.to_dir = topath
258.
259. def getSynopsis(Self):
260. return "%s backup FROM ALIAS:TO" % os.path.basename(sys.argv[0])
261.
262. def opt_exclude(self, pattern):
263. """Ignore files matching a glob pattern. You may give multiple
264. '--exclude' options."""
265. g = pattern.strip()
266. if g:
267. exclude = self['exclude']
268. exclude.add(g)
269.
270. def opt_exclude_from(self, filepath):
271. """Ignore file matching glob patterns listed in file, one per
272. line."""
273. try:
274. exclude_file = file(filepath)
275. except:
276. raise BackupConfigurationError('Error opening exclude file %r.' % filepath)
277. try:
278. for line in exclude_file:
279. self.opt_exclude(line)
280. finally:
281. exclude_file.close()
282.
283. def opt_exclude_vcs(self):
284. """Exclude files and directories used by following version
285. control systems: 'CVS', 'RCS', 'SCCS', 'SVN', 'Arch',
286. 'Bazaar', 'Mercurial', and 'Darcs'."""
287. for pattern in self.vcs_patterns:
288. self.opt_exclude(pattern)
289.
290. def filter_listdir(self, listdir):
291. """Yields non-excluded childpaths in path."""
292. exclude = self['exclude']
293. exclude_regexps = [re.compile(fnmatch.translate(pat)) for pat in exclude]
294. for filename in listdir:
295. for regexp in exclude_regexps:
296. if regexp.match(filename):
297. break
298. else:
299. yield filename
300.
301. longdesc = """Add a versioned backup of the local FROM directory to a timestamped subdir of the (tahoe) TO/Archives directory, sharing as many files and directories as possible with the previous backup. Creates TO/Latest as a reference to the latest backup. Behaves somewhat like 'rsync -a --link-dest=TO/Archives/(previous) FROM TO/Archives/(new); ln -sf TO/Archives/(new) TO/Latest'."""
302.
303. class WebopenOptions(VDriveOptions):
304. def parseArgs(self, where=''):
305. self.where = where
306.
307. def getSynopsis(self):
308. return "%s webopen [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
309.
310. longdesc = """Opens a webbrowser to the contents of some portion of the virtual drive. When called without arguments, opens to the Welcome page."""
311.
312. class ManifestOptions(VDriveOptions):
313. optFlags = [
314. ("storage-index", "s", "Only print storage index strings, not pathname+cap"),
315. ("verify-cap", None, "Only print verifycap, not pathname+cap"),
316. ("repair-cap", None, "Only print repaircap, not pathname+cap"),
317. ("raw", "r", "Display raw JSON data instead of parsed"),
318. ]
319. def parseArgs(self, where=''):
320. self.where = where
321.
322. def getSynopsis(self):
323. return "%s manifest [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
324.
325. longdesc = """Print a list of all files/directories reachable from the given starting point."""
326.
327. class StatsOptions(VDriveOptions):
328. optFlags = [
329. ("raw", "r", "Display raw JSON data instead of parsed"),
330. ]
331. def parseArgs(self, where=''):
332. self.where = where
333.
334. def getSynopsis(self):
335. return "%s stats [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
336.
337. longdesc = """Print statistics about of all files/directories reachable from the given starting point."""
338.
339. class CheckOptions(VDriveOptions):
340. optFlags = [
341. ("raw", None, "Display raw JSON data instead of parsed"),
342. ("verify", None, "Verify all hashes, instead of merely querying share presence"),
343. ("repair", None, "Automatically repair any problems found"),
344. ("add-lease", None, "Add/renew lease on all shares"),
345. ]
346. def parseArgs(self, where=''):
347. self.where = where
348.
349. def getSynopsis(self):
350. return "%s check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
351.
352. longdesc = """Check a single file or directory: count how many shares are available, verify their hashes. Optionally repair the file if any problems were found."""
353.
354. class DeepCheckOptions(VDriveOptions):
355. optFlags = [
356. ("raw", None, "Display raw JSON data instead of parsed"),
357. ("verify", None, "Verify all hashes, instead of merely querying share presence"),
358. ("repair", None, "Automatically repair any problems found"),
359. ("add-lease", None, "Add/renew lease on all shares"),
360. ("verbose", "v", "Be noisy about what is happening."),
361. ]
362. def parseArgs(self, where=''):
363. self.where = where
364.
365. def getSynopsis(self):
366. return "%s deep-check [ALIAS:PATH]" % (os.path.basename(sys.argv[0]),)
367.
368. longdesc = """Check all files/directories reachable from the given starting point (which must be a directory), like 'tahoe check' but for multiple files. Optionally repair any problems found."""
369.
370. subCommands = [
371. ["mkdir", None, MakeDirectoryOptions, "Create a new directory"],
372. ["add-alias", None, AddAliasOptions, "Add a new alias cap"],
373. ["create-alias", None, CreateAliasOptions, "Create a new alias cap"],
374. ["list-aliases", None, ListAliasOptions, "List all alias caps"],
375. ["ls", None, ListOptions, "List a directory"],
376. ["get", None, GetOptions, "Retrieve a file from the virtual drive."],
377. ["put", None, PutOptions, "Upload a file into the virtual drive."],
378. ["cp", None, CpOptions, "Copy one or more files."],
379. ["rm", None, RmOptions, "Unlink a file or directory in the virtual drive."],
380. ["mv", None, MvOptions, "Move a file within the virtual drive."],
381. ["ln", None, LnOptions, "Make an additional link to an existing file."],
382. ["backup", None, BackupOptions, "Make target dir look like local dir."],
383. ["webopen", None, WebopenOptions, "Open a webbrowser to the root_dir"],
384. ["manifest", None, ManifestOptions, "List all files/dirs in a subtree"],
385. ["stats", None, StatsOptions, "Print statistics about all files/dirs in a subtree"],
386. ["check", None, CheckOptions, "Check a single file or directory"],
387. ["deep-check", None, DeepCheckOptions, "Check all files/directories reachable from a starting point"],
388. ]
389.
390. def mkdir(options):
391. from allmydata.scripts import tahoe_mkdir
392. rc = tahoe_mkdir.mkdir(options)
393. return rc
394.
395. def add_alias(options):
396. from allmydata.scripts import tahoe_add_alias
397. rc = tahoe_add_alias.add_alias(options)
398. return rc
399.
400. def create_alias(options):
401. from allmydata.scripts import tahoe_add_alias
402. rc = tahoe_add_alias.create_alias(options)
403. return rc
404.
405. def list_aliases(options):
406. from allmydata.scripts import tahoe_add_alias
407. rc = tahoe_add_alias.list_aliases(options)
408. return rc
409.
410. def list(options):
411. from allmydata.scripts import tahoe_ls
412. rc = tahoe_ls.list(options)
413. return rc
414.
415. def get(options):
416. from allmydata.scripts import tahoe_get
417. rc = tahoe_get.get(options)
418. if rc == 0:
419. if options.to_file is None:
420. # be quiet, since the file being written to stdout should be
421. # proof enough that it worked, unless the user is unlucky
422. # enough to have picked an empty file
423. pass
424. else:
425. print >>options.stderr, "%s retrieved and written to %s" % \
426. (options.from_file, options.to_file)
427. return rc
428.
429. def put(options):
430. from allmydata.scripts import tahoe_put
431. rc = tahoe_put.put(options)
432. return rc
433.
434. def cp(options):
435. from allmydata.scripts import tahoe_cp
436. rc = tahoe_cp.copy(options)
437. return rc
438.
439. def rm(options):
440. from allmydata.scripts import tahoe_rm
441. rc = tahoe_rm.rm(options)
442. return rc
443.
444. def mv(options):
445. from allmydata.scripts import tahoe_mv
446. rc = tahoe_mv.mv(options, mode="move")
447. return rc
448.
449. def ln(options):
450. from allmydata.scripts import tahoe_mv
451. rc = tahoe_mv.mv(options, mode="link")
452. return rc
453.
454. def backup(options):
455. from allmydata.scripts import tahoe_backup
456. rc = tahoe_backup.backup(options)
457. return rc
458.
459. def webopen(options, opener=None):
460. from allmydata.scripts import tahoe_webopen
461. rc = tahoe_webopen.webopen(options, opener=opener)
462. return rc
463.
464. def manifest(options):
465. from allmydata.scripts import tahoe_manifest
466. rc = tahoe_manifest.manifest(options)
467. return rc
468.
469. def stats(options):
470. from allmydata.scripts import tahoe_manifest
471. rc = tahoe_manifest.stats(options)
472. return rc
473.
474. def check(options):
475. from allmydata.scripts import tahoe_check
476. rc = tahoe_check.check(options)
477. return rc
478.
479. def deepcheck(options):
480. from allmydata.scripts import tahoe_check
481. rc = tahoe_check.deepcheck(options)
482. return rc
483.
484. dispatch = {
485. "mkdir": mkdir,
486. "add-alias": add_alias,
487. "create-alias": create_alias,
488. "list-aliases": list_aliases,
489. "ls": list,
490. "get": get,
491. "put": put,
492. "cp": cp,
493. "rm": rm,
494. "mv": mv,
495. "ln": ln,
496. "backup": backup,
497. "webopen": webopen,
498. "manifest": manifest,
499. "stats": stats,
500. "check": check,
501. "deep-check": deepcheck,
502. }