mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-02-18 17:00:24 +00:00
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:
parent
1b6f477549
commit
672475cb2b
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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],
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user