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