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
def create_client(basedir=u"."):
from allmydata.node import read_config
config = read_config(basedir, u"client.port")
config.validate(_valid_config_sections())
config = read_config(basedir, u"client.port", _valid_config_sections=_valid_config_sections)
#defer.returnValue(
return _Client(
config,
@ -192,7 +191,7 @@ class _Client(node.Node, pollmixin.PollMixin):
node.Node.__init__(self, config, basedir=basedir)
# All tub.registerReference must happen *after* we upcall, since
# that's what does tub.setLocation()
self._magic_folder = None
self._magic_folders = dict()
self.started_timestamp = time.time()
self.logSource="Client"
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.")
if self.get_config("magic_folder", "enabled", False, boolean=True):
#print "magic folder enabled"
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:
poll_interval = int(self.get_config("magic_folder", "poll_interval", 3))
except ValueError:
raise ValueError("[magic_folder] poll_interval must be an int")
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()
magic_folders = magic_folder.load_magic_folders(self.basedir)
except Exception as e:
log.msg("Error loading magic-folder config: {}".format(e))
raise
# start processing the upload queue when we've connected to
# enough servers
threshold = min(self.encoding_params["k"],
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):
if os.path.exists(exit_trigger_file):

View File

@ -4,6 +4,7 @@ import os.path
from collections import deque
from datetime import datetime
import time
import ConfigParser
from twisted.internet import defer, reactor, task
from twisted.internet.error import AlreadyCancelled
@ -14,7 +15,7 @@ from twisted.application import service
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.util import log
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))
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):
name = 'magic-folder'
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)
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)
@ -149,14 +320,10 @@ class QueueMixin(HookMixin):
}
self.started_d = self.set_hook('started')
if not self._local_filepath.exists():
raise AssertionError("The '[magic_folder] local.directory' parameter was %s "
"but there is no directory at that location."
% quote_local_unicode_path(self._local_path_u))
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))
# we should have gotten nice errors already while loading the
# config, but just to be safe:
assert self._local_filepath.exists()
assert self._local_filepath.isdir()
self._deque = deque()
# do we also want to bound on "maximum age"?
@ -343,11 +510,9 @@ class Uploader(QueueMixin):
self.is_ready = False
if not IDirectoryNode.providedBy(upload_dirnode):
raise AssertionError("The URI in '%s' does not refer to a directory."
% os.path.join('private', 'magic_folder_dircap'))
raise AssertionError("'upload_dircap' does not refer to a directory")
if upload_dirnode.is_unknown() or upload_dirnode.is_readonly():
raise AssertionError("The URI in '%s' is not a writecap to a directory."
% os.path.join('private', 'magic_folder_dircap'))
raise AssertionError("'upload_dircap' is not a writecap to a directory")
self._upload_dirnode = upload_dirnode
self._inotify = get_inotify_module()
@ -756,11 +921,9 @@ class Downloader(QueueMixin, WriteFileMixin):
QueueMixin.__init__(self, client, local_path_u, db, 'downloader', clock)
if not IDirectoryNode.providedBy(collective_dirnode):
raise AssertionError("The URI in '%s' does not refer to a directory."
% os.path.join('private', 'collective_dircap'))
raise AssertionError("'collective_dircap' does not refer to a directory")
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."
% os.path.join('private', 'collective_dircap'))
raise AssertionError("'collective_dircap' is not a readonly cap to a directory")
self._collective_dirnode = collective_dirnode
self._upload_readonly_dircap = upload_readonly_dircap
@ -778,7 +941,7 @@ class Downloader(QueueMixin, WriteFileMixin):
while True:
try:
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)
defer.returnValue(x)
break

View File

