Multiple magic-folders

This moves all magic-folder configs to a single YAML
file. We load legacy config fine and don't mess with
legacy config unless you use a magic-folder command that
changes the config.

Increase test coverage
This commit is contained in:
meejah 2017-08-23 15:34:11 -06:00
parent 1b6f477549
commit 672475cb2b
12 changed files with 1157 additions and 161 deletions

View File

@ -156,8 +156,7 @@ class Terminator(service.Service):
#@defer.inlineCallbacks #@defer.inlineCallbacks
def create_client(basedir=u"."): def create_client(basedir=u"."):
from allmydata.node import read_config from allmydata.node import read_config
config = read_config(basedir, u"client.port") config = read_config(basedir, u"client.port", _valid_config_sections=_valid_config_sections)
config.validate(_valid_config_sections())
#defer.returnValue( #defer.returnValue(
return _Client( return _Client(
config, config,
@ -192,7 +191,7 @@ class _Client(node.Node, pollmixin.PollMixin):
node.Node.__init__(self, config, basedir=basedir) node.Node.__init__(self, config, basedir=basedir)
# All tub.registerReference must happen *after* we upcall, since # All tub.registerReference must happen *after* we upcall, since
# that's what does tub.setLocation() # that's what does tub.setLocation()
self._magic_folder = None self._magic_folders = dict()
self.started_timestamp = time.time() self.started_timestamp = time.time()
self.logSource="Client" self.logSource="Client"
self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy() self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy()
@ -576,35 +575,47 @@ class _Client(node.Node, pollmixin.PollMixin):
"See docs/frontends/magic-folder.rst for more information.") "See docs/frontends/magic-folder.rst for more information.")
if self.get_config("magic_folder", "enabled", False, boolean=True): if self.get_config("magic_folder", "enabled", False, boolean=True):
#print "magic folder enabled"
from allmydata.frontends import magic_folder from allmydata.frontends import magic_folder
db_filename = os.path.join(self.basedir, "private", "magicfolderdb.sqlite")
local_dir_config = self.get_config("magic_folder", "local.directory").decode("utf-8")
try: try:
poll_interval = int(self.get_config("magic_folder", "poll_interval", 3)) magic_folders = magic_folder.load_magic_folders(self.basedir)
except ValueError: except Exception as e:
raise ValueError("[magic_folder] poll_interval must be an int") log.msg("Error loading magic-folder config: {}".format(e))
raise
s = magic_folder.MagicFolder(
client=self,
upload_dircap=self.get_private_config("magic_folder_dircap"),
collective_dircap=self.get_private_config("collective_dircap"),
local_path_u=abspath_expanduser_unicode(local_dir_config, base=self.basedir),
dbfile=abspath_expanduser_unicode(db_filename),
umask=self.get_config("magic_folder", "download.umask", 0077),
downloader_delay=poll_interval,
)
self._magic_folder = s
s.setServiceParent(self)
s.startService()
# start processing the upload queue when we've connected to # start processing the upload queue when we've connected to
# enough servers # enough servers
threshold = min(self.encoding_params["k"], threshold = min(self.encoding_params["k"],
self.encoding_params["happy"] + 1) self.encoding_params["happy"] + 1)
d = self.storage_broker.when_connected_enough(threshold)
d.addCallback(lambda ign: s.ready()) for (name, mf_config) in magic_folders.items():
self.log("Starting magic_folder '{}'".format(name))
db_filename = os.path.join(self.basedir, "private", "magicfolder_{}.sqlite".format(name))
local_dir_config = mf_config['directory']
try:
poll_interval = int(mf_config["poll_interval"])
except ValueError:
raise ValueError("'poll_interval' option must be an int")
s = magic_folder.MagicFolder(
client=self,
upload_dircap=mf_config["upload_dircap"].encode('ascii'),
collective_dircap=mf_config["collective_dircap"].encode('ascii'),
local_path_u=abspath_expanduser_unicode(local_dir_config, base=self.basedir),
dbfile=abspath_expanduser_unicode(db_filename),
umask=self.get_config("magic_folder", "download.umask", 0077),
name=name,
downloader_delay=poll_interval,
)
self._magic_folders[name] = s
s.setServiceParent(self)
s.startService()
connected_d = self.storage_broker.when_connected_enough(threshold)
def connected_enough(ign, mf):
mf.ready() # returns a Deferred we ignore
return None
connected_d.addCallback(connected_enough, s)
def _check_exit_trigger(self, exit_trigger_file): def _check_exit_trigger(self, exit_trigger_file):
if os.path.exists(exit_trigger_file): if os.path.exists(exit_trigger_file):

View File

