]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/magic_folder_cli.py
Improve the error reporting for 'tahoe magic-folder join/leave'. refs #2568
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / magic_folder_cli.py
1
2 import os
3 from types import NoneType
4 from cStringIO import StringIO
5
6 from twisted.python import usage
7
8 from allmydata.util.assertutil import precondition
9
10 from .common import BaseOptions, BasedirOptions, get_aliases
11 from .cli import MakeDirectoryOptions, LnOptions, CreateAliasOptions
12 import tahoe_mv
13 from allmydata.util.encodingutil import argv_to_abspath, argv_to_unicode, to_str, \
14     quote_local_unicode_path
15 from allmydata.util import fileutil
16 from allmydata import uri
17
18 INVITE_SEPARATOR = "+"
19
20 class CreateOptions(BasedirOptions):
21     nickname = None
22     local_dir = None
23     synopsis = "MAGIC_ALIAS: [NICKNAME LOCAL_DIR]"
24     def parseArgs(self, alias, nickname=None, local_dir=None):
25         BasedirOptions.parseArgs(self)
26         alias = argv_to_unicode(alias)
27         if not alias.endswith(u':'):
28             raise usage.UsageError("An alias must end with a ':' character.")
29         self.alias = alias[:-1]
30         self.nickname = None if nickname is None else argv_to_unicode(nickname)
31
32         # Expand the path relative to the current directory of the CLI command, not the node.
33         self.local_dir = None if local_dir is None else argv_to_abspath(local_dir, long_path=False)
34
35         if self.nickname and not self.local_dir:
36             raise usage.UsageError("If NICKNAME is specified then LOCAL_DIR must also be specified.")
37         node_url_file = os.path.join(self['node-directory'], u"node.url")
38         self['node-url'] = fileutil.read(node_url_file).strip()
39
40 def _delegate_options(source_options, target_options):
41     target_options.aliases = get_aliases(source_options['node-directory'])
42     target_options["node-url"] = source_options["node-url"]
43     target_options["node-directory"] = source_options["node-directory"]
44     target_options.stdin = StringIO("")
45     target_options.stdout = StringIO()
46     target_options.stderr = StringIO()
47     return target_options
48
49 def create(options):
50     precondition(isinstance(options.alias, unicode), alias=options.alias)
51     precondition(isinstance(options.nickname, (unicode, NoneType)), nickname=options.nickname)
52     precondition(isinstance(options.local_dir, (unicode, NoneType)), local_dir=options.local_dir)
53
54     from allmydata.scripts import tahoe_add_alias
55     create_alias_options = _delegate_options(options, CreateAliasOptions())
56     create_alias_options.alias = options.alias
57
58     rc = tahoe_add_alias.create_alias(create_alias_options)
59     if rc != 0:
60         print >>options.stderr, create_alias_options.stderr.getvalue()
61         return rc
62     print >>options.stdout, create_alias_options.stdout.getvalue()
63
64     if options.nickname is not None:
65         invite_options = _delegate_options(options, InviteOptions())
66         invite_options.alias = options.alias
67         invite_options.nickname = options.nickname
68         rc = invite(invite_options)
69         if rc != 0:
70             print >>options.stderr, "magic-folder: failed to invite after create\n"
71             print >>options.stderr, invite_options.stderr.getvalue()
72             return rc
73         invite_code = invite_options.stdout.getvalue().strip()
74         join_options = _delegate_options(options, JoinOptions())
75         join_options.local_dir = options.local_dir
76         join_options.invite_code = invite_code
77         rc = join(join_options)
78         if rc != 0:
79             print >>options.stderr, "magic-folder: failed to join after create\n"
80             print >>options.stderr, join_options.stderr.getvalue()
81             return rc
82     return 0
83
84 class InviteOptions(BasedirOptions):
85     nickname = None
86     synopsis = "MAGIC_ALIAS: NICKNAME"
87     stdin = StringIO("")
88     def parseArgs(self, alias, nickname=None):
89         BasedirOptions.parseArgs(self)
90         alias = argv_to_unicode(alias)
91         if not alias.endswith(u':'):
92             raise usage.UsageError("An alias must end with a ':' character.")
93         self.alias = alias[:-1]
94         self.nickname = argv_to_unicode(nickname)
95         node_url_file = os.path.join(self['node-directory'], u"node.url")
96         self['node-url'] = open(node_url_file, "r").read().strip()
97         aliases = get_aliases(self['node-directory'])
98         self.aliases = aliases
99
100 def invite(options):
101     precondition(isinstance(options.alias, unicode), alias=options.alias)
102     precondition(isinstance(options.nickname, unicode), nickname=options.nickname)
103
104     from allmydata.scripts import tahoe_mkdir
105     mkdir_options = _delegate_options(options, MakeDirectoryOptions())
106     mkdir_options.where = None
107
108     rc = tahoe_mkdir.mkdir(mkdir_options)
109     if rc != 0:
110         print >>options.stderr, "magic-folder: failed to mkdir\n"
111         return rc
112
113     # FIXME this assumes caps are ASCII.
114     dmd_write_cap = mkdir_options.stdout.getvalue().strip()
115     dmd_readonly_cap = uri.from_string(dmd_write_cap).get_readonly().to_string()
116     if dmd_readonly_cap is None:
117         print >>options.stderr, "magic-folder: failed to diminish dmd write cap\n"
118         return 1
119
120     magic_write_cap = get_aliases(options["node-directory"])[options.alias]
121     magic_readonly_cap = uri.from_string(magic_write_cap).get_readonly().to_string()
122
123     # tahoe ln CLIENT_READCAP COLLECTIVE_WRITECAP/NICKNAME
124     ln_options = _delegate_options(options, LnOptions())
125     ln_options.from_file = unicode(dmd_readonly_cap, 'utf-8')
126     ln_options.to_file = u"%s/%s" % (unicode(magic_write_cap, 'utf-8'), options.nickname)
127     rc = tahoe_mv.mv(ln_options, mode="link")
128     if rc != 0:
129         print >>options.stderr, "magic-folder: failed to create link\n"
130         print >>options.stderr, ln_options.stderr.getvalue()
131         return rc
132
133     # FIXME: this assumes caps are ASCII.
134     print >>options.stdout, "%s%s%s" % (magic_readonly_cap, INVITE_SEPARATOR, dmd_write_cap)
135     return 0
136
137 class JoinOptions(BasedirOptions):
138     synopsis = "INVITE_CODE LOCAL_DIR"
139     dmd_write_cap = ""
140     magic_readonly_cap = ""
141     def parseArgs(self, invite_code, local_dir):
142         BasedirOptions.parseArgs(self)
143
144         # Expand the path relative to the current directory of the CLI command, not the node.
145         self.local_dir = None if local_dir is None else argv_to_abspath(local_dir, long_path=False)
146         self.invite_code = to_str(argv_to_unicode(invite_code))
147
148 def join(options):
149     fields = options.invite_code.split(INVITE_SEPARATOR)
150     if len(fields) != 2:
151         raise usage.UsageError("Invalid invite code.")
152     magic_readonly_cap, dmd_write_cap = fields
153
154     dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap")
155     collective_readcap_file = os.path.join(options["node-directory"], u"private", u"collective_dircap")
156     magic_folder_db_file = os.path.join(options["node-directory"], u"private", u"magicfolderdb.sqlite")
157
158     if os.path.exists(dmd_cap_file) or os.path.exists(collective_readcap_file) or os.path.exists(magic_folder_db_file):
159         print >>options.stderr, ("\nThis client has already joined a magic folder."
160                                  "\nUse the 'tahoe magic-folder leave' command first.\n")
161         return 1
162
163     fileutil.write(dmd_cap_file, dmd_write_cap)
164     fileutil.write(collective_readcap_file, magic_readonly_cap)
165
166     # FIXME: modify any existing [magic_folder] fields, rather than appending.
167     fileutil.write(os.path.join(options["node-directory"], u"tahoe.cfg"),
168                    "[magic_folder]\nenabled = True\nlocal.directory = %s\n"
169                    % (options.local_dir.encode('utf-8'),), mode="ab")
170     return 0
171
172 class LeaveOptions(BasedirOptions):
173     synopsis = ""
174     def parseArgs(self):
175         BasedirOptions.parseArgs(self)
176
177 def leave(options):
178     from ConfigParser import SafeConfigParser
179
180     dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap")
181     collective_readcap_file = os.path.join(options["node-directory"], u"private", u"collective_dircap")
182     magic_folder_db_file = os.path.join(options["node-directory"], u"private", u"magicfolderdb.sqlite")
183
184     parser = SafeConfigParser()
185     parser.read(os.path.join(options["node-directory"], u"tahoe.cfg"))
186     parser.remove_section("magic_folder")
187     f = open(os.path.join(options["node-directory"], u"tahoe.cfg"), "w")
188     parser.write(f)
189     f.close()
190
191     for f in [dmd_cap_file, collective_readcap_file, magic_folder_db_file]:
192         try:
193             fileutil.remove(f)
194         except Exception as e:
195             print >>options.stderr, ("Warning: unable to remove %s due to %s: %s"
196                 % (quote_local_unicode_path(f), e.__class__.__name__, str(e)))
197
198     return 0
199
200 class MagicFolderCommand(BaseOptions):
201     subCommands = [
202         ["create", None, CreateOptions, "Create a Magic Folder."],
203         ["invite", None, InviteOptions, "Invite someone to a Magic Folder."],
204         ["join", None, JoinOptions, "Join a Magic Folder."],
205         ["leave", None, LeaveOptions, "Leave a Magic Folder."],
206     ]
207     def postOptions(self):
208         if not hasattr(self, 'subOptions'):
209             raise usage.UsageError("must specify a subcommand")
210     def getSynopsis(self):
211         return "Usage: tahoe [global-options] magic SUBCOMMAND"
212     def getUsage(self, width=None):
213         t = BaseOptions.getUsage(self, width)
214         t += """\
215 Please run e.g. 'tahoe magic-folder create --help' for more details on each
216 subcommand.
217 """
218         return t
219
220 subDispatch = {
221     "create": create,
222     "invite": invite,
223     "join": join,
224     "leave": leave,
225 }
226
227 def do_magic_folder(options):
228     so = options.subOptions
229     so.stdout = options.stdout
230     so.stderr = options.stderr
231     f = subDispatch[options.subCommand]
232     return f(so)
233
234 subCommands = [
235     ["magic-folder", None, MagicFolderCommand,
236      "Magic Folder subcommands: use 'tahoe magic-folder' for a list."],
237 ]
238
239 dispatch = {
240     "magic-folder": do_magic_folder,
241 }