@ -1,7 +1,6 @@
import os
import urllib
from sys import stderr
from types import NoneType
from cStringIO import StringIO
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
from allmydata.scripts.common_http import do_http, BadResponse
from allmydata.util import fileutil
from allmydata.util import configutil
from allmydata import uri
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 = "+"
class CreateOptions(BasedirOptions):
nickname = None
nickname = None # NOTE: *not* the "name of this magic-folder"
local_dir = None
synopsis = "MAGIC_ALIAS: [NICKNAME LOCAL_DIR]"
optParameters = [
("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):
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["node-url"] = source_options["node-url"]
target_options["node-directory"] = source_options["node-directory"]
target_options["name"] = source_options["name"]
target_options.stdin = StringIO("")
target_options.stdout = StringIO()
target_options.stderr = StringIO()
@ -71,6 +80,15 @@ def create(options):
precondition(isinstance(options.nickname, (unicode, NoneType)), nickname=options.nickname)
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
create_alias_options = _delegate_options(options, CreateAliasOptions())
create_alias_options.alias = options.alias
@ -82,30 +100,95 @@ def create(options):
print >>options.stdout, create_alias_options.stdout.getvalue()
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.alias = options.alias
invite_options.nickname = options.nickname
invite_options['name'] = options['name']
rc = invite(invite_options)
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()
return rc
invite_code = invite_options.stdout.getvalue().strip()
print >>options.stdout, u" created invite code"
join_options = _delegate_options(options, JoinOptions())
join_options['poll-interval'] = options['poll-interval']
join_options.nickname = options.nickname
join_options.local_dir = options.local_dir
join_options.invite_code = invite_code
rc = join(join_options)
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()
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
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):
nickname = None
synopsis = "MAGIC_ALIAS: NICKNAME"
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):
BasedirOptions.parseArgs(self)
alias = argv_to_unicode(alias)
@ -118,6 +201,7 @@ class InviteOptions(BasedirOptions):
aliases = get_aliases(self['node-directory'])
self.aliases = aliases
def invite(options):
precondition(isinstance(options.alias, unicode), alias=options.alias)
precondition(isinstance(options.nickname, unicode), nickname=options.nickname)
@ -161,6 +245,7 @@ class JoinOptions(BasedirOptions):
magic_readonly_cap = ""
optParameters = [
("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):
@ -183,57 +268,85 @@ def join(options):
raise usage.UsageError("Invalid invite code.")
magic_readonly_cap, dmd_write_cap = fields
dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap")
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")
maybe_upgrade_magic_folders(options["node-directory"])
existing_folders = load_magic_folders(options["node-directory"])
if os.path.exists(dmd_cap_file) or os.path.exists(collective_readcap_file) or os.path.exists(magic_folder_db_file):
print >>options.stderr, ("\nThis client has already joined a magic folder."
"\nUse the 'tahoe magic-folder leave' command first.\n")
if options['name'] in existing_folders:
print >>options.stderr, "This client already has a magic-folder named '{}'".format(options['name'])
return 1
fileutil.write(dmd_cap_file, dmd_write_cap)
fileutil.write(collective_readcap_file, magic_readonly_cap)
db_fname = os.path.join(
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"))
configutil.set_config(config, "magic_folder", "enabled", "True")
configutil.set_config(config, "magic_folder", "local.directory", options.local_dir.encode('utf-8'))
configutil.set_config(config, "magic_folder", "poll_interval", options.get("poll-interval", "60"))
configutil.write_config(os.path.join(options["node-directory"], u"tahoe.cfg"), config)
folder = {
u"directory": options.local_dir.encode('utf-8'),
u"collective_dircap": magic_readonly_cap,
u"upload_dircap": dmd_write_cap,
u"poll_interval": options["poll-interval"],
}
existing_folders[options["name"]] = folder
save_magic_folders(options["node-directory"], existing_folders)
return 0
class LeaveOptions(BasedirOptions):
synopsis = ""
def parseArgs(self):
BasedirOptions.parseArgs(self)
synopsis = "Remove a magic-folder and forget all state"
optParameters = [
("name", "n", "default", "Name of magic-folder to leave"),
]
def leave(options):
from ConfigParser import SafeConfigParser
dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap")
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")
existing_folders = load_magic_folders(options["node-directory"])
parser = SafeConfigParser()
parser.read(os.path.join(options["node-directory"], u"tahoe.cfg"))
parser.remove_section("magic_folder")
f = open(os.path.join(options["node-directory"], u"tahoe.cfg"), "w")
parser.write(f)
f.close()
if not existing_folders:
print >>options.stderr, "No magic-folders at all"
return 1
if options["name"] not in existing_folders:
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
class StatusOptions(BasedirOptions):
nickname = None
synopsis = ""
stdin = StringIO("")
optParameters = [
("name", "n", "default", "Name for the magic-folder to show status"),
]
def parseArgs(self):
BasedirOptions.parseArgs(self)
@ -311,15 +424,25 @@ def _print_item_status(item, now, longest):
print " %s: %s" % (paddedname, prog)
def status(options):
nodedir = options["node-directory"]
with open(os.path.join(nodedir, u"private", u"magic_folder_dircap")) as f:
dmd_cap = f.read().strip()
with open(os.path.join(nodedir, u"private", u"collective_dircap")) as f:
collective_readcap = f.read().strip()
stdout, stderr = options.stdout, options.stderr
magic_folders = load_magic_folders(os.path.join(options["node-directory"]))
with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'rb') as f:
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
try:
dmd_data = _get_json_for_cap(options, dmd_cap)
@ -330,6 +453,7 @@ def status(options):
method='POST',
post_args=dict(
t='json',
name=options["name"],
token=token,
)
)
@ -350,7 +474,7 @@ def status(options):
now = datetime.now()
print "Local files:"
print >>stdout, "Local files:"
for (name, child) in dmd['children'].items():
captype, meta = child
status = 'good'
@ -360,28 +484,28 @@ def status(options):
nice_size = abbreviate_space(size)
nice_created = abbreviate_time(now - created)
if captype != 'filenode':
print "%20s: error, should be a filecap" % name
print >>stdout, "%20s: error, should be a filecap" % name
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 "Remote files:"
print >>stdout
print >>stdout, "Remote files:"
captype, collective = remote_data
for (name, data) in collective['children'].items():
if data[0] != 'dirnode':
print "Error: '%s': expected a dirnode, not '%s'" % (name, data[0])
print " %s's remote:" % name
print >>stdout, "Error: '%s': expected a dirnode, not '%s'" % (name, data[0])
print >>stdout, " %s's remote:" % name
dmd = _get_json_for_cap(options, data[1]['ro_uri'])
if isinstance(dmd, dict) and 'error' in dmd:
print(" Error: could not retrieve directory")
print >>stdout, " Error: could not retrieve directory"
continue
if dmd[0] != 'dirnode':
print "Error: should be a dirnode"
print >>stdout, "Error: should be a dirnode"
continue
for (n, d) in dmd[1]['children'].items():
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]
status = 'good'
@ -390,7 +514,7 @@ def status(options):
version = meta['metadata']['version']
nice_size = abbreviate_space(size)
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):
uploads = [item for item in magic_data if item['kind'] == 'upload']
@ -403,19 +527,19 @@ def status(options):
if len(uploads):
print
print "Uploads:"
print >>stdout, "Uploads:"
for item in uploads:
_print_item_status(item, now, longest)
if len(downloads):
print
print "Downloads:"
print >>stdout, "Downloads:"
for item in downloads:
_print_item_status(item, now, longest)
for item in magic_data:
if item['status'] == 'failure':
print "Failed:", item
print >>stdout, "Failed:", item
return 0
@ -427,21 +551,31 @@ class MagicFolderCommand(BaseOptions):
["join", None, JoinOptions, "Join a Magic Folder."],
["leave", None, LeaveOptions, "Leave a Magic Folder."],
["status", None, StatusOptions, "Display status of uploads/downloads."],
["list", None, ListOptions, "List Magic Folders configured in this client."],
]
optFlags = [
["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):
if not hasattr(self, 'subOptions'):
raise usage.UsageError("must specify a subcommand")
def getSynopsis(self):
return "Usage: tahoe [global-options] magic SUBCOMMAND"
return "Usage: tahoe [global-options] magic-folder"
def getUsage(self, width=None):
t = BaseOptions.getUsage(self, width)
t += """\
Please run e.g. 'tahoe magic-folder create --help' for more details on each
subcommand.
"""
t += (
"Please run e.g. 'tahoe magic-folder create --help' for more "
"details on each subcommand.\n"
)
return t
subDispatch = {
@ -450,6 +584,7 @@ subDispatch = {
"join": join,
"leave": leave,
"status": status,
"list": list_,
}
def do_magic_folder(options):
@ -460,7 +595,7 @@ def do_magic_folder(options):
try:
return f(so)
except Exception as e:
print("Error: %s" % (e,))
print >>options.stderr, "Error: %s" % (e,)
if options['debug']:
raise

View File

@ -1,4 +1,7 @@
import json
import shutil
import os.path
import mock
import re
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)
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)):
self.failUnlessEqual(rc, 0, stdout + stderr)
self.failUnlessIn("Alias 'magic' created", stdout)
# self.failUnlessIn("joined new magic-folder", stdout)
# self.failUnlessIn("Successfully created magic-folder", stdout)
self.failUnlessEqual(stderr, "")
aliases = get_aliases(self.get_clientdir(i=client_num))
self.failUnlessIn("magic", aliases)
@ -47,6 +52,26 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
d.addCallback(_done)
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):
precondition(isinstance(local_dir, unicode), local_dir=local_dir)
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
our upload directory.
"""
collective_readonly_cap = fileutil.read(os.path.join(self.get_clientdir(i=client_num),
u"private", u"collective_dircap"))
collective_readonly_cap = self.get_caps_from_files(client_num)[0]
d = self.do_cli("ls", "--json", collective_readonly_cap, client_num=client_num)
def _done((rc, stdout, stderr)):
self.failUnlessEqual(rc, 0)
@ -89,23 +113,24 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
return d
def get_caps_from_files(self, client_num):
collective_dircap = fileutil.read(os.path.join(self.get_clientdir(i=client_num),
u"private", u"collective_dircap"))
upload_dircap = fileutil.read(os.path.join(self.get_clientdir(i=client_num),
u"private", u"magic_folder_dircap"))
self.failIf(collective_dircap is None or upload_dircap is None)
return collective_dircap, upload_dircap
from allmydata.frontends.magic_folder import load_magic_folders
folders = load_magic_folders(self.get_clientdir(i=client_num))
mf = folders["default"]
return mf['collective_dircap'], mf['upload_dircap']
def check_config(self, client_num, local_dir):
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')
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(local_dir_utf8, mf_yaml)
def create_invite_join_magic_folder(self, nickname, local_dir):
nickname_arg = unicode_to_argv(nickname)
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)):
self.failUnlessEqual(rc, 0, stdout + stderr)
@ -132,7 +157,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
return d
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(
client=self.get_client(client_num),
upload_dircap=upload_dircap,
@ -140,6 +165,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
local_path_u=local_magic_dir,
dbfile=dbfile,
umask=0o077,
name='default',
clock=clock,
uploader_delay=0.2,
downloader_delay=0,
@ -199,11 +225,207 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
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):
def test_create_and_then_invite_join(self):
self.basedir = "cli/MagicFolder/create-and-then-invite-join"
self.set_up_grid(oneshare=True)
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)
d = self.do_create_magic_folder(0)
@ -230,6 +452,94 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
d.addCallback(_done)
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):
self.basedir = "cli/MagicFolder/create-invite-join"
self.set_up_grid(oneshare=True)
@ -297,8 +607,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
def get_results(result):
(rc, out, err) = result
self.failUnlessEqual(out, "")
self.failUnlessIn("This client has already joined a magic folder.", err)
self.failUnlessIn("Use the 'tahoe magic-folder leave' command first.", err)
self.failUnlessIn("This client already has a magic-folder", err)
self.failIfEqual(rc, 0)
d.addCallback(get_results)
return d
@ -339,6 +648,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
os.makedirs(self.basedir)
self.set_up_grid(oneshare=True)
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)
self.invite_code = None
@ -357,7 +667,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
def check_success(result):
(rc, out, err) = result
self.failUnlessEqual(rc, 0)
self.failUnlessEqual(rc, 0, out + err)
def check_failure(result):
(rc, out, err) = result
self.failIfEqual(rc, 0)
@ -367,9 +677,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
d.addCallback(leave)
d.addCallback(check_success)
collective_dircap_file = os.path.join(self.get_clientdir(i=0), u"private", u"collective_dircap")
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")
magic_folder_db_file = os.path.join(self.get_clientdir(i=0), u"private", u"magicfolder_default.sqlite")
def check_join_if_file(my_file):
fileutil.write(my_file, "my file data")
@ -377,10 +685,11 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
d2.addCallback(check_failure)
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(leave)
d.addCallback(check_success)
# we didn't successfully join, so leaving should be an error
d.addCallback(check_failure)
return d

View File

@ -7,7 +7,7 @@ import allmydata
import allmydata.frontends.magic_folder
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 import client
from allmydata.storage_client import StorageFarmBroker
@ -322,8 +322,8 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
class MockMagicFolder(service.MultiService):
name = 'magic-folder'
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, inotify=None,
uploader_delay=1.0, clock=None, downloader_delay=3):
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, name,
inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3):
service.MultiService.__init__(self)
self.client = client
self._umask = umask
@ -349,15 +349,20 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
basedir1 = "test_client.Basic.test_create_magic_folder_service1"
os.mkdir(basedir1)
os.mkdir(local_dir_u)
# which config-entry should be missing?
fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
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, "private", "magic_folder_dircap"), "URI:DIR2:blah")
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"),
config.replace("[magic_folder]\n", "[drop_upload]\n"))
@ -376,7 +381,7 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
class Boom(Exception):
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):
raise Boom()
self.patch(allmydata.frontends.magic_folder, 'MagicFolder', BoomMagicFolder)

View File

@ -1,6 +1,7 @@
import os, sys, time
import shutil, json
from os.path import join, exists
from twisted.trial import unittest
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.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.consumer import download_to_data
from allmydata.test.no_network import GridTestMixin
@ -26,6 +27,259 @@ from allmydata.immutable.upload import Data
_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):
def setUp(self):
@ -1034,10 +1288,11 @@ class SingleMagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, Reall
return res
d.addBoth(_disable_debugging)
d.addCallback(self.cleanup)
shutil.rmtree(self.basedir, ignore_errors=True)
return d
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))
self.failUnless(mdb, "unable to create magicfolderdb from %r" % (dbfile,))
self.failUnlessEqual(mdb.VERSION, 1)
@ -1051,7 +1306,7 @@ class SingleMagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, Reall
def _wait_until_started(self, ign):
#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.up_clock = task.Clock()
self.down_clock = task.Clock()
@ -1348,24 +1603,24 @@ class MockTest(SingleMagicFolderTestMixin, unittest.TestCase):
upload_dircap = n.get_uri()
readonly_dircap = n.get_readonly_uri()
self.shouldFail(AssertionError, 'nonexistent local.directory', 'there is no directory',
MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077)
self.shouldFail(AssertionError, 'non-directory local.directory', 'is not a directory',
MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077)
self.shouldFail(ValueError, 'does not exist', 'does not exist',
MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077, 'default')
self.shouldFail(ValueError, 'is not a directory', 'is not a directory',
MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077, 'default')
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',
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',
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',
MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077)
MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077, 'default')
def _not_implemented():
raise NotImplementedError("blah")
self.patch(magic_folder, 'get_inotify_module', _not_implemented)
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)
return d

View File

@ -109,6 +109,44 @@ class FakeUploader(service.Service):
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():
ds = DownloadStatus("storage_index", 1234)
now = time.time()
@ -243,7 +281,7 @@ class FakeClient(_Client):
# don't upcall to Client.__init__, since we only want to initialize a
# minimal subset
service.MultiService.__init__(self)
self._magic_folder = None
self._magic_folders = dict()
self.all_contents = {}
self.nodeid = "fake_nodeid"
self.nickname = u"fake_nickname \u263A"
@ -281,6 +319,9 @@ class FakeClient(_Client):
def get_long_tubid(self):
return "tubid"
def get_auth_token(self):
return 'a fake debug auth token'
def startService(self):
return service.MultiService.startService(self)
def stopService(self):
@ -936,6 +977,61 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_check)
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):
h = self.s.get_history()
dl_num = h.list_all_download_statuses()[0].get_counter()

View File

@ -488,9 +488,12 @@ class TokenOnlyWebApi(resource.Resource):
if t == u'json':
try:
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())
req.setResponseCode(code)
req.setResponseCode(500 if code is None else code)
return json.dumps({"error": message})
else:
raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST)

View File

@ -1,6 +1,6 @@
import json
from allmydata.web.common import TokenOnlyWebApi
from allmydata.web.common import TokenOnlyWebApi, get_arg, WebError
class MagicFolderWebApi(TokenOnlyWebApi):
@ -14,9 +14,18 @@ class MagicFolderWebApi(TokenOnlyWebApi):
def post_json(self, req):
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 = []
for item in self.client._magic_folder.uploader.get_status():
for item in magic_folder.uploader.get_status():
d = dict(
path=item.relpath_u,
status=item.status_history()[-1][0],
@ -27,7 +36,7 @@ class MagicFolderWebApi(TokenOnlyWebApi):
d['percent_done'] = item.progress.progress
data.append(d)
for item in self.client._magic_folder.downloader.get_status():
for item in magic_folder.downloader.get_status():
d = dict(
path=item.relpath_u,
status=item.status_history()[-1][0],

View File

@ -237,12 +237,13 @@ class Root(MultiFormatPage):
return description
def render_magic_folder(self, ctx, data):
if self.client._magic_folder is None:
return T.p()
(ok, messages) = self.client._magic_folder.get_public_status()
def data_magic_folders(self, ctx, data):
return self.client._magic_folders.keys()
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:
ctx.fillSlots("magic_folder_status", "yes")
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_alt", "not working")
status = T.ul()
status = T.ul(class_="magic-folder-status")
for msg in messages:
status[T.li[str(msg)]]
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):
ul = T.ul()
try:

View File

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

View File

@ -160,10 +160,10 @@
</div>
<div n:render="magic_folder" class="row-fluid">
<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>
Magic Folder
</h2>
<h2>Magic Folders</h2>
<div n:render="sequence" n:data="magic_folders">
<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>
</div>
</div><!--/row-->
<div class="row-fluid">