@ -4,6 +4,7 @@ import os.path
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
import time import time
import ConfigParser
from twisted.internet import defer, reactor, task from twisted.internet import defer, reactor, task
from twisted.internet.error import AlreadyCancelled from twisted.internet.error import AlreadyCancelled
@ -14,7 +15,7 @@ from twisted.application import service
from zope.interface import Interface, Attribute, implementer from zope.interface import Interface, Attribute, implementer
from allmydata.util import fileutil from allmydata.util import fileutil, configutil, yamlutil
from allmydata.interfaces import IDirectoryNode from allmydata.interfaces import IDirectoryNode
from allmydata.util import log from allmydata.util import log
from allmydata.util.fileutil import precondition_abspath, get_pathinfo, ConflictError from allmydata.util.fileutil import precondition_abspath, get_pathinfo, ConflictError
@ -59,12 +60,182 @@ def is_new_file(pathinfo, db_entry):
(db_entry.size, db_entry.ctime_ns, db_entry.mtime_ns)) (db_entry.size, db_entry.ctime_ns, db_entry.mtime_ns))
def _upgrade_magic_folder_config(basedir):
"""
Helper that upgrades from single-magic-folder-only configs to
multiple magic-folder configuration style (in YAML)
"""
config_fname = os.path.join(basedir, "tahoe.cfg")
config = configutil.get_config(config_fname)
collective_fname = os.path.join(basedir, "private", "collective_dircap")
upload_fname = os.path.join(basedir, "private", "magic_folder_dircap")
magic_folders = {
u"default": {
u"directory": config.get("magic_folder", "local.directory").decode("utf-8"),
u"collective_dircap": fileutil.read(collective_fname),
u"upload_dircap": fileutil.read(upload_fname),
u"poll_interval": int(config.get("magic_folder", "poll_interval")),
},
}
fileutil.move_into_place(
source=os.path.join(basedir, "private", "magicfolderdb.sqlite"),
dest=os.path.join(basedir, "private", "magicfolder_default.sqlite"),
)
save_magic_folders(basedir, magic_folders)
config.remove_option("magic_folder", "local.directory")
config.remove_option("magic_folder", "poll_interval")
configutil.write_config(os.path.join(basedir, 'tahoe.cfg'), config)
fileutil.remove_if_possible(collective_fname)
fileutil.remove_if_possible(upload_fname)
def maybe_upgrade_magic_folders(node_directory):
"""
If the given node directory is not already using the new-style
magic-folder config it will be upgraded to do so. (This should
only be done if the user is running a command that needs to modify
the config)
"""
yaml_fname = os.path.join(node_directory, u"private", u"magic_folders.yaml")
if os.path.exists(yaml_fname):
# we already have new-style magic folders
return
config_fname = os.path.join(node_directory, "tahoe.cfg")
config = configutil.get_config(config_fname)
# we have no YAML config; if we have config in tahoe.cfg then we
# can upgrade it to the YAML-based configuration
if config.has_option("magic_folder", "local.directory"):
_upgrade_magic_folder_config(node_directory)
def load_magic_folders(node_directory):
"""
Loads existing magic-folder configuration and returns it as a dict
mapping name -> dict of config. This will NOT upgrade from
old-style to new-style config (but WILL read old-style config and
return in the same way as if it was new-style).
:returns: dict mapping magic-folder-name to its config (also a dict)
"""
yaml_fname = os.path.join(node_directory, u"private", u"magic_folders.yaml")
folders = dict()
config_fname = os.path.join(node_directory, "tahoe.cfg")
config = configutil.get_config(config_fname)
if not os.path.exists(yaml_fname):
# there will still be a magic_folder section in a "new"
# config, but it won't have local.directory nor poll_interval
# in it.
if config.has_option("magic_folder", "local.directory"):
up_fname = os.path.join(node_directory, "private", "magic_folder_dircap")
coll_fname = os.path.join(node_directory, "private", "collective_dircap")
directory = config.get("magic_folder", "local.directory").decode('utf8')
try:
interval = int(config.get("magic_folder", "poll_interval"))
except ConfigParser.NoOptionError:
interval = 60
dir_fp = to_filepath(directory)
if not dir_fp.exists():
raise Exception(
"The '[magic_folder] local.directory' parameter is {} "
"but there is no directory at that location.".format(
quote_local_unicode_path(directory),
)
)
if not dir_fp.isdir():
raise Exception(
"The '[magic_folder] local.directory' parameter is {} "
"but the thing at that location is not a directory.".format(
quote_local_unicode_path(directory)
)
)
folders[u"default"] = {
u"directory": directory,
u"upload_dircap": fileutil.read(up_fname),
u"collective_dircap": fileutil.read(coll_fname),
u"poll_interval": interval,
}
else:
# without any YAML file AND no local.directory option it's
# an error if magic-folder is "enabled" because we don't
# actually have enough config for any magic-folders at all
if config.has_section("magic_folder") \
and config.getboolean("magic_folder", "enabled") \
and not folders:
raise Exception(
"[magic_folder] is enabled but has no YAML file and no "
"'local.directory' option."
)
elif os.path.exists(yaml_fname): # yaml config-file exists
if config.has_option("magic_folder", "local.directory"):
raise Exception(
"magic-folder config has both old-style configuration"
" and new-style configuration; please remove the "
"'local.directory' key from tahoe.cfg or remove "
"'magic_folders.yaml' from {}".format(node_directory)
)
with open(yaml_fname, "r") as f:
magic_folders = yamlutil.safe_load(f.read())
if not isinstance(magic_folders, dict):
raise Exception(
"'{}' should contain a dict".format(yaml_fname)
)
folders = magic_folders['magic-folders']
if not isinstance(folders, dict):
raise Exception(
"'magic-folders' in '{}' should be a dict".format(yaml_fname)
)
# check configuration
for (name, mf_config) in folders.items():
if not isinstance(mf_config, dict):
raise Exception(
"Each item in '{}' must itself be a dict".format(yaml_fname)
)
for k in ['collective_dircap', 'upload_dircap', 'directory', 'poll_interval']:
if k not in mf_config:
raise Exception(
"Config for magic folder '{}' is missing '{}'".format(
name, k
)
)
for k in ['collective_dircap', 'upload_dircap']:
if isinstance(mf_config[k], unicode):
mf_config[k] = mf_config[k].encode('ascii')
return folders
def save_magic_folders(node_directory, folders):
fileutil.write_atomically(
os.path.join(node_directory, u"private", u"magic_folders.yaml"),
yamlutil.safe_dump({u"magic-folders": folders}),
)
config = configutil.get_config(os.path.join(node_directory, u"tahoe.cfg"))
configutil.set_config(config, "magic_folder", "enabled", "True")
configutil.write_config(os.path.join(node_directory, u"tahoe.cfg"), config)
class MagicFolder(service.MultiService): class MagicFolder(service.MultiService):
name = 'magic-folder'
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask,
uploader_delay=1.0, clock=None, downloader_delay=60): name, uploader_delay=1.0, clock=None, downloader_delay=60):
precondition_abspath(local_path_u) precondition_abspath(local_path_u)
if not os.path.exists(local_path_u):
raise ValueError("'{}' does not exist".format(local_path_u))
if not os.path.isdir(local_path_u):
raise ValueError("'{}' is not a directory".format(local_path_u))
# this is used by 'service' things and must be unique in this Service hierarchy
self.name = 'magic-folder-{}'.format(name)
service.MultiService.__init__(self) service.MultiService.__init__(self)
@ -149,14 +320,10 @@ class QueueMixin(HookMixin):
} }
self.started_d = self.set_hook('started') self.started_d = self.set_hook('started')
if not self._local_filepath.exists(): # we should have gotten nice errors already while loading the
raise AssertionError("The '[magic_folder] local.directory' parameter was %s " # config, but just to be safe:
"but there is no directory at that location." assert self._local_filepath.exists()
% quote_local_unicode_path(self._local_path_u)) assert self._local_filepath.isdir()
if not self._local_filepath.isdir():
raise AssertionError("The '[magic_folder] local.directory' parameter was %s "
"but the thing at that location is not a directory."
% quote_local_unicode_path(self._local_path_u))
self._deque = deque() self._deque = deque()
# do we also want to bound on "maximum age"? # do we also want to bound on "maximum age"?
@ -343,11 +510,9 @@ class Uploader(QueueMixin):
self.is_ready = False self.is_ready = False
if not IDirectoryNode.providedBy(upload_dirnode): if not IDirectoryNode.providedBy(upload_dirnode):
raise AssertionError("The URI in '%s' does not refer to a directory." raise AssertionError("'upload_dircap' does not refer to a directory")
% os.path.join('private', 'magic_folder_dircap'))
if upload_dirnode.is_unknown() or upload_dirnode.is_readonly(): if upload_dirnode.is_unknown() or upload_dirnode.is_readonly():
raise AssertionError("The URI in '%s' is not a writecap to a directory." raise AssertionError("'upload_dircap' is not a writecap to a directory")
% os.path.join('private', 'magic_folder_dircap'))
self._upload_dirnode = upload_dirnode self._upload_dirnode = upload_dirnode
self._inotify = get_inotify_module() self._inotify = get_inotify_module()
@ -756,11 +921,9 @@ class Downloader(QueueMixin, WriteFileMixin):
QueueMixin.__init__(self, client, local_path_u, db, 'downloader', clock) QueueMixin.__init__(self, client, local_path_u, db, 'downloader', clock)
if not IDirectoryNode.providedBy(collective_dirnode): if not IDirectoryNode.providedBy(collective_dirnode):
raise AssertionError("The URI in '%s' does not refer to a directory." raise AssertionError("'collective_dircap' does not refer to a directory")
% os.path.join('private', 'collective_dircap'))
if collective_dirnode.is_unknown() or not collective_dirnode.is_readonly(): if collective_dirnode.is_unknown() or not collective_dirnode.is_readonly():
raise AssertionError("The URI in '%s' is not a readonly cap to a directory." raise AssertionError("'collective_dircap' is not a readonly cap to a directory")
% os.path.join('private', 'collective_dircap'))
self._collective_dirnode = collective_dirnode self._collective_dirnode = collective_dirnode
self._upload_readonly_dircap = upload_readonly_dircap self._upload_readonly_dircap = upload_readonly_dircap
@ -778,7 +941,7 @@ class Downloader(QueueMixin, WriteFileMixin):
while True: while True:
try: try:
data = yield self._scan_remote_collective(scan_self=True) data = yield self._scan_remote_collective(scan_self=True)
twlog.msg("Completed initial Magic Folder scan successfully") twlog.msg("Completed initial Magic Folder scan successfully ({})".format(self))
x = yield self._begin_processing(data) x = yield self._begin_processing(data)
defer.returnValue(x) defer.returnValue(x)
break break

View File

@ -1,7 +1,6 @@
import os import os
import urllib import urllib
from sys import stderr
from types import NoneType from types import NoneType
from cStringIO import StringIO from cStringIO import StringIO
from datetime import datetime from datetime import datetime
@ -19,20 +18,29 @@ from allmydata.util.encodingutil import argv_to_abspath, argv_to_unicode, to_str
quote_local_unicode_path quote_local_unicode_path
from allmydata.scripts.common_http import do_http, BadResponse from allmydata.scripts.common_http import do_http, BadResponse
from allmydata.util import fileutil from allmydata.util import fileutil
from allmydata.util import configutil
from allmydata import uri from allmydata import uri
from allmydata.util.abbreviate import abbreviate_space, abbreviate_time from allmydata.util.abbreviate import abbreviate_space, abbreviate_time
from allmydata.frontends.magic_folder import load_magic_folders
from allmydata.frontends.magic_folder import save_magic_folders
from allmydata.frontends.magic_folder import maybe_upgrade_magic_folders
INVITE_SEPARATOR = "+" INVITE_SEPARATOR = "+"
class CreateOptions(BasedirOptions): class CreateOptions(BasedirOptions):
nickname = None nickname = None # NOTE: *not* the "name of this magic-folder"
local_dir = None local_dir = None
synopsis = "MAGIC_ALIAS: [NICKNAME LOCAL_DIR]" synopsis = "MAGIC_ALIAS: [NICKNAME LOCAL_DIR]"
optParameters = [ optParameters = [
("poll-interval", "p", "60", "How often to ask for updates"), ("poll-interval", "p", "60", "How often to ask for updates"),
("name", "n", "default", "The name of this magic-folder"),
] ]
description = (
"Create a new magic-folder. If you specify NICKNAME and "
"LOCAL_DIR, this client will also be invited and join "
"using the given nickname. A new alias (see 'tahoe list-aliases') "
"will be added with the master folder's writecap."
)
def parseArgs(self, alias, nickname=None, local_dir=None): def parseArgs(self, alias, nickname=None, local_dir=None):
BasedirOptions.parseArgs(self) BasedirOptions.parseArgs(self)
@ -61,6 +69,7 @@ def _delegate_options(source_options, target_options):
target_options.aliases = get_aliases(source_options['node-directory']) target_options.aliases = get_aliases(source_options['node-directory'])
target_options["node-url"] = source_options["node-url"] target_options["node-url"] = source_options["node-url"]
target_options["node-directory"] = source_options["node-directory"] target_options["node-directory"] = source_options["node-directory"]
target_options["name"] = source_options["name"]
target_options.stdin = StringIO("") target_options.stdin = StringIO("")
target_options.stdout = StringIO() target_options.stdout = StringIO()
target_options.stderr = StringIO() target_options.stderr = StringIO()
@ -71,6 +80,15 @@ def create(options):
precondition(isinstance(options.nickname, (unicode, NoneType)), nickname=options.nickname) precondition(isinstance(options.nickname, (unicode, NoneType)), nickname=options.nickname)
precondition(isinstance(options.local_dir, (unicode, NoneType)), local_dir=options.local_dir) precondition(isinstance(options.local_dir, (unicode, NoneType)), local_dir=options.local_dir)
# make sure we don't already have a magic-folder with this name before we create the alias
maybe_upgrade_magic_folders(options["node-directory"])
folders = load_magic_folders(options["node-directory"])
if options['name'] in folders:
print >>options.stderr, "Already have a magic-folder named '{}'".format(options['name'])
return 1
# create an alias; this basically just remembers the cap for the
# master directory
from allmydata.scripts import tahoe_add_alias from allmydata.scripts import tahoe_add_alias
create_alias_options = _delegate_options(options, CreateAliasOptions()) create_alias_options = _delegate_options(options, CreateAliasOptions())
create_alias_options.alias = options.alias create_alias_options.alias = options.alias
@ -82,30 +100,95 @@ def create(options):
print >>options.stdout, create_alias_options.stdout.getvalue() print >>options.stdout, create_alias_options.stdout.getvalue()
if options.nickname is not None: if options.nickname is not None:
print >>options.stdout, u"Inviting myself as client '{}':".format(options.nickname)
invite_options = _delegate_options(options, InviteOptions()) invite_options = _delegate_options(options, InviteOptions())
invite_options.alias = options.alias invite_options.alias = options.alias
invite_options.nickname = options.nickname invite_options.nickname = options.nickname
invite_options['name'] = options['name']
rc = invite(invite_options) rc = invite(invite_options)
if rc != 0: if rc != 0:
print >>options.stderr, "magic-folder: failed to invite after create\n" print >>options.stderr, u"magic-folder: failed to invite after create\n"
print >>options.stderr, invite_options.stderr.getvalue() print >>options.stderr, invite_options.stderr.getvalue()
return rc return rc
invite_code = invite_options.stdout.getvalue().strip() invite_code = invite_options.stdout.getvalue().strip()
print >>options.stdout, u" created invite code"
join_options = _delegate_options(options, JoinOptions()) join_options = _delegate_options(options, JoinOptions())
join_options['poll-interval'] = options['poll-interval'] join_options['poll-interval'] = options['poll-interval']
join_options.nickname = options.nickname
join_options.local_dir = options.local_dir join_options.local_dir = options.local_dir
join_options.invite_code = invite_code join_options.invite_code = invite_code
rc = join(join_options) rc = join(join_options)
if rc != 0: if rc != 0:
print >>options.stderr, "magic-folder: failed to join after create\n" print >>options.stderr, u"magic-folder: failed to join after create\n"
print >>options.stderr, join_options.stderr.getvalue() print >>options.stderr, join_options.stderr.getvalue()
return rc return rc
print >>options.stdout, u" joined new magic-folder"
print >>options.stdout, (
u"Successfully created magic-folder '{}' with alias '{}:' "
u"and client '{}'\nYou must re-start your node before the "
u"magic-folder will be active."
).format(options['name'], options.alias, options.nickname)
return 0 return 0
class ListOptions(BasedirOptions):
description = (
"List all magic-folders this client has joined"
)
optFlags = [
("json", "", "Produce JSON output")
]
def list_(options):
folders = load_magic_folders(options["node-directory"])
if options["json"]:
_list_json(options, folders)
return 0
_list_human(options, folders)
return 0
def _list_json(options, folders):
"""
List our magic-folders using JSON
"""
info = dict()
for name, details in folders.items():
info[name] = {
u"directory": details["directory"],
}
print >>options.stdout, json.dumps(info)
return 0
def _list_human(options, folders):
"""
List our magic-folders for a human user
"""
if folders:
print >>options.stdout, "This client has the following magic-folders:"
biggest = max([len(nm) for nm in folders.keys()])
fmt = " {:>%d}: {}" % (biggest, )
for name, details in folders.items():
print >>options.stdout, fmt.format(name, details["directory"])
else:
print >>options.stdout, "No magic-folders"
class InviteOptions(BasedirOptions): class InviteOptions(BasedirOptions):
nickname = None nickname = None
synopsis = "MAGIC_ALIAS: NICKNAME" synopsis = "MAGIC_ALIAS: NICKNAME"
stdin = StringIO("") stdin = StringIO("")
optParameters = [
("name", "n", "default", "The name of this magic-folder"),
]
description = (
"Invite a new participant to a given magic-folder. The resulting "
"invite-code that is printed is secret information and MUST be "
"transmitted securely to the invitee."
)
def parseArgs(self, alias, nickname=None): def parseArgs(self, alias, nickname=None):
BasedirOptions.parseArgs(self) BasedirOptions.parseArgs(self)
alias = argv_to_unicode(alias) alias = argv_to_unicode(alias)
@ -118,6 +201,7 @@ class InviteOptions(BasedirOptions):
aliases = get_aliases(self['node-directory']) aliases = get_aliases(self['node-directory'])
self.aliases = aliases self.aliases = aliases
def invite(options): def invite(options):
precondition(isinstance(options.alias, unicode), alias=options.alias) precondition(isinstance(options.alias, unicode), alias=options.alias)
precondition(isinstance(options.nickname, unicode), nickname=options.nickname) precondition(isinstance(options.nickname, unicode), nickname=options.nickname)
@ -161,6 +245,7 @@ class JoinOptions(BasedirOptions):
magic_readonly_cap = "" magic_readonly_cap = ""
optParameters = [ optParameters = [
("poll-interval", "p", "60", "How often to ask for updates"), ("poll-interval", "p", "60", "How often to ask for updates"),
("name", "n", "default", "Name of the magic-folder"),
] ]
def parseArgs(self, invite_code, local_dir): def parseArgs(self, invite_code, local_dir):
@ -183,57 +268,85 @@ def join(options):
raise usage.UsageError("Invalid invite code.") raise usage.UsageError("Invalid invite code.")
magic_readonly_cap, dmd_write_cap = fields magic_readonly_cap, dmd_write_cap = fields
dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap") maybe_upgrade_magic_folders(options["node-directory"])
collective_readcap_file = os.path.join(options["node-directory"], u"private", u"collective_dircap") existing_folders = load_magic_folders(options["node-directory"])
magic_folder_db_file = os.path.join(options["node-directory"], u"private", u"magicfolderdb.sqlite")
if os.path.exists(dmd_cap_file) or os.path.exists(collective_readcap_file) or os.path.exists(magic_folder_db_file): if options['name'] in existing_folders:
print >>options.stderr, ("\nThis client has already joined a magic folder." print >>options.stderr, "This client already has a magic-folder named '{}'".format(options['name'])
"\nUse the 'tahoe magic-folder leave' command first.\n")
return 1 return 1
fileutil.write(dmd_cap_file, dmd_write_cap) db_fname = os.path.join(
fileutil.write(collective_readcap_file, magic_readonly_cap) options["node-directory"],
u"private",
u"magicfolder_{}.sqlite".format(options['name']),
)
if os.path.exists(db_fname):
print >>options.stderr, "Database '{}' already exists; not overwriting".format(db_fname)
return 1
config = configutil.get_config(os.path.join(options["node-directory"], u"tahoe.cfg")) folder = {
configutil.set_config(config, "magic_folder", "enabled", "True") u"directory": options.local_dir.encode('utf-8'),
configutil.set_config(config, "magic_folder", "local.directory", options.local_dir.encode('utf-8')) u"collective_dircap": magic_readonly_cap,
configutil.set_config(config, "magic_folder", "poll_interval", options.get("poll-interval", "60")) u"upload_dircap": dmd_write_cap,
configutil.write_config(os.path.join(options["node-directory"], u"tahoe.cfg"), config) u"poll_interval": options["poll-interval"],
}
existing_folders[options["name"]] = folder
save_magic_folders(options["node-directory"], existing_folders)
return 0 return 0
class LeaveOptions(BasedirOptions): class LeaveOptions(BasedirOptions):
synopsis = "" synopsis = "Remove a magic-folder and forget all state"
def parseArgs(self): optParameters = [
BasedirOptions.parseArgs(self) ("name", "n", "default", "Name of magic-folder to leave"),
]
def leave(options): def leave(options):
from ConfigParser import SafeConfigParser from ConfigParser import SafeConfigParser
dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap") existing_folders = load_magic_folders(options["node-directory"])
collective_readcap_file = os.path.join(options["node-directory"], u"private", u"collective_dircap")
magic_folder_db_file = os.path.join(options["node-directory"], u"private", u"magicfolderdb.sqlite")
parser = SafeConfigParser() if not existing_folders:
parser.read(os.path.join(options["node-directory"], u"tahoe.cfg")) print >>options.stderr, "No magic-folders at all"
parser.remove_section("magic_folder") return 1
f = open(os.path.join(options["node-directory"], u"tahoe.cfg"), "w")
parser.write(f) if options["name"] not in existing_folders:
f.close() print >>options.stderr, "No such magic-folder '{}'".format(options["name"])
return 1
privdir = os.path.join(options["node-directory"], u"private")
db_fname = os.path.join(privdir, u"magicfolder_{}.sqlite".format(options["name"]))
# delete from YAML file and re-write it
del existing_folders[options["name"]]
save_magic_folders(options["node-directory"], existing_folders)
# delete the database file
try:
fileutil.remove(db_fname)
except Exception as e:
print >>options.stderr, ("Warning: unable to remove %s due to %s: %s"
% (quote_local_unicode_path(db_fname), e.__class__.__name__, str(e)))
# if this was the last magic-folder, disable them entirely
if not existing_folders:
parser = SafeConfigParser()
parser.read(os.path.join(options["node-directory"], u"tahoe.cfg"))
parser.remove_section("magic_folder")
with open(os.path.join(options["node-directory"], u"tahoe.cfg"), "w") as f:
parser.write(f)
for f in [dmd_cap_file, collective_readcap_file, magic_folder_db_file]:
try:
fileutil.remove(f)
except Exception as e:
print >>options.stderr, ("Warning: unable to remove %s due to %s: %s"
% (quote_local_unicode_path(f), e.__class__.__name__, str(e)))
# if this doesn't return 0, then the CLI stuff fails
return 0 return 0
class StatusOptions(BasedirOptions): class StatusOptions(BasedirOptions):
nickname = None
synopsis = "" synopsis = ""
stdin = StringIO("") stdin = StringIO("")
optParameters = [
("name", "n", "default", "Name for the magic-folder to show status"),
]
def parseArgs(self): def parseArgs(self):
BasedirOptions.parseArgs(self) BasedirOptions.parseArgs(self)
@ -311,15 +424,25 @@ def _print_item_status(item, now, longest):
print " %s: %s" % (paddedname, prog) print " %s: %s" % (paddedname, prog)
def status(options): def status(options):
nodedir = options["node-directory"] nodedir = options["node-directory"]
with open(os.path.join(nodedir, u"private", u"magic_folder_dircap")) as f: stdout, stderr = options.stdout, options.stderr
dmd_cap = f.read().strip() magic_folders = load_magic_folders(os.path.join(options["node-directory"]))
with open(os.path.join(nodedir, u"private", u"collective_dircap")) as f:
collective_readcap = f.read().strip()
with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'rb') as f: with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'rb') as f:
token = f.read() token = f.read()
print >>stdout, "Magic-folder status for '{}':".format(options["name"])
if options["name"] not in magic_folders:
raise Exception(
"No such magic-folder '{}'".format(options["name"])
)
dmd_cap = magic_folders[options["name"]]["upload_dircap"]
collective_readcap = magic_folders[options["name"]]["collective_dircap"]
# do *all* our data-retrievals first in case there's an error # do *all* our data-retrievals first in case there's an error
try: try:
dmd_data = _get_json_for_cap(options, dmd_cap) dmd_data = _get_json_for_cap(options, dmd_cap)
@ -330,6 +453,7 @@ def status(options):
method='POST', method='POST',
post_args=dict( post_args=dict(
t='json', t='json',
name=options["name"],
token=token, token=token,
) )
) )
@ -350,7 +474,7 @@ def status(options):
now = datetime.now() now = datetime.now()
print "Local files:" print >>stdout, "Local files:"
for (name, child) in dmd['children'].items(): for (name, child) in dmd['children'].items():
captype, meta = child captype, meta = child
status = 'good' status = 'good'
@ -360,28 +484,28 @@ def status(options):
nice_size = abbreviate_space(size) nice_size = abbreviate_space(size)
nice_created = abbreviate_time(now - created) nice_created = abbreviate_time(now - created)
if captype != 'filenode': if captype != 'filenode':
print "%20s: error, should be a filecap" % name print >>stdout, "%20s: error, should be a filecap" % name
continue continue
print " %s (%s): %s, version=%s, created %s" % (name, nice_size, status, version, nice_created) print >>stdout, " %s (%s): %s, version=%s, created %s" % (name, nice_size, status, version, nice_created)
print print >>stdout
print "Remote files:" print >>stdout, "Remote files:"
captype, collective = remote_data captype, collective = remote_data
for (name, data) in collective['children'].items(): for (name, data) in collective['children'].items():
if data[0] != 'dirnode': if data[0] != 'dirnode':
print "Error: '%s': expected a dirnode, not '%s'" % (name, data[0]) print >>stdout, "Error: '%s': expected a dirnode, not '%s'" % (name, data[0])
print " %s's remote:" % name print >>stdout, " %s's remote:" % name
dmd = _get_json_for_cap(options, data[1]['ro_uri']) dmd = _get_json_for_cap(options, data[1]['ro_uri'])
if isinstance(dmd, dict) and 'error' in dmd: if isinstance(dmd, dict) and 'error' in dmd:
print(" Error: could not retrieve directory") print >>stdout, " Error: could not retrieve directory"
continue continue
if dmd[0] != 'dirnode': if dmd[0] != 'dirnode':
print "Error: should be a dirnode" print >>stdout, "Error: should be a dirnode"
continue continue
for (n, d) in dmd[1]['children'].items(): for (n, d) in dmd[1]['children'].items():
if d[0] != 'filenode': if d[0] != 'filenode':
print "Error: expected '%s' to be a filenode." % (n,) print >>stdout, "Error: expected '%s' to be a filenode." % (n,)
meta = d[1] meta = d[1]
status = 'good' status = 'good'
@ -390,7 +514,7 @@ def status(options):
version = meta['metadata']['version'] version = meta['metadata']['version']
nice_size = abbreviate_space(size) nice_size = abbreviate_space(size)
nice_created = abbreviate_time(now - created) nice_created = abbreviate_time(now - created)
print " %s (%s): %s, version=%s, created %s" % (n, nice_size, status, version, nice_created) print >>stdout, " %s (%s): %s, version=%s, created %s" % (n, nice_size, status, version, nice_created)
if len(magic_data): if len(magic_data):
uploads = [item for item in magic_data if item['kind'] == 'upload'] uploads = [item for item in magic_data if item['kind'] == 'upload']
@ -403,19 +527,19 @@ def status(options):
if len(uploads): if len(uploads):
print print
print "Uploads:" print >>stdout, "Uploads:"
for item in uploads: for item in uploads:
_print_item_status(item, now, longest) _print_item_status(item, now, longest)
if len(downloads): if len(downloads):
print print
print "Downloads:" print >>stdout, "Downloads:"
for item in downloads: for item in downloads:
_print_item_status(item, now, longest) _print_item_status(item, now, longest)
for item in magic_data: for item in magic_data:
if item['status'] == 'failure': if item['status'] == 'failure':
print "Failed:", item print >>stdout, "Failed:", item
return 0 return 0
@ -427,21 +551,31 @@ class MagicFolderCommand(BaseOptions):
["join", None, JoinOptions, "Join a Magic Folder."], ["join", None, JoinOptions, "Join a Magic Folder."],
["leave", None, LeaveOptions, "Leave a Magic Folder."], ["leave", None, LeaveOptions, "Leave a Magic Folder."],
["status", None, StatusOptions, "Display status of uploads/downloads."], ["status", None, StatusOptions, "Display status of uploads/downloads."],
["list", None, ListOptions, "List Magic Folders configured in this client."],
] ]
optFlags = [ optFlags = [
["debug", "d", "Print full stack-traces"], ["debug", "d", "Print full stack-traces"],
] ]
description = (
"A magic-folder has an owner who controls the writecap "
"containing a list of nicknames and readcaps. The owner can invite "
"new participants. Every participant has the writecap for their "
"own folder (the corresponding readcap is in the master folder). "
"All clients download files from all other participants using the "
"readcaps contained in the master magic-folder directory."
)
def postOptions(self): def postOptions(self):
if not hasattr(self, 'subOptions'): if not hasattr(self, 'subOptions'):
raise usage.UsageError("must specify a subcommand") raise usage.UsageError("must specify a subcommand")
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-options] magic SUBCOMMAND" return "Usage: tahoe [global-options] magic-folder"
def getUsage(self, width=None): def getUsage(self, width=None):
t = BaseOptions.getUsage(self, width) t = BaseOptions.getUsage(self, width)
t += """\ t += (
Please run e.g. 'tahoe magic-folder create --help' for more details on each "Please run e.g. 'tahoe magic-folder create --help' for more "
subcommand. "details on each subcommand.\n"
""" )
return t return t
subDispatch = { subDispatch = {
@ -450,6 +584,7 @@ subDispatch = {
"join": join, "join": join,
"leave": leave, "leave": leave,
"status": status, "status": status,
"list": list_,
} }
def do_magic_folder(options): def do_magic_folder(options):
@ -460,7 +595,7 @@ def do_magic_folder(options):
try: try:
return f(so) return f(so)
except Exception as e: except Exception as e:
print("Error: %s" % (e,)) print >>options.stderr, "Error: %s" % (e,)
if options['debug']: if options['debug']:
raise raise

View File

@ -1,4 +1,7 @@
import json
import shutil
import os.path import os.path
import mock
import re import re
from twisted.trial import unittest from twisted.trial import unittest
@ -27,10 +30,12 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
self.bob_nickname = self.unicode_or_fallback(u"Bob\u00F8", u"Bob", io_as_well=True) self.bob_nickname = self.unicode_or_fallback(u"Bob\u00F8", u"Bob", io_as_well=True)
def do_create_magic_folder(self, client_num): def do_create_magic_folder(self, client_num):
d = self.do_cli("magic-folder", "create", "magic:", client_num=client_num) d = self.do_cli("magic-folder", "--debug", "create", "magic:", client_num=client_num)
def _done((rc,stdout,stderr)): def _done((rc,stdout,stderr)):
self.failUnlessEqual(rc, 0, stdout + stderr) self.failUnlessEqual(rc, 0, stdout + stderr)
self.failUnlessIn("Alias 'magic' created", stdout) self.failUnlessIn("Alias 'magic' created", stdout)
# self.failUnlessIn("joined new magic-folder", stdout)
# self.failUnlessIn("Successfully created magic-folder", stdout)
self.failUnlessEqual(stderr, "") self.failUnlessEqual(stderr, "")
aliases = get_aliases(self.get_clientdir(i=client_num)) aliases = get_aliases(self.get_clientdir(i=client_num))
self.failUnlessIn("magic", aliases) self.failUnlessIn("magic", aliases)
@ -47,6 +52,26 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
d.addCallback(_done) d.addCallback(_done)
return d return d
def do_list(self, client_num, json=False):
args = ("magic-folder", "list",)
if json:
args = args + ("--json",)
d = self.do_cli(*args, client_num=client_num)
def _done((rc, stdout, stderr)):
return (rc, stdout, stderr)
d.addCallback(_done)
return d
def do_status(self, client_num, name=None):
args = ("magic-folder", "status",)
if name is not None:
args = args + ("--name", name)
d = self.do_cli(*args, client_num=client_num)
def _done((rc, stdout, stderr)):
return (rc, stdout, stderr)
d.addCallback(_done)
return d
def do_join(self, client_num, local_dir, invite_code): def do_join(self, client_num, local_dir, invite_code):
precondition(isinstance(local_dir, unicode), local_dir=local_dir) precondition(isinstance(local_dir, unicode), local_dir=local_dir)
precondition(isinstance(invite_code, str), invite_code=invite_code) precondition(isinstance(invite_code, str), invite_code=invite_code)
@ -73,8 +98,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
"""Tests that our collective directory has the readonly cap of """Tests that our collective directory has the readonly cap of
our upload directory. our upload directory.
""" """
collective_readonly_cap = fileutil.read(os.path.join(self.get_clientdir(i=client_num), collective_readonly_cap = self.get_caps_from_files(client_num)[0]
u"private", u"collective_dircap"))
d = self.do_cli("ls", "--json", collective_readonly_cap, client_num=client_num) d = self.do_cli("ls", "--json", collective_readonly_cap, client_num=client_num)
def _done((rc, stdout, stderr)): def _done((rc, stdout, stderr)):
self.failUnlessEqual(rc, 0) self.failUnlessEqual(rc, 0)
@ -89,23 +113,24 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
return d return d
def get_caps_from_files(self, client_num): def get_caps_from_files(self, client_num):
collective_dircap = fileutil.read(os.path.join(self.get_clientdir(i=client_num), from allmydata.frontends.magic_folder import load_magic_folders
u"private", u"collective_dircap")) folders = load_magic_folders(self.get_clientdir(i=client_num))
upload_dircap = fileutil.read(os.path.join(self.get_clientdir(i=client_num), mf = folders["default"]
u"private", u"magic_folder_dircap")) return mf['collective_dircap'], mf['upload_dircap']
self.failIf(collective_dircap is None or upload_dircap is None)
return collective_dircap, upload_dircap
def check_config(self, client_num, local_dir): def check_config(self, client_num, local_dir):
client_config = fileutil.read(os.path.join(self.get_clientdir(i=client_num), "tahoe.cfg")) client_config = fileutil.read(os.path.join(self.get_clientdir(i=client_num), "tahoe.cfg"))
mf_yaml = fileutil.read(os.path.join(self.get_clientdir(i=client_num), "private", "magic_folders.yaml"))
local_dir_utf8 = local_dir.encode('utf-8') local_dir_utf8 = local_dir.encode('utf-8')
magic_folder_config = "[magic_folder]\nenabled = True\nlocal.directory = %s" % (local_dir_utf8,) magic_folder_config = "[magic_folder]\nenabled = True"
self.failUnlessIn(magic_folder_config, client_config) self.failUnlessIn(magic_folder_config, client_config)
self.failUnlessIn(local_dir_utf8, mf_yaml)
def create_invite_join_magic_folder(self, nickname, local_dir): def create_invite_join_magic_folder(self, nickname, local_dir):
nickname_arg = unicode_to_argv(nickname) nickname_arg = unicode_to_argv(nickname)
local_dir_arg = unicode_to_argv(local_dir) local_dir_arg = unicode_to_argv(local_dir)
d = self.do_cli("magic-folder", "create", "magic:", nickname_arg, local_dir_arg) # the --debug means we get real exceptions on failures
d = self.do_cli("magic-folder", "--debug", "create", "magic:", nickname_arg, local_dir_arg)
def _done((rc, stdout, stderr)): def _done((rc, stdout, stderr)):
self.failUnlessEqual(rc, 0, stdout + stderr) self.failUnlessEqual(rc, 0, stdout + stderr)
@ -132,7 +157,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
return d return d
def init_magicfolder(self, client_num, upload_dircap, collective_dircap, local_magic_dir, clock): def init_magicfolder(self, client_num, upload_dircap, collective_dircap, local_magic_dir, clock):
dbfile = abspath_expanduser_unicode(u"magicfolderdb.sqlite", base=self.get_clientdir(i=client_num)) dbfile = abspath_expanduser_unicode(u"magicfolder_default.sqlite", base=self.get_clientdir(i=client_num))
magicfolder = MagicFolder( magicfolder = MagicFolder(
client=self.get_client(client_num), client=self.get_client(client_num),
upload_dircap=upload_dircap, upload_dircap=upload_dircap,
@ -140,6 +165,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
local_path_u=local_magic_dir, local_path_u=local_magic_dir,
dbfile=dbfile, dbfile=dbfile,
umask=0o077, umask=0o077,
name='default',
clock=clock, clock=clock,
uploader_delay=0.2, uploader_delay=0.2,
downloader_delay=0, downloader_delay=0,
@ -199,11 +225,207 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
return d return d
class ListMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
@defer.inlineCallbacks
def setUp(self):
yield super(ListMagicFolder, self).setUp()
self.basedir="mf_list"
self.set_up_grid(oneshare=True)
self.local_dir = os.path.join(self.basedir, "magic")
os.mkdir(self.local_dir)
self.abs_local_dir_u = abspath_expanduser_unicode(unicode(self.local_dir), long_path=False)
yield self.do_create_magic_folder(0)
(rc, stdout, stderr) = yield self.do_invite(0, self.alice_nickname)
invite_code = stdout.strip()
yield self.do_join(0, unicode(self.local_dir), invite_code)
@defer.inlineCallbacks
def tearDown(self):
yield super(ListMagicFolder, self).tearDown()
shutil.rmtree(self.basedir)
@defer.inlineCallbacks
def test_list(self):
rc, stdout, stderr = yield self.do_list(0)
self.failUnlessEqual(rc, 0)
self.assertIn("default:", stdout)
@defer.inlineCallbacks
def test_list_none(self):
yield self.do_leave(0)
rc, stdout, stderr = yield self.do_list(0)
self.failUnlessEqual(rc, 0)
self.assertIn("No magic-folders", stdout)
@defer.inlineCallbacks
def test_list_json(self):
rc, stdout, stderr = yield self.do_list(0, json=True)
self.failUnlessEqual(rc, 0)
res = json.loads(stdout)
self.assertEqual(
dict(default=dict(directory=self.abs_local_dir_u)),
res,
)
class StatusMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
@defer.inlineCallbacks
def setUp(self):
yield super(StatusMagicFolder, self).setUp()
self.basedir="mf_list"
self.set_up_grid(oneshare=True)
self.local_dir = os.path.join(self.basedir, "magic")
os.mkdir(self.local_dir)
self.abs_local_dir_u = abspath_expanduser_unicode(unicode(self.local_dir), long_path=False)
yield self.do_create_magic_folder(0)
(rc, stdout, stderr) = yield self.do_invite(0, self.alice_nickname)
invite_code = stdout.strip()
yield self.do_join(0, unicode(self.local_dir), invite_code)
@defer.inlineCallbacks
def tearDown(self):
yield super(StatusMagicFolder, self).tearDown()
shutil.rmtree(self.basedir)
@defer.inlineCallbacks
def test_status(self):
def json_for_cap(options, cap):
if cap.startswith('URI:DIR2:'):
return (
'dirnode',
{
"children": {
"foo": ('filenode', {
"size": 1234,
"metadata": {
"tahoe": {
"linkcrtime": 0.0,
},
"version": 1,
},
"ro_uri": "read-only URI",
})
}
}
)
else:
return ('dirnode', {"children": {}})
jc = mock.patch(
"allmydata.scripts.magic_folder_cli._get_json_for_cap",
side_effect=json_for_cap,
)
def json_for_frag(options, fragment, method='GET', post_args=None):
return {}
jf = mock.patch(
"allmydata.scripts.magic_folder_cli._get_json_for_fragment",
side_effect=json_for_frag,
)
with jc, jf:
rc, stdout, stderr = yield self.do_status(0)
self.failUnlessEqual(rc, 0)
self.assertIn("default", stdout)
self.assertIn(
"foo (1.23 kB): good, version=1, created 47 years ago",
stdout,
)
@defer.inlineCallbacks
def test_status_child_not_dirnode(self):
def json_for_cap(options, cap):
if cap.startswith('URI:DIR2'):
return (
'dirnode',
{
"children": {
"foo": ('filenode', {
"size": 1234,
"metadata": {
"tahoe": {
"linkcrtime": 0.0,
},
"version": 1,
},
"ro_uri": "read-only URI",
})
}
}
)
elif cap == "read-only URI":
return {
"error": "bad stuff",
}
else:
return ('dirnode', {"children": {}})
jc = mock.patch(
"allmydata.scripts.magic_folder_cli._get_json_for_cap",
side_effect=json_for_cap,
)
def json_for_frag(options, fragment, method='GET', post_args=None):
return {}
jf = mock.patch(
"allmydata.scripts.magic_folder_cli._get_json_for_fragment",
side_effect=json_for_frag,
)
with jc, jf:
rc, stdout, stderr = yield self.do_status(0)
self.failUnlessEqual(rc, 0)
self.assertIn(
"expected a dirnode",
stdout + stderr,
)
@defer.inlineCallbacks
def test_status_error_not_dircap(self):
def json_for_cap(options, cap):
if cap.startswith('URI:DIR2:'):
return (
'filenode',
{}
)
else:
return ('dirnode', {"children": {}})
jc = mock.patch(
"allmydata.scripts.magic_folder_cli._get_json_for_cap",
side_effect=json_for_cap,
)
def json_for_frag(options, fragment, method='GET', post_args=None):
return {}
jf = mock.patch(
"allmydata.scripts.magic_folder_cli._get_json_for_fragment",
side_effect=json_for_frag,
)
with jc, jf:
rc, stdout, stderr = yield self.do_status(0)
self.failUnlessEqual(rc, 2)
self.assertIn(
"magic_folder_dircap isn't a directory capability",
stdout + stderr,
)
@defer.inlineCallbacks
def test_status_nothing(self):
rc, stdout, stderr = yield self.do_status(0, name="blam")
self.assertIn("No such magic-folder 'blam'", stderr)
class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase): class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
def test_create_and_then_invite_join(self): def test_create_and_then_invite_join(self):
self.basedir = "cli/MagicFolder/create-and-then-invite-join" self.basedir = "cli/MagicFolder/create-and-then-invite-join"
self.set_up_grid(oneshare=True) self.set_up_grid(oneshare=True)
local_dir = os.path.join(self.basedir, "magic") local_dir = os.path.join(self.basedir, "magic")
os.mkdir(local_dir)
abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False) abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False)
d = self.do_create_magic_folder(0) d = self.do_create_magic_folder(0)
@ -230,6 +452,94 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
d.addCallback(_done) d.addCallback(_done)
return d return d
@defer.inlineCallbacks
def test_create_duplicate_name(self):
self.basedir = "cli/MagicFolder/create-dup"
self.set_up_grid(oneshare=True)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "create", "magic:", "--name", "foo",
client_num=0,
)
self.assertEqual(rc, 0)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "create", "magic:", "--name", "foo",
client_num=0,
)
self.assertEqual(rc, 1)
self.assertIn(
"Already have a magic-folder named 'default'",
stderr
)
@defer.inlineCallbacks
def test_leave_wrong_folder(self):
self.basedir = "cli/MagicFolder/leave_wrong_folders"
yield self.set_up_grid(oneshare=True)
magic_dir = os.path.join(self.basedir, 'magic')
os.mkdir(magic_dir)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "create", "--name", "foo", "magic:", "my_name", magic_dir,
client_num=0,
)
self.assertEqual(rc, 0)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "leave", "--name", "bar",
client_num=0,
)
self.assertNotEqual(rc, 0)
self.assertIn(
"No such magic-folder 'bar'",
stdout + stderr,
)
@defer.inlineCallbacks
def test_leave_no_folder(self):
self.basedir = "cli/MagicFolder/leave_no_folders"
yield self.set_up_grid(oneshare=True)
magic_dir = os.path.join(self.basedir, 'magic')
os.mkdir(magic_dir)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "create", "--name", "foo", "magic:", "my_name", magic_dir,
client_num=0,
)
self.assertEqual(rc, 0)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "leave", "--name", "foo",
client_num=0,
)
self.assertEqual(rc, 0)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "leave", "--name", "foo",
client_num=0,
)
self.assertEqual(rc, 1)
self.assertIn(
"No magic-folders at all",
stderr,
)
@defer.inlineCallbacks
def test_leave_no_folders_at_all(self):
self.basedir = "cli/MagicFolder/leave_no_folders_at_all"
yield self.set_up_grid(oneshare=True)
rc, stdout, stderr = yield self.do_cli(
"magic-folder", "leave",
client_num=0,
)
self.assertEqual(rc, 1)
self.assertIn(
"No magic-folders at all",
stderr,
)
def test_create_invite_join(self): def test_create_invite_join(self):
self.basedir = "cli/MagicFolder/create-invite-join" self.basedir = "cli/MagicFolder/create-invite-join"
self.set_up_grid(oneshare=True) self.set_up_grid(oneshare=True)
@ -297,8 +607,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
def get_results(result): def get_results(result):
(rc, out, err) = result (rc, out, err) = result
self.failUnlessEqual(out, "") self.failUnlessEqual(out, "")
self.failUnlessIn("This client has already joined a magic folder.", err) self.failUnlessIn("This client already has a magic-folder", err)
self.failUnlessIn("Use the 'tahoe magic-folder leave' command first.", err)
self.failIfEqual(rc, 0) self.failIfEqual(rc, 0)
d.addCallback(get_results) d.addCallback(get_results)
return d return d
@ -339,6 +648,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
os.makedirs(self.basedir) os.makedirs(self.basedir)
self.set_up_grid(oneshare=True) self.set_up_grid(oneshare=True)
local_dir = os.path.join(self.basedir, "magic") local_dir = os.path.join(self.basedir, "magic")
os.mkdir(local_dir)
abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False) abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False)
self.invite_code = None self.invite_code = None
@ -357,7 +667,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
def check_success(result): def check_success(result):
(rc, out, err) = result (rc, out, err) = result
self.failUnlessEqual(rc, 0) self.failUnlessEqual(rc, 0, out + err)
def check_failure(result): def check_failure(result):
(rc, out, err) = result (rc, out, err) = result
self.failIfEqual(rc, 0) self.failIfEqual(rc, 0)
@ -367,9 +677,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
d.addCallback(leave) d.addCallback(leave)
d.addCallback(check_success) d.addCallback(check_success)
collective_dircap_file = os.path.join(self.get_clientdir(i=0), u"private", u"collective_dircap") magic_folder_db_file = os.path.join(self.get_clientdir(i=0), u"private", u"magicfolder_default.sqlite")
upload_dircap = os.path.join(self.get_clientdir(i=0), u"private", u"magic_folder_dircap")
magic_folder_db_file = os.path.join(self.get_clientdir(i=0), u"private", u"magicfolderdb.sqlite")
def check_join_if_file(my_file): def check_join_if_file(my_file):
fileutil.write(my_file, "my file data") fileutil.write(my_file, "my file data")
@ -377,10 +685,11 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
d2.addCallback(check_failure) d2.addCallback(check_failure)
return d2 return d2
for my_file in [collective_dircap_file, upload_dircap, magic_folder_db_file]: for my_file in [magic_folder_db_file]:
d.addCallback(lambda ign, my_file: check_join_if_file(my_file), my_file) d.addCallback(lambda ign, my_file: check_join_if_file(my_file), my_file)
d.addCallback(leave) d.addCallback(leave)
d.addCallback(check_success) # we didn't successfully join, so leaving should be an error
d.addCallback(check_failure)
return d return d

View File

@ -7,7 +7,7 @@ import allmydata
import allmydata.frontends.magic_folder import allmydata.frontends.magic_folder
import allmydata.util.log import allmydata.util.log
from allmydata.node import OldConfigError, OldConfigOptionError, MissingConfigEntry, UnescapedHashError, _Config, read_config from allmydata.node import OldConfigError, OldConfigOptionError, UnescapedHashError, _Config, read_config
from allmydata.frontends.auth import NeedRootcapLookupScheme from allmydata.frontends.auth import NeedRootcapLookupScheme
from allmydata import client from allmydata import client
from allmydata.storage_client import StorageFarmBroker from allmydata.storage_client import StorageFarmBroker
@ -322,8 +322,8 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
class MockMagicFolder(service.MultiService): class MockMagicFolder(service.MultiService):
name = 'magic-folder' name = 'magic-folder'
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, inotify=None, def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, name,
uploader_delay=1.0, clock=None, downloader_delay=3): inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3):
service.MultiService.__init__(self) service.MultiService.__init__(self)
self.client = client self.client = client
self._umask = umask self._umask = umask
@ -349,15 +349,20 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
basedir1 = "test_client.Basic.test_create_magic_folder_service1" basedir1 = "test_client.Basic.test_create_magic_folder_service1"
os.mkdir(basedir1) os.mkdir(basedir1)
os.mkdir(local_dir_u)
# which config-entry should be missing?
fileutil.write(os.path.join(basedir1, "tahoe.cfg"), fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
config + "local.directory = " + local_dir_utf8 + "\n") config + "local.directory = " + local_dir_utf8 + "\n")
self.failUnlessRaises(MissingConfigEntry, client.create_client, basedir1) self.failUnlessRaises(IOError, client.create_client, basedir1)
# local.directory entry missing .. but that won't be an error
# now, it'll just assume there are not magic folders
# .. hrm...should we make that an error (if enabled=true but
# there's not yaml AND no local.directory?)
fileutil.write(os.path.join(basedir1, "tahoe.cfg"), config) fileutil.write(os.path.join(basedir1, "tahoe.cfg"), config)
fileutil.write(os.path.join(basedir1, "private", "magic_folder_dircap"), "URI:DIR2:blah") fileutil.write(os.path.join(basedir1, "private", "magic_folder_dircap"), "URI:DIR2:blah")
fileutil.write(os.path.join(basedir1, "private", "collective_dircap"), "URI:DIR2:meow") fileutil.write(os.path.join(basedir1, "private", "collective_dircap"), "URI:DIR2:meow")
self.failUnlessRaises(MissingConfigEntry, client.create_client, basedir1)
fileutil.write(os.path.join(basedir1, "tahoe.cfg"), fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
config.replace("[magic_folder]\n", "[drop_upload]\n")) config.replace("[magic_folder]\n", "[drop_upload]\n"))
@ -376,7 +381,7 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
class Boom(Exception): class Boom(Exception):
pass pass
def BoomMagicFolder(client, upload_dircap, collective_dircap, local_path_u, dbfile, def BoomMagicFolder(client, upload_dircap, collective_dircap, local_path_u, dbfile, name,
umask, inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3): umask, inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3):
raise Boom() raise Boom()
self.patch(allmydata.frontends.magic_folder, 'MagicFolder', BoomMagicFolder) self.patch(allmydata.frontends.magic_folder, 'MagicFolder', BoomMagicFolder)

View File

@ -1,6 +1,7 @@
import os, sys, time import os, sys, time
import shutil, json import shutil, json
from os.path import join, exists
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import defer, task, reactor from twisted.internet import defer, task, reactor
@ -8,7 +9,7 @@ from twisted.internet import defer, task, reactor
from allmydata.interfaces import IDirectoryNode from allmydata.interfaces import IDirectoryNode
from allmydata.util.assertutil import precondition from allmydata.util.assertutil import precondition
from allmydata.util import fake_inotify, fileutil from allmydata.util import fake_inotify, fileutil, configutil, yamlutil
from allmydata.util.encodingutil import get_filesystem_encoding, to_filepath from allmydata.util.encodingutil import get_filesystem_encoding, to_filepath
from allmydata.util.consumer import download_to_data from allmydata.util.consumer import download_to_data
from allmydata.test.no_network import GridTestMixin from allmydata.test.no_network import GridTestMixin
@ -26,6 +27,259 @@ from allmydata.immutable.upload import Data
_debug = False _debug = False
class NewConfigUtilTests(unittest.TestCase):
def setUp(self):
self.basedir = abspath_expanduser_unicode(unicode(self.mktemp()))
os.mkdir(self.basedir)
self.local_dir = abspath_expanduser_unicode(unicode(self.mktemp()))
os.mkdir(self.local_dir)
privdir = join(self.basedir, "private")
os.mkdir(privdir)
self.poll_interval = 60
self.collective_dircap = u"a" * 32
self.magic_folder_dircap = u"b" * 32
self.folders = {
u"default": {
u"directory": self.local_dir,
u"upload_dircap": self.magic_folder_dircap,
u"collective_dircap": self.collective_dircap,
u"poll_interval": self.poll_interval,
}
}
# we need a bit of tahoe.cfg
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
f.write(
u"[magic_folder]\n"
u"enabled = True\n"
)
# ..and the yaml
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
with open(yaml_fname, "w") as f:
f.write(yamlutil.safe_dump({u"magic-folders": self.folders}))
def test_load(self):
folders = magic_folder.load_magic_folders(self.basedir)
self.assertEqual(['default'], list(folders.keys()))
def test_both_styles_of_config(self):
os.unlink(join(self.basedir, u"private", u"magic_folders.yaml"))
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"[magic_folder] is enabled but has no YAML file and no 'local.directory' option",
str(ctx.exception)
)
def test_wrong_obj(self):
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
with open(yaml_fname, "w") as f:
f.write('----\n')
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"should contain a dict",
str(ctx.exception)
)
def test_no_magic_folders(self):
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
with open(yaml_fname, "w") as f:
f.write('')
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"should contain a dict",
str(ctx.exception)
)
def test_magic_folders_not_dict(self):
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
with open(yaml_fname, "w") as f:
f.write('magic-folders: "foo"\n')
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"should be a dict",
str(ctx.exception)
)
self.assertIn(
"'magic-folders'",
str(ctx.exception)
)
def test_wrong_sub_obj(self):
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
with open(yaml_fname, "w") as f:
f.write("magic-folders:\n default: foo\n")
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"must itself be a dict",
str(ctx.exception)
)
def test_missing_interval(self):
del self.folders[u"default"]["poll_interval"]
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
with open(yaml_fname, "w") as f:
f.write(yamlutil.safe_dump({u"magic-folders": self.folders}))
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"missing 'poll_interval'",
str(ctx.exception)
)
class LegacyConfigUtilTests(unittest.TestCase):
def setUp(self):
# create a valid 'old style' magic-folder configuration
self.basedir = abspath_expanduser_unicode(unicode(self.mktemp()))
os.mkdir(self.basedir)
self.local_dir = abspath_expanduser_unicode(unicode(self.mktemp()))
os.mkdir(self.local_dir)
privdir = join(self.basedir, "private")
os.mkdir(privdir)
# state tests might need to know
self.poll_interval = 60
self.collective_dircap = u"a" * 32
self.magic_folder_dircap = u"b" * 32
# write fake config structure
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
f.write(
u"[magic_folder]\n"
u"enabled = True\n"
u"local.directory = {}\n"
u"poll_interval = {}\n".format(
self.local_dir,
self.poll_interval,
)
)
with open(join(privdir, "collective_dircap"), "w") as f:
f.write("{}\n".format(self.collective_dircap))
with open(join(privdir, "magic_folder_dircap"), "w") as f:
f.write("{}\n".format(self.magic_folder_dircap))
with open(join(privdir, "magicfolderdb.sqlite"), "w") as f:
pass
def test_load_legacy_no_dir(self):
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
f.write(
u"[magic_folder]\n"
u"enabled = True\n"
u"local.directory = {}\n"
u"poll_interval = {}\n".format(
self.local_dir + 'foo',
self.poll_interval,
)
)
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"there is no directory at that location",
str(ctx.exception)
)
def test_load_legacy_not_a_dir(self):
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
f.write(
u"[magic_folder]\n"
u"enabled = True\n"
u"local.directory = {}\n"
u"poll_interval = {}\n".format(
self.local_dir + "foo",
self.poll_interval,
)
)
with open(self.local_dir + "foo", "w") as f:
f.write("not a directory")
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"location is not a directory",
str(ctx.exception)
)
def test_load_legacy_and_new(self):
with open(join(self.basedir, u"private", u"magic_folders.yaml"), "w") as f:
f.write("---")
with self.assertRaises(Exception) as ctx:
magic_folder.load_magic_folders(self.basedir)
self.assertIn(
"both old-style configuration and new-style",
str(ctx.exception)
)
def test_upgrade(self):
# test data is created in setUp; upgrade config
magic_folder._upgrade_magic_folder_config(self.basedir)
# ensure old stuff is gone
self.assertFalse(
exists(join(self.basedir, "private", "collective_dircap"))
)
self.assertFalse(
exists(join(self.basedir, "private", "magic_folder_dircap"))
)
self.assertFalse(
exists(join(self.basedir, "private", "magicfolderdb.sqlite"))
)
# ensure we've got the new stuff
self.assertTrue(
exists(join(self.basedir, "private", "magicfolder_default.sqlite"))
)
# what about config?
config = configutil.get_config(join(self.basedir, u"tahoe.cfg"))
self.assertFalse(config.has_option("magic_folder", "local.directory"))
def test_load_legacy(self):
folders = magic_folder.load_magic_folders(self.basedir)
self.assertEqual(['default'], list(folders.keys()))
self.assertTrue(
exists(join(self.basedir, "private", "collective_dircap"))
)
self.assertTrue(
exists(join(self.basedir, "private", "magic_folder_dircap"))
)
self.assertTrue(
exists(join(self.basedir, "private", "magicfolderdb.sqlite"))
)
def test_load_legacy_upgrade(self):
magic_folder.maybe_upgrade_magic_folders(self.basedir)
folders = magic_folder.load_magic_folders(self.basedir)
self.assertEqual(['default'], list(folders.keys()))
# 'legacy' files should be gone
self.assertFalse(
exists(join(self.basedir, "private", "collective_dircap"))
)
self.assertFalse(
exists(join(self.basedir, "private", "magic_folder_dircap"))
)
self.assertFalse(
exists(join(self.basedir, "private", "magicfolderdb.sqlite"))
)
class MagicFolderDbTests(unittest.TestCase): class MagicFolderDbTests(unittest.TestCase):
def setUp(self): def setUp(self):
@ -1034,10 +1288,11 @@ class SingleMagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, Reall
return res return res
d.addBoth(_disable_debugging) d.addBoth(_disable_debugging)
d.addCallback(self.cleanup) d.addCallback(self.cleanup)
shutil.rmtree(self.basedir, ignore_errors=True)
return d return d
def _createdb(self): def _createdb(self):
dbfile = abspath_expanduser_unicode(u"magicfolderdb.sqlite", base=self.basedir) dbfile = abspath_expanduser_unicode(u"magicfolder_default.sqlite", base=self.basedir)
mdb = magicfolderdb.get_magicfolderdb(dbfile, create_version=(magicfolderdb.SCHEMA_v1, 1)) mdb = magicfolderdb.get_magicfolderdb(dbfile, create_version=(magicfolderdb.SCHEMA_v1, 1))
self.failUnless(mdb, "unable to create magicfolderdb from %r" % (dbfile,)) self.failUnless(mdb, "unable to create magicfolderdb from %r" % (dbfile,))
self.failUnlessEqual(mdb.VERSION, 1) self.failUnlessEqual(mdb.VERSION, 1)
@ -1051,7 +1306,7 @@ class SingleMagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, Reall
def _wait_until_started(self, ign): def _wait_until_started(self, ign):
#print "_wait_until_started" #print "_wait_until_started"
self.magicfolder = self.get_client().getServiceNamed('magic-folder') self.magicfolder = self.get_client().getServiceNamed('magic-folder-default')
self.fileops = FileOperationsHelper(self.magicfolder.uploader, self.inject_inotify) self.fileops = FileOperationsHelper(self.magicfolder.uploader, self.inject_inotify)
self.up_clock = task.Clock() self.up_clock = task.Clock()
self.down_clock = task.Clock() self.down_clock = task.Clock()
@ -1348,24 +1603,24 @@ class MockTest(SingleMagicFolderTestMixin, unittest.TestCase):
upload_dircap = n.get_uri() upload_dircap = n.get_uri()
readonly_dircap = n.get_readonly_uri() readonly_dircap = n.get_readonly_uri()
self.shouldFail(AssertionError, 'nonexistent local.directory', 'there is no directory', self.shouldFail(ValueError, 'does not exist', 'does not exist',
MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077) MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077, 'default')
self.shouldFail(AssertionError, 'non-directory local.directory', 'is not a directory', self.shouldFail(ValueError, 'is not a directory', 'is not a directory',
MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077) MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077, 'default')
self.shouldFail(AssertionError, 'bad upload.dircap', 'does not refer to a directory', self.shouldFail(AssertionError, 'bad upload.dircap', 'does not refer to a directory',
MagicFolder, client, 'bad', '', errors_dir, magicfolderdb, 0077) MagicFolder, client, 'bad', '', errors_dir, magicfolderdb, 0077, 'default')
self.shouldFail(AssertionError, 'non-directory upload.dircap', 'does not refer to a directory', self.shouldFail(AssertionError, 'non-directory upload.dircap', 'does not refer to a directory',
MagicFolder, client, 'URI:LIT:foo', '', errors_dir, magicfolderdb, 0077) MagicFolder, client, 'URI:LIT:foo', '', errors_dir, magicfolderdb, 0077, 'default')
self.shouldFail(AssertionError, 'readonly upload.dircap', 'is not a writecap to a directory', self.shouldFail(AssertionError, 'readonly upload.dircap', 'is not a writecap to a directory',
MagicFolder, client, readonly_dircap, '', errors_dir, magicfolderdb, 0077) MagicFolder, client, readonly_dircap, '', errors_dir, magicfolderdb, 0077, 'default')
self.shouldFail(AssertionError, 'collective dircap', 'is not a readonly cap to a directory', self.shouldFail(AssertionError, 'collective dircap', 'is not a readonly cap to a directory',
MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077) MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077, 'default')
def _not_implemented(): def _not_implemented():
raise NotImplementedError("blah") raise NotImplementedError("blah")
self.patch(magic_folder, 'get_inotify_module', _not_implemented) self.patch(magic_folder, 'get_inotify_module', _not_implemented)
self.shouldFail(NotImplementedError, 'unsupported', 'blah', self.shouldFail(NotImplementedError, 'unsupported', 'blah',
MagicFolder, client, upload_dircap, '', errors_dir, magicfolderdb, 0077) MagicFolder, client, upload_dircap, '', errors_dir, magicfolderdb, 0077, 'default')
d.addCallback(_check_errors) d.addCallback(_check_errors)
return d return d

View File

@ -109,6 +109,44 @@ class FakeUploader(service.Service):
return (self.helper_furl, self.helper_connected) return (self.helper_furl, self.helper_connected)
class FakeStatus(object):
def __init__(self):
self.status = []
def setServiceParent(self, p):
pass
def get_status(self):
return self.status
class FakeStatusItem(object):
def __init__(self, p, history):
self.relpath_u = p
self.history = history
import mock
self.progress = mock.Mock()
self.progress.progress = 100.0
def status_history(self):
return self.history
class FakeMagicFolder(object):
def __init__(self):
self.uploader = FakeStatus()
self.downloader = FakeStatus()
def get_public_status(self):
return (
True,
[
'a magic-folder status message'
],
)
def build_one_ds(): def build_one_ds():
ds = DownloadStatus("storage_index", 1234) ds = DownloadStatus("storage_index", 1234)
now = time.time() now = time.time()
@ -243,7 +281,7 @@ class FakeClient(_Client):
# don't upcall to Client.__init__, since we only want to initialize a # don't upcall to Client.__init__, since we only want to initialize a
# minimal subset # minimal subset
service.MultiService.__init__(self) service.MultiService.__init__(self)
self._magic_folder = None self._magic_folders = dict()
self.all_contents = {} self.all_contents = {}
self.nodeid = "fake_nodeid" self.nodeid = "fake_nodeid"
self.nickname = u"fake_nickname \u263A" self.nickname = u"fake_nickname \u263A"
@ -281,6 +319,9 @@ class FakeClient(_Client):
def get_long_tubid(self): def get_long_tubid(self):
return "tubid" return "tubid"
def get_auth_token(self):
return 'a fake debug auth token'
def startService(self): def startService(self):
return service.MultiService.startService(self) return service.MultiService.startService(self)
def stopService(self): def stopService(self):
@ -936,6 +977,61 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_check) d.addCallback(_check)
return d return d
@defer.inlineCallbacks
def test_magicfolder_status_bad_token(self):
with self.assertRaises(Error):
yield self.POST(
'/magic_folder?t=json',
t='json',
name='default',
token='not the token you are looking for',
)
@defer.inlineCallbacks
def test_magicfolder_status_wrong_folder(self):
with self.assertRaises(Exception) as ctx:
yield self.POST(
'/magic_folder?t=json',
t='json',
name='a non-existent magic-folder',
token=self.s.get_auth_token(),
)
self.assertIn(
"Not Found",
str(ctx.exception)
)
@defer.inlineCallbacks
def test_magicfolder_status_success(self):
self.s._magic_folders['default'] = mf = FakeMagicFolder()
mf.uploader.status = [
FakeStatusItem(u"rel/path", [('done', 12345)])
]
data = yield self.POST(
'/magic_folder?t=json',
t='json',
name='default',
token=self.s.get_auth_token(),
)
data = json.loads(data)
self.assertEqual(
data,
[
{"status": "done", "path": "rel/path", "kind": "upload", "percent_done": 100.0, "done_at": 12345},
]
)
@defer.inlineCallbacks
def test_magicfolder_root_success(self):
self.s._magic_folders['default'] = mf = FakeMagicFolder()
mf.uploader.status = [
FakeStatusItem(u"rel/path", [('done', 12345)])
]
data = yield self.GET(
'/',
)
print(data)
def test_status(self): def test_status(self):
h = self.s.get_history() h = self.s.get_history()
dl_num = h.list_all_download_statuses()[0].get_counter() dl_num = h.list_all_download_statuses()[0].get_counter()

View File

@ -488,9 +488,12 @@ class TokenOnlyWebApi(resource.Resource):
if t == u'json': if t == u'json':
try: try:
return self.post_json(req) return self.post_json(req)
except Exception: except WebError as e:
req.setResponseCode(e.code)
return json.dumps({"error": e.text})
except Exception as e:
message, code = humanize_failure(Failure()) message, code = humanize_failure(Failure())
req.setResponseCode(code) req.setResponseCode(500 if code is None else code)
return json.dumps({"error": message}) return json.dumps({"error": message})
else: else:
raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST) raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST)

View File

@ -1,6 +1,6 @@
import json import json
from allmydata.web.common import TokenOnlyWebApi from allmydata.web.common import TokenOnlyWebApi, get_arg, WebError
class MagicFolderWebApi(TokenOnlyWebApi): class MagicFolderWebApi(TokenOnlyWebApi):
@ -14,9 +14,18 @@ class MagicFolderWebApi(TokenOnlyWebApi):
def post_json(self, req): def post_json(self, req):
req.setHeader("content-type", "application/json") req.setHeader("content-type", "application/json")
nick = get_arg(req, 'name', 'default')
try:
magic_folder = self.client._magic_folders[nick]
except KeyError:
raise WebError(
"No such magic-folder '{}'".format(nick),
404,
)
data = [] data = []
for item in self.client._magic_folder.uploader.get_status(): for item in magic_folder.uploader.get_status():
d = dict( d = dict(
path=item.relpath_u, path=item.relpath_u,
status=item.status_history()[-1][0], status=item.status_history()[-1][0],
@ -27,7 +36,7 @@ class MagicFolderWebApi(TokenOnlyWebApi):
d['percent_done'] = item.progress.progress d['percent_done'] = item.progress.progress
data.append(d) data.append(d)
for item in self.client._magic_folder.downloader.get_status(): for item in magic_folder.downloader.get_status():
d = dict( d = dict(
path=item.relpath_u, path=item.relpath_u,
status=item.status_history()[-1][0], status=item.status_history()[-1][0],

View File

@ -237,12 +237,13 @@ class Root(MultiFormatPage):
return description return description
def render_magic_folder(self, ctx, data): def data_magic_folders(self, ctx, data):
if self.client._magic_folder is None: return self.client._magic_folders.keys()
return T.p()
(ok, messages) = self.client._magic_folder.get_public_status()
def render_magic_folder_row(self, ctx, data):
magic_folder = self.client._magic_folders[data]
(ok, messages) = magic_folder.get_public_status()
ctx.fillSlots("magic_folder_name", data)
if ok: if ok:
ctx.fillSlots("magic_folder_status", "yes") ctx.fillSlots("magic_folder_status", "yes")
ctx.fillSlots("magic_folder_status_alt", "working") ctx.fillSlots("magic_folder_status_alt", "working")
@ -250,12 +251,16 @@ class Root(MultiFormatPage):
ctx.fillSlots("magic_folder_status", "no") ctx.fillSlots("magic_folder_status", "no")
ctx.fillSlots("magic_folder_status_alt", "not working") ctx.fillSlots("magic_folder_status_alt", "not working")
status = T.ul() status = T.ul(class_="magic-folder-status")
for msg in messages: for msg in messages:
status[T.li[str(msg)]] status[T.li[str(msg)]]
return ctx.tag[status] return ctx.tag[status]
def render_magic_folder(self, ctx, data):
if not self.client._magic_folders:
return T.p()
return ctx.tag
def render_services(self, ctx, data): def render_services(self, ctx, data):
ul = T.ul() ul = T.ul()
try: try:

View File

@ -53,6 +53,11 @@ body {
.connection-status { .connection-status {
} }
.magic-folder-status {
clear: left;
margin-left: 40px; /* width of status-indicator + margins */
}
.furl { .furl {
font-size: 0.8em; font-size: 0.8em;
word-wrap: break-word; word-wrap: break-word;

View File

@ -160,10 +160,10 @@
</div> </div>
<div n:render="magic_folder" class="row-fluid"> <div n:render="magic_folder" class="row-fluid">
<h2> <h2>Magic Folders</h2>
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="magic_folder_status" />.png</n:attr><n:attr name="alt"><n:slot name="magic_folder_status_alt" /></n:attr></img></div> <div n:render="sequence" n:data="magic_folders">
Magic Folder <div n:pattern="item" n:render="magic_folder_row"><div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="magic_folder_status" />.png</n:attr><n:attr name="alt"><n:slot name="magic_folder_status_alt" /></n:attr></img></div><h3><n:slot name="magic_folder_name" /></h3></div>
</h2> </div>
</div><!--/row--> </div><!--/row-->
<div class="row-fluid"> <div class="row-fluid">