mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-02-20 17:52:50 +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
|
#@defer.inlineCallbacks
|
||||||
def create_client(basedir=u"."):
|
def create_client(basedir=u"."):
|
||||||
from allmydata.node import read_config
|
from allmydata.node import read_config
|
||||||
config = read_config(basedir, u"client.port")
|
config = read_config(basedir, u"client.port", _valid_config_sections=_valid_config_sections)
|
||||||
config.validate(_valid_config_sections())
|
|
||||||
#defer.returnValue(
|
#defer.returnValue(
|
||||||
return _Client(
|
return _Client(
|
||||||
config,
|
config,
|
||||||
@ -192,7 +191,7 @@ class _Client(node.Node, pollmixin.PollMixin):
|
|||||||
node.Node.__init__(self, config, basedir=basedir)
|
node.Node.__init__(self, config, basedir=basedir)
|
||||||
# All tub.registerReference must happen *after* we upcall, since
|
# All tub.registerReference must happen *after* we upcall, since
|
||||||
# that's what does tub.setLocation()
|
# that's what does tub.setLocation()
|
||||||
self._magic_folder = None
|
self._magic_folders = dict()
|
||||||
self.started_timestamp = time.time()
|
self.started_timestamp = time.time()
|
||||||
self.logSource="Client"
|
self.logSource="Client"
|
||||||
self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy()
|
self.encoding_params = self.DEFAULT_ENCODING_PARAMETERS.copy()
|
||||||
@ -576,35 +575,47 @@ class _Client(node.Node, pollmixin.PollMixin):
|
|||||||
"See docs/frontends/magic-folder.rst for more information.")
|
"See docs/frontends/magic-folder.rst for more information.")
|
||||||
|
|
||||||
if self.get_config("magic_folder", "enabled", False, boolean=True):
|
if self.get_config("magic_folder", "enabled", False, boolean=True):
|
||||||
#print "magic folder enabled"
|
|
||||||
from allmydata.frontends import magic_folder
|
from allmydata.frontends import magic_folder
|
||||||
|
|
||||||
db_filename = os.path.join(self.basedir, "private", "magicfolderdb.sqlite")
|
|
||||||
local_dir_config = self.get_config("magic_folder", "local.directory").decode("utf-8")
|
|
||||||
try:
|
try:
|
||||||
poll_interval = int(self.get_config("magic_folder", "poll_interval", 3))
|
magic_folders = magic_folder.load_magic_folders(self.basedir)
|
||||||
except ValueError:
|
except Exception as e:
|
||||||
raise ValueError("[magic_folder] poll_interval must be an int")
|
log.msg("Error loading magic-folder config: {}".format(e))
|
||||||
|
raise
|
||||||
s = magic_folder.MagicFolder(
|
|
||||||
client=self,
|
|
||||||
upload_dircap=self.get_private_config("magic_folder_dircap"),
|
|
||||||
collective_dircap=self.get_private_config("collective_dircap"),
|
|
||||||
local_path_u=abspath_expanduser_unicode(local_dir_config, base=self.basedir),
|
|
||||||
dbfile=abspath_expanduser_unicode(db_filename),
|
|
||||||
umask=self.get_config("magic_folder", "download.umask", 0077),
|
|
||||||
downloader_delay=poll_interval,
|
|
||||||
)
|
|
||||||
self._magic_folder = s
|
|
||||||
s.setServiceParent(self)
|
|
||||||
s.startService()
|
|
||||||
|
|
||||||
# start processing the upload queue when we've connected to
|
# start processing the upload queue when we've connected to
|
||||||
# enough servers
|
# enough servers
|
||||||
threshold = min(self.encoding_params["k"],
|
threshold = min(self.encoding_params["k"],
|
||||||
self.encoding_params["happy"] + 1)
|
self.encoding_params["happy"] + 1)
|
||||||
d = self.storage_broker.when_connected_enough(threshold)
|
|
||||||
d.addCallback(lambda ign: s.ready())
|
for (name, mf_config) in magic_folders.items():
|
||||||
|
self.log("Starting magic_folder '{}'".format(name))
|
||||||
|
db_filename = os.path.join(self.basedir, "private", "magicfolder_{}.sqlite".format(name))
|
||||||
|
local_dir_config = mf_config['directory']
|
||||||
|
try:
|
||||||
|
poll_interval = int(mf_config["poll_interval"])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("'poll_interval' option must be an int")
|
||||||
|
|
||||||
|
s = magic_folder.MagicFolder(
|
||||||
|
client=self,
|
||||||
|
upload_dircap=mf_config["upload_dircap"].encode('ascii'),
|
||||||
|
collective_dircap=mf_config["collective_dircap"].encode('ascii'),
|
||||||
|
local_path_u=abspath_expanduser_unicode(local_dir_config, base=self.basedir),
|
||||||
|
dbfile=abspath_expanduser_unicode(db_filename),
|
||||||
|
umask=self.get_config("magic_folder", "download.umask", 0077),
|
||||||
|
name=name,
|
||||||
|
downloader_delay=poll_interval,
|
||||||
|
)
|
||||||
|
self._magic_folders[name] = s
|
||||||
|
s.setServiceParent(self)
|
||||||
|
s.startService()
|
||||||
|
|
||||||
|
connected_d = self.storage_broker.when_connected_enough(threshold)
|
||||||
|
def connected_enough(ign, mf):
|
||||||
|
mf.ready() # returns a Deferred we ignore
|
||||||
|
return None
|
||||||
|
connected_d.addCallback(connected_enough, s)
|
||||||
|
|
||||||
def _check_exit_trigger(self, exit_trigger_file):
|
def _check_exit_trigger(self, exit_trigger_file):
|
||||||
if os.path.exists(exit_trigger_file):
|
if os.path.exists(exit_trigger_file):
|
||||||
|
@ -4,6 +4,7 @@ import os.path
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
|
import ConfigParser
|
||||||
|
|
||||||
from twisted.internet import defer, reactor, task
|
from twisted.internet import defer, reactor, task
|
||||||
from twisted.internet.error import AlreadyCancelled
|
from twisted.internet.error import AlreadyCancelled
|
||||||
@ -14,7 +15,7 @@ from twisted.application import service
|
|||||||
|
|
||||||
from zope.interface import Interface, Attribute, implementer
|
from zope.interface import Interface, Attribute, implementer
|
||||||
|
|
||||||
from allmydata.util import fileutil
|
from allmydata.util import fileutil, configutil, yamlutil
|
||||||
from allmydata.interfaces import IDirectoryNode
|
from allmydata.interfaces import IDirectoryNode
|
||||||
from allmydata.util import log
|
from allmydata.util import log
|
||||||
from allmydata.util.fileutil import precondition_abspath, get_pathinfo, ConflictError
|
from allmydata.util.fileutil import precondition_abspath, get_pathinfo, ConflictError
|
||||||
@ -59,12 +60,182 @@ def is_new_file(pathinfo, db_entry):
|
|||||||
(db_entry.size, db_entry.ctime_ns, db_entry.mtime_ns))
|
(db_entry.size, db_entry.ctime_ns, db_entry.mtime_ns))
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_magic_folder_config(basedir):
|
||||||
|
"""
|
||||||
|
Helper that upgrades from single-magic-folder-only configs to
|
||||||
|
multiple magic-folder configuration style (in YAML)
|
||||||
|
"""
|
||||||
|
config_fname = os.path.join(basedir, "tahoe.cfg")
|
||||||
|
config = configutil.get_config(config_fname)
|
||||||
|
|
||||||
|
collective_fname = os.path.join(basedir, "private", "collective_dircap")
|
||||||
|
upload_fname = os.path.join(basedir, "private", "magic_folder_dircap")
|
||||||
|
magic_folders = {
|
||||||
|
u"default": {
|
||||||
|
u"directory": config.get("magic_folder", "local.directory").decode("utf-8"),
|
||||||
|
u"collective_dircap": fileutil.read(collective_fname),
|
||||||
|
u"upload_dircap": fileutil.read(upload_fname),
|
||||||
|
u"poll_interval": int(config.get("magic_folder", "poll_interval")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fileutil.move_into_place(
|
||||||
|
source=os.path.join(basedir, "private", "magicfolderdb.sqlite"),
|
||||||
|
dest=os.path.join(basedir, "private", "magicfolder_default.sqlite"),
|
||||||
|
)
|
||||||
|
save_magic_folders(basedir, magic_folders)
|
||||||
|
config.remove_option("magic_folder", "local.directory")
|
||||||
|
config.remove_option("magic_folder", "poll_interval")
|
||||||
|
configutil.write_config(os.path.join(basedir, 'tahoe.cfg'), config)
|
||||||
|
fileutil.remove_if_possible(collective_fname)
|
||||||
|
fileutil.remove_if_possible(upload_fname)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_upgrade_magic_folders(node_directory):
|
||||||
|
"""
|
||||||
|
If the given node directory is not already using the new-style
|
||||||
|
magic-folder config it will be upgraded to do so. (This should
|
||||||
|
only be done if the user is running a command that needs to modify
|
||||||
|
the config)
|
||||||
|
"""
|
||||||
|
yaml_fname = os.path.join(node_directory, u"private", u"magic_folders.yaml")
|
||||||
|
if os.path.exists(yaml_fname):
|
||||||
|
# we already have new-style magic folders
|
||||||
|
return
|
||||||
|
|
||||||
|
config_fname = os.path.join(node_directory, "tahoe.cfg")
|
||||||
|
config = configutil.get_config(config_fname)
|
||||||
|
|
||||||
|
# we have no YAML config; if we have config in tahoe.cfg then we
|
||||||
|
# can upgrade it to the YAML-based configuration
|
||||||
|
if config.has_option("magic_folder", "local.directory"):
|
||||||
|
_upgrade_magic_folder_config(node_directory)
|
||||||
|
|
||||||
|
|
||||||
|
def load_magic_folders(node_directory):
|
||||||
|
"""
|
||||||
|
Loads existing magic-folder configuration and returns it as a dict
|
||||||
|
mapping name -> dict of config. This will NOT upgrade from
|
||||||
|
old-style to new-style config (but WILL read old-style config and
|
||||||
|
return in the same way as if it was new-style).
|
||||||
|
|
||||||
|
:returns: dict mapping magic-folder-name to its config (also a dict)
|
||||||
|
"""
|
||||||
|
yaml_fname = os.path.join(node_directory, u"private", u"magic_folders.yaml")
|
||||||
|
folders = dict()
|
||||||
|
|
||||||
|
config_fname = os.path.join(node_directory, "tahoe.cfg")
|
||||||
|
config = configutil.get_config(config_fname)
|
||||||
|
|
||||||
|
if not os.path.exists(yaml_fname):
|
||||||
|
# there will still be a magic_folder section in a "new"
|
||||||
|
# config, but it won't have local.directory nor poll_interval
|
||||||
|
# in it.
|
||||||
|
if config.has_option("magic_folder", "local.directory"):
|
||||||
|
up_fname = os.path.join(node_directory, "private", "magic_folder_dircap")
|
||||||
|
coll_fname = os.path.join(node_directory, "private", "collective_dircap")
|
||||||
|
directory = config.get("magic_folder", "local.directory").decode('utf8')
|
||||||
|
try:
|
||||||
|
interval = int(config.get("magic_folder", "poll_interval"))
|
||||||
|
except ConfigParser.NoOptionError:
|
||||||
|
interval = 60
|
||||||
|
dir_fp = to_filepath(directory)
|
||||||
|
|
||||||
|
if not dir_fp.exists():
|
||||||
|
raise Exception(
|
||||||
|
"The '[magic_folder] local.directory' parameter is {} "
|
||||||
|
"but there is no directory at that location.".format(
|
||||||
|
quote_local_unicode_path(directory),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not dir_fp.isdir():
|
||||||
|
raise Exception(
|
||||||
|
"The '[magic_folder] local.directory' parameter is {} "
|
||||||
|
"but the thing at that location is not a directory.".format(
|
||||||
|
quote_local_unicode_path(directory)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
folders[u"default"] = {
|
||||||
|
u"directory": directory,
|
||||||
|
u"upload_dircap": fileutil.read(up_fname),
|
||||||
|
u"collective_dircap": fileutil.read(coll_fname),
|
||||||
|
u"poll_interval": interval,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# without any YAML file AND no local.directory option it's
|
||||||
|
# an error if magic-folder is "enabled" because we don't
|
||||||
|
# actually have enough config for any magic-folders at all
|
||||||
|
if config.has_section("magic_folder") \
|
||||||
|
and config.getboolean("magic_folder", "enabled") \
|
||||||
|
and not folders:
|
||||||
|
raise Exception(
|
||||||
|
"[magic_folder] is enabled but has no YAML file and no "
|
||||||
|
"'local.directory' option."
|
||||||
|
)
|
||||||
|
|
||||||
|
elif os.path.exists(yaml_fname): # yaml config-file exists
|
||||||
|
if config.has_option("magic_folder", "local.directory"):
|
||||||
|
raise Exception(
|
||||||
|
"magic-folder config has both old-style configuration"
|
||||||
|
" and new-style configuration; please remove the "
|
||||||
|
"'local.directory' key from tahoe.cfg or remove "
|
||||||
|
"'magic_folders.yaml' from {}".format(node_directory)
|
||||||
|
)
|
||||||
|
with open(yaml_fname, "r") as f:
|
||||||
|
magic_folders = yamlutil.safe_load(f.read())
|
||||||
|
if not isinstance(magic_folders, dict):
|
||||||
|
raise Exception(
|
||||||
|
"'{}' should contain a dict".format(yaml_fname)
|
||||||
|
)
|
||||||
|
|
||||||
|
folders = magic_folders['magic-folders']
|
||||||
|
if not isinstance(folders, dict):
|
||||||
|
raise Exception(
|
||||||
|
"'magic-folders' in '{}' should be a dict".format(yaml_fname)
|
||||||
|
)
|
||||||
|
|
||||||
|
# check configuration
|
||||||
|
for (name, mf_config) in folders.items():
|
||||||
|
if not isinstance(mf_config, dict):
|
||||||
|
raise Exception(
|
||||||
|
"Each item in '{}' must itself be a dict".format(yaml_fname)
|
||||||
|
)
|
||||||
|
for k in ['collective_dircap', 'upload_dircap', 'directory', 'poll_interval']:
|
||||||
|
if k not in mf_config:
|
||||||
|
raise Exception(
|
||||||
|
"Config for magic folder '{}' is missing '{}'".format(
|
||||||
|
name, k
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for k in ['collective_dircap', 'upload_dircap']:
|
||||||
|
if isinstance(mf_config[k], unicode):
|
||||||
|
mf_config[k] = mf_config[k].encode('ascii')
|
||||||
|
|
||||||
|
return folders
|
||||||
|
|
||||||
|
|
||||||
|
def save_magic_folders(node_directory, folders):
|
||||||
|
fileutil.write_atomically(
|
||||||
|
os.path.join(node_directory, u"private", u"magic_folders.yaml"),
|
||||||
|
yamlutil.safe_dump({u"magic-folders": folders}),
|
||||||
|
)
|
||||||
|
|
||||||
|
config = configutil.get_config(os.path.join(node_directory, u"tahoe.cfg"))
|
||||||
|
configutil.set_config(config, "magic_folder", "enabled", "True")
|
||||||
|
configutil.write_config(os.path.join(node_directory, u"tahoe.cfg"), config)
|
||||||
|
|
||||||
|
|
||||||
class MagicFolder(service.MultiService):
|
class MagicFolder(service.MultiService):
|
||||||
name = 'magic-folder'
|
|
||||||
|
|
||||||
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask,
|
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask,
|
||||||
uploader_delay=1.0, clock=None, downloader_delay=60):
|
name, uploader_delay=1.0, clock=None, downloader_delay=60):
|
||||||
precondition_abspath(local_path_u)
|
precondition_abspath(local_path_u)
|
||||||
|
if not os.path.exists(local_path_u):
|
||||||
|
raise ValueError("'{}' does not exist".format(local_path_u))
|
||||||
|
if not os.path.isdir(local_path_u):
|
||||||
|
raise ValueError("'{}' is not a directory".format(local_path_u))
|
||||||
|
# this is used by 'service' things and must be unique in this Service hierarchy
|
||||||
|
self.name = 'magic-folder-{}'.format(name)
|
||||||
|
|
||||||
service.MultiService.__init__(self)
|
service.MultiService.__init__(self)
|
||||||
|
|
||||||
@ -149,14 +320,10 @@ class QueueMixin(HookMixin):
|
|||||||
}
|
}
|
||||||
self.started_d = self.set_hook('started')
|
self.started_d = self.set_hook('started')
|
||||||
|
|
||||||
if not self._local_filepath.exists():
|
# we should have gotten nice errors already while loading the
|
||||||
raise AssertionError("The '[magic_folder] local.directory' parameter was %s "
|
# config, but just to be safe:
|
||||||
"but there is no directory at that location."
|
assert self._local_filepath.exists()
|
||||||
% quote_local_unicode_path(self._local_path_u))
|
assert self._local_filepath.isdir()
|
||||||
if not self._local_filepath.isdir():
|
|
||||||
raise AssertionError("The '[magic_folder] local.directory' parameter was %s "
|
|
||||||
"but the thing at that location is not a directory."
|
|
||||||
% quote_local_unicode_path(self._local_path_u))
|
|
||||||
|
|
||||||
self._deque = deque()
|
self._deque = deque()
|
||||||
# do we also want to bound on "maximum age"?
|
# do we also want to bound on "maximum age"?
|
||||||
@ -343,11 +510,9 @@ class Uploader(QueueMixin):
|
|||||||
self.is_ready = False
|
self.is_ready = False
|
||||||
|
|
||||||
if not IDirectoryNode.providedBy(upload_dirnode):
|
if not IDirectoryNode.providedBy(upload_dirnode):
|
||||||
raise AssertionError("The URI in '%s' does not refer to a directory."
|
raise AssertionError("'upload_dircap' does not refer to a directory")
|
||||||
% os.path.join('private', 'magic_folder_dircap'))
|
|
||||||
if upload_dirnode.is_unknown() or upload_dirnode.is_readonly():
|
if upload_dirnode.is_unknown() or upload_dirnode.is_readonly():
|
||||||
raise AssertionError("The URI in '%s' is not a writecap to a directory."
|
raise AssertionError("'upload_dircap' is not a writecap to a directory")
|
||||||
% os.path.join('private', 'magic_folder_dircap'))
|
|
||||||
|
|
||||||
self._upload_dirnode = upload_dirnode
|
self._upload_dirnode = upload_dirnode
|
||||||
self._inotify = get_inotify_module()
|
self._inotify = get_inotify_module()
|
||||||
@ -756,11 +921,9 @@ class Downloader(QueueMixin, WriteFileMixin):
|
|||||||
QueueMixin.__init__(self, client, local_path_u, db, 'downloader', clock)
|
QueueMixin.__init__(self, client, local_path_u, db, 'downloader', clock)
|
||||||
|
|
||||||
if not IDirectoryNode.providedBy(collective_dirnode):
|
if not IDirectoryNode.providedBy(collective_dirnode):
|
||||||
raise AssertionError("The URI in '%s' does not refer to a directory."
|
raise AssertionError("'collective_dircap' does not refer to a directory")
|
||||||
% os.path.join('private', 'collective_dircap'))
|
|
||||||
if collective_dirnode.is_unknown() or not collective_dirnode.is_readonly():
|
if collective_dirnode.is_unknown() or not collective_dirnode.is_readonly():
|
||||||
raise AssertionError("The URI in '%s' is not a readonly cap to a directory."
|
raise AssertionError("'collective_dircap' is not a readonly cap to a directory")
|
||||||
% os.path.join('private', 'collective_dircap'))
|
|
||||||
|
|
||||||
self._collective_dirnode = collective_dirnode
|
self._collective_dirnode = collective_dirnode
|
||||||
self._upload_readonly_dircap = upload_readonly_dircap
|
self._upload_readonly_dircap = upload_readonly_dircap
|
||||||
@ -778,7 +941,7 @@ class Downloader(QueueMixin, WriteFileMixin):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = yield self._scan_remote_collective(scan_self=True)
|
data = yield self._scan_remote_collective(scan_self=True)
|
||||||
twlog.msg("Completed initial Magic Folder scan successfully")
|
twlog.msg("Completed initial Magic Folder scan successfully ({})".format(self))
|
||||||
x = yield self._begin_processing(data)
|
x = yield self._begin_processing(data)
|
||||||
defer.returnValue(x)
|
defer.returnValue(x)
|
||||||
break
|
break
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import urllib
|
import urllib
|
||||||
from sys import stderr
|
|
||||||
from types import NoneType
|
from types import NoneType
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -19,20 +18,29 @@ from allmydata.util.encodingutil import argv_to_abspath, argv_to_unicode, to_str
|
|||||||
quote_local_unicode_path
|
quote_local_unicode_path
|
||||||
from allmydata.scripts.common_http import do_http, BadResponse
|
from allmydata.scripts.common_http import do_http, BadResponse
|
||||||
from allmydata.util import fileutil
|
from allmydata.util import fileutil
|
||||||
from allmydata.util import configutil
|
|
||||||
from allmydata import uri
|
from allmydata import uri
|
||||||
from allmydata.util.abbreviate import abbreviate_space, abbreviate_time
|
from allmydata.util.abbreviate import abbreviate_space, abbreviate_time
|
||||||
|
from allmydata.frontends.magic_folder import load_magic_folders
|
||||||
|
from allmydata.frontends.magic_folder import save_magic_folders
|
||||||
|
from allmydata.frontends.magic_folder import maybe_upgrade_magic_folders
|
||||||
|
|
||||||
|
|
||||||
INVITE_SEPARATOR = "+"
|
INVITE_SEPARATOR = "+"
|
||||||
|
|
||||||
class CreateOptions(BasedirOptions):
|
class CreateOptions(BasedirOptions):
|
||||||
nickname = None
|
nickname = None # NOTE: *not* the "name of this magic-folder"
|
||||||
local_dir = None
|
local_dir = None
|
||||||
synopsis = "MAGIC_ALIAS: [NICKNAME LOCAL_DIR]"
|
synopsis = "MAGIC_ALIAS: [NICKNAME LOCAL_DIR]"
|
||||||
optParameters = [
|
optParameters = [
|
||||||
("poll-interval", "p", "60", "How often to ask for updates"),
|
("poll-interval", "p", "60", "How often to ask for updates"),
|
||||||
|
("name", "n", "default", "The name of this magic-folder"),
|
||||||
]
|
]
|
||||||
|
description = (
|
||||||
|
"Create a new magic-folder. If you specify NICKNAME and "
|
||||||
|
"LOCAL_DIR, this client will also be invited and join "
|
||||||
|
"using the given nickname. A new alias (see 'tahoe list-aliases') "
|
||||||
|
"will be added with the master folder's writecap."
|
||||||
|
)
|
||||||
|
|
||||||
def parseArgs(self, alias, nickname=None, local_dir=None):
|
def parseArgs(self, alias, nickname=None, local_dir=None):
|
||||||
BasedirOptions.parseArgs(self)
|
BasedirOptions.parseArgs(self)
|
||||||
@ -61,6 +69,7 @@ def _delegate_options(source_options, target_options):
|
|||||||
target_options.aliases = get_aliases(source_options['node-directory'])
|
target_options.aliases = get_aliases(source_options['node-directory'])
|
||||||
target_options["node-url"] = source_options["node-url"]
|
target_options["node-url"] = source_options["node-url"]
|
||||||
target_options["node-directory"] = source_options["node-directory"]
|
target_options["node-directory"] = source_options["node-directory"]
|
||||||
|
target_options["name"] = source_options["name"]
|
||||||
target_options.stdin = StringIO("")
|
target_options.stdin = StringIO("")
|
||||||
target_options.stdout = StringIO()
|
target_options.stdout = StringIO()
|
||||||
target_options.stderr = StringIO()
|
target_options.stderr = StringIO()
|
||||||
@ -71,6 +80,15 @@ def create(options):
|
|||||||
precondition(isinstance(options.nickname, (unicode, NoneType)), nickname=options.nickname)
|
precondition(isinstance(options.nickname, (unicode, NoneType)), nickname=options.nickname)
|
||||||
precondition(isinstance(options.local_dir, (unicode, NoneType)), local_dir=options.local_dir)
|
precondition(isinstance(options.local_dir, (unicode, NoneType)), local_dir=options.local_dir)
|
||||||
|
|
||||||
|
# make sure we don't already have a magic-folder with this name before we create the alias
|
||||||
|
maybe_upgrade_magic_folders(options["node-directory"])
|
||||||
|
folders = load_magic_folders(options["node-directory"])
|
||||||
|
if options['name'] in folders:
|
||||||
|
print >>options.stderr, "Already have a magic-folder named '{}'".format(options['name'])
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# create an alias; this basically just remembers the cap for the
|
||||||
|
# master directory
|
||||||
from allmydata.scripts import tahoe_add_alias
|
from allmydata.scripts import tahoe_add_alias
|
||||||
create_alias_options = _delegate_options(options, CreateAliasOptions())
|
create_alias_options = _delegate_options(options, CreateAliasOptions())
|
||||||
create_alias_options.alias = options.alias
|
create_alias_options.alias = options.alias
|
||||||
@ -82,30 +100,95 @@ def create(options):
|
|||||||
print >>options.stdout, create_alias_options.stdout.getvalue()
|
print >>options.stdout, create_alias_options.stdout.getvalue()
|
||||||
|
|
||||||
if options.nickname is not None:
|
if options.nickname is not None:
|
||||||
|
print >>options.stdout, u"Inviting myself as client '{}':".format(options.nickname)
|
||||||
invite_options = _delegate_options(options, InviteOptions())
|
invite_options = _delegate_options(options, InviteOptions())
|
||||||
invite_options.alias = options.alias
|
invite_options.alias = options.alias
|
||||||
invite_options.nickname = options.nickname
|
invite_options.nickname = options.nickname
|
||||||
|
invite_options['name'] = options['name']
|
||||||
rc = invite(invite_options)
|
rc = invite(invite_options)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
print >>options.stderr, "magic-folder: failed to invite after create\n"
|
print >>options.stderr, u"magic-folder: failed to invite after create\n"
|
||||||
print >>options.stderr, invite_options.stderr.getvalue()
|
print >>options.stderr, invite_options.stderr.getvalue()
|
||||||
return rc
|
return rc
|
||||||
invite_code = invite_options.stdout.getvalue().strip()
|
invite_code = invite_options.stdout.getvalue().strip()
|
||||||
|
print >>options.stdout, u" created invite code"
|
||||||
join_options = _delegate_options(options, JoinOptions())
|
join_options = _delegate_options(options, JoinOptions())
|
||||||
join_options['poll-interval'] = options['poll-interval']
|
join_options['poll-interval'] = options['poll-interval']
|
||||||
|
join_options.nickname = options.nickname
|
||||||
join_options.local_dir = options.local_dir
|
join_options.local_dir = options.local_dir
|
||||||
join_options.invite_code = invite_code
|
join_options.invite_code = invite_code
|
||||||
rc = join(join_options)
|
rc = join(join_options)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
print >>options.stderr, "magic-folder: failed to join after create\n"
|
print >>options.stderr, u"magic-folder: failed to join after create\n"
|
||||||
print >>options.stderr, join_options.stderr.getvalue()
|
print >>options.stderr, join_options.stderr.getvalue()
|
||||||
return rc
|
return rc
|
||||||
|
print >>options.stdout, u" joined new magic-folder"
|
||||||
|
print >>options.stdout, (
|
||||||
|
u"Successfully created magic-folder '{}' with alias '{}:' "
|
||||||
|
u"and client '{}'\nYou must re-start your node before the "
|
||||||
|
u"magic-folder will be active."
|
||||||
|
).format(options['name'], options.alias, options.nickname)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class ListOptions(BasedirOptions):
|
||||||
|
description = (
|
||||||
|
"List all magic-folders this client has joined"
|
||||||
|
)
|
||||||
|
optFlags = [
|
||||||
|
("json", "", "Produce JSON output")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_(options):
|
||||||
|
folders = load_magic_folders(options["node-directory"])
|
||||||
|
if options["json"]:
|
||||||
|
_list_json(options, folders)
|
||||||
|
return 0
|
||||||
|
_list_human(options, folders)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _list_json(options, folders):
|
||||||
|
"""
|
||||||
|
List our magic-folders using JSON
|
||||||
|
"""
|
||||||
|
info = dict()
|
||||||
|
for name, details in folders.items():
|
||||||
|
info[name] = {
|
||||||
|
u"directory": details["directory"],
|
||||||
|
}
|
||||||
|
print >>options.stdout, json.dumps(info)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _list_human(options, folders):
|
||||||
|
"""
|
||||||
|
List our magic-folders for a human user
|
||||||
|
"""
|
||||||
|
if folders:
|
||||||
|
print >>options.stdout, "This client has the following magic-folders:"
|
||||||
|
biggest = max([len(nm) for nm in folders.keys()])
|
||||||
|
fmt = " {:>%d}: {}" % (biggest, )
|
||||||
|
for name, details in folders.items():
|
||||||
|
print >>options.stdout, fmt.format(name, details["directory"])
|
||||||
|
else:
|
||||||
|
print >>options.stdout, "No magic-folders"
|
||||||
|
|
||||||
|
|
||||||
class InviteOptions(BasedirOptions):
|
class InviteOptions(BasedirOptions):
|
||||||
nickname = None
|
nickname = None
|
||||||
synopsis = "MAGIC_ALIAS: NICKNAME"
|
synopsis = "MAGIC_ALIAS: NICKNAME"
|
||||||
stdin = StringIO("")
|
stdin = StringIO("")
|
||||||
|
optParameters = [
|
||||||
|
("name", "n", "default", "The name of this magic-folder"),
|
||||||
|
]
|
||||||
|
description = (
|
||||||
|
"Invite a new participant to a given magic-folder. The resulting "
|
||||||
|
"invite-code that is printed is secret information and MUST be "
|
||||||
|
"transmitted securely to the invitee."
|
||||||
|
)
|
||||||
|
|
||||||
def parseArgs(self, alias, nickname=None):
|
def parseArgs(self, alias, nickname=None):
|
||||||
BasedirOptions.parseArgs(self)
|
BasedirOptions.parseArgs(self)
|
||||||
alias = argv_to_unicode(alias)
|
alias = argv_to_unicode(alias)
|
||||||
@ -118,6 +201,7 @@ class InviteOptions(BasedirOptions):
|
|||||||
aliases = get_aliases(self['node-directory'])
|
aliases = get_aliases(self['node-directory'])
|
||||||
self.aliases = aliases
|
self.aliases = aliases
|
||||||
|
|
||||||
|
|
||||||
def invite(options):
|
def invite(options):
|
||||||
precondition(isinstance(options.alias, unicode), alias=options.alias)
|
precondition(isinstance(options.alias, unicode), alias=options.alias)
|
||||||
precondition(isinstance(options.nickname, unicode), nickname=options.nickname)
|
precondition(isinstance(options.nickname, unicode), nickname=options.nickname)
|
||||||
@ -161,6 +245,7 @@ class JoinOptions(BasedirOptions):
|
|||||||
magic_readonly_cap = ""
|
magic_readonly_cap = ""
|
||||||
optParameters = [
|
optParameters = [
|
||||||
("poll-interval", "p", "60", "How often to ask for updates"),
|
("poll-interval", "p", "60", "How often to ask for updates"),
|
||||||
|
("name", "n", "default", "Name of the magic-folder"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def parseArgs(self, invite_code, local_dir):
|
def parseArgs(self, invite_code, local_dir):
|
||||||
@ -183,57 +268,85 @@ def join(options):
|
|||||||
raise usage.UsageError("Invalid invite code.")
|
raise usage.UsageError("Invalid invite code.")
|
||||||
magic_readonly_cap, dmd_write_cap = fields
|
magic_readonly_cap, dmd_write_cap = fields
|
||||||
|
|
||||||
dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap")
|
maybe_upgrade_magic_folders(options["node-directory"])
|
||||||
collective_readcap_file = os.path.join(options["node-directory"], u"private", u"collective_dircap")
|
existing_folders = load_magic_folders(options["node-directory"])
|
||||||
magic_folder_db_file = os.path.join(options["node-directory"], u"private", u"magicfolderdb.sqlite")
|
|
||||||
|
|
||||||
if os.path.exists(dmd_cap_file) or os.path.exists(collective_readcap_file) or os.path.exists(magic_folder_db_file):
|
if options['name'] in existing_folders:
|
||||||
print >>options.stderr, ("\nThis client has already joined a magic folder."
|
print >>options.stderr, "This client already has a magic-folder named '{}'".format(options['name'])
|
||||||
"\nUse the 'tahoe magic-folder leave' command first.\n")
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
fileutil.write(dmd_cap_file, dmd_write_cap)
|
db_fname = os.path.join(
|
||||||
fileutil.write(collective_readcap_file, magic_readonly_cap)
|
options["node-directory"],
|
||||||
|
u"private",
|
||||||
|
u"magicfolder_{}.sqlite".format(options['name']),
|
||||||
|
)
|
||||||
|
if os.path.exists(db_fname):
|
||||||
|
print >>options.stderr, "Database '{}' already exists; not overwriting".format(db_fname)
|
||||||
|
return 1
|
||||||
|
|
||||||
config = configutil.get_config(os.path.join(options["node-directory"], u"tahoe.cfg"))
|
folder = {
|
||||||
configutil.set_config(config, "magic_folder", "enabled", "True")
|
u"directory": options.local_dir.encode('utf-8'),
|
||||||
configutil.set_config(config, "magic_folder", "local.directory", options.local_dir.encode('utf-8'))
|
u"collective_dircap": magic_readonly_cap,
|
||||||
configutil.set_config(config, "magic_folder", "poll_interval", options.get("poll-interval", "60"))
|
u"upload_dircap": dmd_write_cap,
|
||||||
configutil.write_config(os.path.join(options["node-directory"], u"tahoe.cfg"), config)
|
u"poll_interval": options["poll-interval"],
|
||||||
|
}
|
||||||
|
existing_folders[options["name"]] = folder
|
||||||
|
|
||||||
|
save_magic_folders(options["node-directory"], existing_folders)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class LeaveOptions(BasedirOptions):
|
class LeaveOptions(BasedirOptions):
|
||||||
synopsis = ""
|
synopsis = "Remove a magic-folder and forget all state"
|
||||||
def parseArgs(self):
|
optParameters = [
|
||||||
BasedirOptions.parseArgs(self)
|
("name", "n", "default", "Name of magic-folder to leave"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def leave(options):
|
def leave(options):
|
||||||
from ConfigParser import SafeConfigParser
|
from ConfigParser import SafeConfigParser
|
||||||
|
|
||||||
dmd_cap_file = os.path.join(options["node-directory"], u"private", u"magic_folder_dircap")
|
existing_folders = load_magic_folders(options["node-directory"])
|
||||||
collective_readcap_file = os.path.join(options["node-directory"], u"private", u"collective_dircap")
|
|
||||||
magic_folder_db_file = os.path.join(options["node-directory"], u"private", u"magicfolderdb.sqlite")
|
|
||||||
|
|
||||||
parser = SafeConfigParser()
|
if not existing_folders:
|
||||||
parser.read(os.path.join(options["node-directory"], u"tahoe.cfg"))
|
print >>options.stderr, "No magic-folders at all"
|
||||||
parser.remove_section("magic_folder")
|
return 1
|
||||||
f = open(os.path.join(options["node-directory"], u"tahoe.cfg"), "w")
|
|
||||||
parser.write(f)
|
if options["name"] not in existing_folders:
|
||||||
f.close()
|
print >>options.stderr, "No such magic-folder '{}'".format(options["name"])
|
||||||
|
return 1
|
||||||
|
|
||||||
|
privdir = os.path.join(options["node-directory"], u"private")
|
||||||
|
db_fname = os.path.join(privdir, u"magicfolder_{}.sqlite".format(options["name"]))
|
||||||
|
|
||||||
|
# delete from YAML file and re-write it
|
||||||
|
del existing_folders[options["name"]]
|
||||||
|
save_magic_folders(options["node-directory"], existing_folders)
|
||||||
|
|
||||||
|
# delete the database file
|
||||||
|
try:
|
||||||
|
fileutil.remove(db_fname)
|
||||||
|
except Exception as e:
|
||||||
|
print >>options.stderr, ("Warning: unable to remove %s due to %s: %s"
|
||||||
|
% (quote_local_unicode_path(db_fname), e.__class__.__name__, str(e)))
|
||||||
|
|
||||||
|
# if this was the last magic-folder, disable them entirely
|
||||||
|
if not existing_folders:
|
||||||
|
parser = SafeConfigParser()
|
||||||
|
parser.read(os.path.join(options["node-directory"], u"tahoe.cfg"))
|
||||||
|
parser.remove_section("magic_folder")
|
||||||
|
with open(os.path.join(options["node-directory"], u"tahoe.cfg"), "w") as f:
|
||||||
|
parser.write(f)
|
||||||
|
|
||||||
for f in [dmd_cap_file, collective_readcap_file, magic_folder_db_file]:
|
|
||||||
try:
|
|
||||||
fileutil.remove(f)
|
|
||||||
except Exception as e:
|
|
||||||
print >>options.stderr, ("Warning: unable to remove %s due to %s: %s"
|
|
||||||
% (quote_local_unicode_path(f), e.__class__.__name__, str(e)))
|
|
||||||
# if this doesn't return 0, then the CLI stuff fails
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class StatusOptions(BasedirOptions):
|
class StatusOptions(BasedirOptions):
|
||||||
nickname = None
|
|
||||||
synopsis = ""
|
synopsis = ""
|
||||||
stdin = StringIO("")
|
stdin = StringIO("")
|
||||||
|
optParameters = [
|
||||||
|
("name", "n", "default", "Name for the magic-folder to show status"),
|
||||||
|
]
|
||||||
|
|
||||||
def parseArgs(self):
|
def parseArgs(self):
|
||||||
BasedirOptions.parseArgs(self)
|
BasedirOptions.parseArgs(self)
|
||||||
@ -311,15 +424,25 @@ def _print_item_status(item, now, longest):
|
|||||||
|
|
||||||
print " %s: %s" % (paddedname, prog)
|
print " %s: %s" % (paddedname, prog)
|
||||||
|
|
||||||
|
|
||||||
def status(options):
|
def status(options):
|
||||||
nodedir = options["node-directory"]
|
nodedir = options["node-directory"]
|
||||||
with open(os.path.join(nodedir, u"private", u"magic_folder_dircap")) as f:
|
stdout, stderr = options.stdout, options.stderr
|
||||||
dmd_cap = f.read().strip()
|
magic_folders = load_magic_folders(os.path.join(options["node-directory"]))
|
||||||
with open(os.path.join(nodedir, u"private", u"collective_dircap")) as f:
|
|
||||||
collective_readcap = f.read().strip()
|
|
||||||
with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'rb') as f:
|
with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'rb') as f:
|
||||||
token = f.read()
|
token = f.read()
|
||||||
|
|
||||||
|
print >>stdout, "Magic-folder status for '{}':".format(options["name"])
|
||||||
|
|
||||||
|
if options["name"] not in magic_folders:
|
||||||
|
raise Exception(
|
||||||
|
"No such magic-folder '{}'".format(options["name"])
|
||||||
|
)
|
||||||
|
|
||||||
|
dmd_cap = magic_folders[options["name"]]["upload_dircap"]
|
||||||
|
collective_readcap = magic_folders[options["name"]]["collective_dircap"]
|
||||||
|
|
||||||
# do *all* our data-retrievals first in case there's an error
|
# do *all* our data-retrievals first in case there's an error
|
||||||
try:
|
try:
|
||||||
dmd_data = _get_json_for_cap(options, dmd_cap)
|
dmd_data = _get_json_for_cap(options, dmd_cap)
|
||||||
@ -330,6 +453,7 @@ def status(options):
|
|||||||
method='POST',
|
method='POST',
|
||||||
post_args=dict(
|
post_args=dict(
|
||||||
t='json',
|
t='json',
|
||||||
|
name=options["name"],
|
||||||
token=token,
|
token=token,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -350,7 +474,7 @@ def status(options):
|
|||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
print "Local files:"
|
print >>stdout, "Local files:"
|
||||||
for (name, child) in dmd['children'].items():
|
for (name, child) in dmd['children'].items():
|
||||||
captype, meta = child
|
captype, meta = child
|
||||||
status = 'good'
|
status = 'good'
|
||||||
@ -360,28 +484,28 @@ def status(options):
|
|||||||
nice_size = abbreviate_space(size)
|
nice_size = abbreviate_space(size)
|
||||||
nice_created = abbreviate_time(now - created)
|
nice_created = abbreviate_time(now - created)
|
||||||
if captype != 'filenode':
|
if captype != 'filenode':
|
||||||
print "%20s: error, should be a filecap" % name
|
print >>stdout, "%20s: error, should be a filecap" % name
|
||||||
continue
|
continue
|
||||||
print " %s (%s): %s, version=%s, created %s" % (name, nice_size, status, version, nice_created)
|
print >>stdout, " %s (%s): %s, version=%s, created %s" % (name, nice_size, status, version, nice_created)
|
||||||
|
|
||||||
print
|
print >>stdout
|
||||||
print "Remote files:"
|
print >>stdout, "Remote files:"
|
||||||
|
|
||||||
captype, collective = remote_data
|
captype, collective = remote_data
|
||||||
for (name, data) in collective['children'].items():
|
for (name, data) in collective['children'].items():
|
||||||
if data[0] != 'dirnode':
|
if data[0] != 'dirnode':
|
||||||
print "Error: '%s': expected a dirnode, not '%s'" % (name, data[0])
|
print >>stdout, "Error: '%s': expected a dirnode, not '%s'" % (name, data[0])
|
||||||
print " %s's remote:" % name
|
print >>stdout, " %s's remote:" % name
|
||||||
dmd = _get_json_for_cap(options, data[1]['ro_uri'])
|
dmd = _get_json_for_cap(options, data[1]['ro_uri'])
|
||||||
if isinstance(dmd, dict) and 'error' in dmd:
|
if isinstance(dmd, dict) and 'error' in dmd:
|
||||||
print(" Error: could not retrieve directory")
|
print >>stdout, " Error: could not retrieve directory"
|
||||||
continue
|
continue
|
||||||
if dmd[0] != 'dirnode':
|
if dmd[0] != 'dirnode':
|
||||||
print "Error: should be a dirnode"
|
print >>stdout, "Error: should be a dirnode"
|
||||||
continue
|
continue
|
||||||
for (n, d) in dmd[1]['children'].items():
|
for (n, d) in dmd[1]['children'].items():
|
||||||
if d[0] != 'filenode':
|
if d[0] != 'filenode':
|
||||||
print "Error: expected '%s' to be a filenode." % (n,)
|
print >>stdout, "Error: expected '%s' to be a filenode." % (n,)
|
||||||
|
|
||||||
meta = d[1]
|
meta = d[1]
|
||||||
status = 'good'
|
status = 'good'
|
||||||
@ -390,7 +514,7 @@ def status(options):
|
|||||||
version = meta['metadata']['version']
|
version = meta['metadata']['version']
|
||||||
nice_size = abbreviate_space(size)
|
nice_size = abbreviate_space(size)
|
||||||
nice_created = abbreviate_time(now - created)
|
nice_created = abbreviate_time(now - created)
|
||||||
print " %s (%s): %s, version=%s, created %s" % (n, nice_size, status, version, nice_created)
|
print >>stdout, " %s (%s): %s, version=%s, created %s" % (n, nice_size, status, version, nice_created)
|
||||||
|
|
||||||
if len(magic_data):
|
if len(magic_data):
|
||||||
uploads = [item for item in magic_data if item['kind'] == 'upload']
|
uploads = [item for item in magic_data if item['kind'] == 'upload']
|
||||||
@ -403,19 +527,19 @@ def status(options):
|
|||||||
|
|
||||||
if len(uploads):
|
if len(uploads):
|
||||||
print
|
print
|
||||||
print "Uploads:"
|
print >>stdout, "Uploads:"
|
||||||
for item in uploads:
|
for item in uploads:
|
||||||
_print_item_status(item, now, longest)
|
_print_item_status(item, now, longest)
|
||||||
|
|
||||||
if len(downloads):
|
if len(downloads):
|
||||||
print
|
print
|
||||||
print "Downloads:"
|
print >>stdout, "Downloads:"
|
||||||
for item in downloads:
|
for item in downloads:
|
||||||
_print_item_status(item, now, longest)
|
_print_item_status(item, now, longest)
|
||||||
|
|
||||||
for item in magic_data:
|
for item in magic_data:
|
||||||
if item['status'] == 'failure':
|
if item['status'] == 'failure':
|
||||||
print "Failed:", item
|
print >>stdout, "Failed:", item
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@ -427,21 +551,31 @@ class MagicFolderCommand(BaseOptions):
|
|||||||
["join", None, JoinOptions, "Join a Magic Folder."],
|
["join", None, JoinOptions, "Join a Magic Folder."],
|
||||||
["leave", None, LeaveOptions, "Leave a Magic Folder."],
|
["leave", None, LeaveOptions, "Leave a Magic Folder."],
|
||||||
["status", None, StatusOptions, "Display status of uploads/downloads."],
|
["status", None, StatusOptions, "Display status of uploads/downloads."],
|
||||||
|
["list", None, ListOptions, "List Magic Folders configured in this client."],
|
||||||
]
|
]
|
||||||
optFlags = [
|
optFlags = [
|
||||||
["debug", "d", "Print full stack-traces"],
|
["debug", "d", "Print full stack-traces"],
|
||||||
]
|
]
|
||||||
|
description = (
|
||||||
|
"A magic-folder has an owner who controls the writecap "
|
||||||
|
"containing a list of nicknames and readcaps. The owner can invite "
|
||||||
|
"new participants. Every participant has the writecap for their "
|
||||||
|
"own folder (the corresponding readcap is in the master folder). "
|
||||||
|
"All clients download files from all other participants using the "
|
||||||
|
"readcaps contained in the master magic-folder directory."
|
||||||
|
)
|
||||||
|
|
||||||
def postOptions(self):
|
def postOptions(self):
|
||||||
if not hasattr(self, 'subOptions'):
|
if not hasattr(self, 'subOptions'):
|
||||||
raise usage.UsageError("must specify a subcommand")
|
raise usage.UsageError("must specify a subcommand")
|
||||||
def getSynopsis(self):
|
def getSynopsis(self):
|
||||||
return "Usage: tahoe [global-options] magic SUBCOMMAND"
|
return "Usage: tahoe [global-options] magic-folder"
|
||||||
def getUsage(self, width=None):
|
def getUsage(self, width=None):
|
||||||
t = BaseOptions.getUsage(self, width)
|
t = BaseOptions.getUsage(self, width)
|
||||||
t += """\
|
t += (
|
||||||
Please run e.g. 'tahoe magic-folder create --help' for more details on each
|
"Please run e.g. 'tahoe magic-folder create --help' for more "
|
||||||
subcommand.
|
"details on each subcommand.\n"
|
||||||
"""
|
)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
subDispatch = {
|
subDispatch = {
|
||||||
@ -450,6 +584,7 @@ subDispatch = {
|
|||||||
"join": join,
|
"join": join,
|
||||||
"leave": leave,
|
"leave": leave,
|
||||||
"status": status,
|
"status": status,
|
||||||
|
"list": list_,
|
||||||
}
|
}
|
||||||
|
|
||||||
def do_magic_folder(options):
|
def do_magic_folder(options):
|
||||||
@ -460,7 +595,7 @@ def do_magic_folder(options):
|
|||||||
try:
|
try:
|
||||||
return f(so)
|
return f(so)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error: %s" % (e,))
|
print >>options.stderr, "Error: %s" % (e,)
|
||||||
if options['debug']:
|
if options['debug']:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import json
|
||||||
|
import shutil
|
||||||
import os.path
|
import os.path
|
||||||
|
import mock
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
@ -27,10 +30,12 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
self.bob_nickname = self.unicode_or_fallback(u"Bob\u00F8", u"Bob", io_as_well=True)
|
self.bob_nickname = self.unicode_or_fallback(u"Bob\u00F8", u"Bob", io_as_well=True)
|
||||||
|
|
||||||
def do_create_magic_folder(self, client_num):
|
def do_create_magic_folder(self, client_num):
|
||||||
d = self.do_cli("magic-folder", "create", "magic:", client_num=client_num)
|
d = self.do_cli("magic-folder", "--debug", "create", "magic:", client_num=client_num)
|
||||||
def _done((rc,stdout,stderr)):
|
def _done((rc,stdout,stderr)):
|
||||||
self.failUnlessEqual(rc, 0, stdout + stderr)
|
self.failUnlessEqual(rc, 0, stdout + stderr)
|
||||||
self.failUnlessIn("Alias 'magic' created", stdout)
|
self.failUnlessIn("Alias 'magic' created", stdout)
|
||||||
|
# self.failUnlessIn("joined new magic-folder", stdout)
|
||||||
|
# self.failUnlessIn("Successfully created magic-folder", stdout)
|
||||||
self.failUnlessEqual(stderr, "")
|
self.failUnlessEqual(stderr, "")
|
||||||
aliases = get_aliases(self.get_clientdir(i=client_num))
|
aliases = get_aliases(self.get_clientdir(i=client_num))
|
||||||
self.failUnlessIn("magic", aliases)
|
self.failUnlessIn("magic", aliases)
|
||||||
@ -47,6 +52,26 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def do_list(self, client_num, json=False):
|
||||||
|
args = ("magic-folder", "list",)
|
||||||
|
if json:
|
||||||
|
args = args + ("--json",)
|
||||||
|
d = self.do_cli(*args, client_num=client_num)
|
||||||
|
def _done((rc, stdout, stderr)):
|
||||||
|
return (rc, stdout, stderr)
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def do_status(self, client_num, name=None):
|
||||||
|
args = ("magic-folder", "status",)
|
||||||
|
if name is not None:
|
||||||
|
args = args + ("--name", name)
|
||||||
|
d = self.do_cli(*args, client_num=client_num)
|
||||||
|
def _done((rc, stdout, stderr)):
|
||||||
|
return (rc, stdout, stderr)
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
def do_join(self, client_num, local_dir, invite_code):
|
def do_join(self, client_num, local_dir, invite_code):
|
||||||
precondition(isinstance(local_dir, unicode), local_dir=local_dir)
|
precondition(isinstance(local_dir, unicode), local_dir=local_dir)
|
||||||
precondition(isinstance(invite_code, str), invite_code=invite_code)
|
precondition(isinstance(invite_code, str), invite_code=invite_code)
|
||||||
@ -73,8 +98,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
"""Tests that our collective directory has the readonly cap of
|
"""Tests that our collective directory has the readonly cap of
|
||||||
our upload directory.
|
our upload directory.
|
||||||
"""
|
"""
|
||||||
collective_readonly_cap = fileutil.read(os.path.join(self.get_clientdir(i=client_num),
|
collective_readonly_cap = self.get_caps_from_files(client_num)[0]
|
||||||
u"private", u"collective_dircap"))
|
|
||||||
d = self.do_cli("ls", "--json", collective_readonly_cap, client_num=client_num)
|
d = self.do_cli("ls", "--json", collective_readonly_cap, client_num=client_num)
|
||||||
def _done((rc, stdout, stderr)):
|
def _done((rc, stdout, stderr)):
|
||||||
self.failUnlessEqual(rc, 0)
|
self.failUnlessEqual(rc, 0)
|
||||||
@ -89,23 +113,24 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def get_caps_from_files(self, client_num):
|
def get_caps_from_files(self, client_num):
|
||||||
collective_dircap = fileutil.read(os.path.join(self.get_clientdir(i=client_num),
|
from allmydata.frontends.magic_folder import load_magic_folders
|
||||||
u"private", u"collective_dircap"))
|
folders = load_magic_folders(self.get_clientdir(i=client_num))
|
||||||
upload_dircap = fileutil.read(os.path.join(self.get_clientdir(i=client_num),
|
mf = folders["default"]
|
||||||
u"private", u"magic_folder_dircap"))
|
return mf['collective_dircap'], mf['upload_dircap']
|
||||||
self.failIf(collective_dircap is None or upload_dircap is None)
|
|
||||||
return collective_dircap, upload_dircap
|
|
||||||
|
|
||||||
def check_config(self, client_num, local_dir):
|
def check_config(self, client_num, local_dir):
|
||||||
client_config = fileutil.read(os.path.join(self.get_clientdir(i=client_num), "tahoe.cfg"))
|
client_config = fileutil.read(os.path.join(self.get_clientdir(i=client_num), "tahoe.cfg"))
|
||||||
|
mf_yaml = fileutil.read(os.path.join(self.get_clientdir(i=client_num), "private", "magic_folders.yaml"))
|
||||||
local_dir_utf8 = local_dir.encode('utf-8')
|
local_dir_utf8 = local_dir.encode('utf-8')
|
||||||
magic_folder_config = "[magic_folder]\nenabled = True\nlocal.directory = %s" % (local_dir_utf8,)
|
magic_folder_config = "[magic_folder]\nenabled = True"
|
||||||
self.failUnlessIn(magic_folder_config, client_config)
|
self.failUnlessIn(magic_folder_config, client_config)
|
||||||
|
self.failUnlessIn(local_dir_utf8, mf_yaml)
|
||||||
|
|
||||||
def create_invite_join_magic_folder(self, nickname, local_dir):
|
def create_invite_join_magic_folder(self, nickname, local_dir):
|
||||||
nickname_arg = unicode_to_argv(nickname)
|
nickname_arg = unicode_to_argv(nickname)
|
||||||
local_dir_arg = unicode_to_argv(local_dir)
|
local_dir_arg = unicode_to_argv(local_dir)
|
||||||
d = self.do_cli("magic-folder", "create", "magic:", nickname_arg, local_dir_arg)
|
# the --debug means we get real exceptions on failures
|
||||||
|
d = self.do_cli("magic-folder", "--debug", "create", "magic:", nickname_arg, local_dir_arg)
|
||||||
def _done((rc, stdout, stderr)):
|
def _done((rc, stdout, stderr)):
|
||||||
self.failUnlessEqual(rc, 0, stdout + stderr)
|
self.failUnlessEqual(rc, 0, stdout + stderr)
|
||||||
|
|
||||||
@ -132,7 +157,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def init_magicfolder(self, client_num, upload_dircap, collective_dircap, local_magic_dir, clock):
|
def init_magicfolder(self, client_num, upload_dircap, collective_dircap, local_magic_dir, clock):
|
||||||
dbfile = abspath_expanduser_unicode(u"magicfolderdb.sqlite", base=self.get_clientdir(i=client_num))
|
dbfile = abspath_expanduser_unicode(u"magicfolder_default.sqlite", base=self.get_clientdir(i=client_num))
|
||||||
magicfolder = MagicFolder(
|
magicfolder = MagicFolder(
|
||||||
client=self.get_client(client_num),
|
client=self.get_client(client_num),
|
||||||
upload_dircap=upload_dircap,
|
upload_dircap=upload_dircap,
|
||||||
@ -140,6 +165,7 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
local_path_u=local_magic_dir,
|
local_path_u=local_magic_dir,
|
||||||
dbfile=dbfile,
|
dbfile=dbfile,
|
||||||
umask=0o077,
|
umask=0o077,
|
||||||
|
name='default',
|
||||||
clock=clock,
|
clock=clock,
|
||||||
uploader_delay=0.2,
|
uploader_delay=0.2,
|
||||||
downloader_delay=0,
|
downloader_delay=0,
|
||||||
@ -199,11 +225,207 @@ class MagicFolderCLITestMixin(CLITestMixin, GridTestMixin, NonASCIIPathMixin):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class ListMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def setUp(self):
|
||||||
|
yield super(ListMagicFolder, self).setUp()
|
||||||
|
self.basedir="mf_list"
|
||||||
|
self.set_up_grid(oneshare=True)
|
||||||
|
self.local_dir = os.path.join(self.basedir, "magic")
|
||||||
|
os.mkdir(self.local_dir)
|
||||||
|
self.abs_local_dir_u = abspath_expanduser_unicode(unicode(self.local_dir), long_path=False)
|
||||||
|
|
||||||
|
yield self.do_create_magic_folder(0)
|
||||||
|
(rc, stdout, stderr) = yield self.do_invite(0, self.alice_nickname)
|
||||||
|
invite_code = stdout.strip()
|
||||||
|
yield self.do_join(0, unicode(self.local_dir), invite_code)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def tearDown(self):
|
||||||
|
yield super(ListMagicFolder, self).tearDown()
|
||||||
|
shutil.rmtree(self.basedir)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_list(self):
|
||||||
|
rc, stdout, stderr = yield self.do_list(0)
|
||||||
|
self.failUnlessEqual(rc, 0)
|
||||||
|
self.assertIn("default:", stdout)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_list_none(self):
|
||||||
|
yield self.do_leave(0)
|
||||||
|
rc, stdout, stderr = yield self.do_list(0)
|
||||||
|
self.failUnlessEqual(rc, 0)
|
||||||
|
self.assertIn("No magic-folders", stdout)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_list_json(self):
|
||||||
|
rc, stdout, stderr = yield self.do_list(0, json=True)
|
||||||
|
self.failUnlessEqual(rc, 0)
|
||||||
|
res = json.loads(stdout)
|
||||||
|
self.assertEqual(
|
||||||
|
dict(default=dict(directory=self.abs_local_dir_u)),
|
||||||
|
res,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StatusMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def setUp(self):
|
||||||
|
yield super(StatusMagicFolder, self).setUp()
|
||||||
|
self.basedir="mf_list"
|
||||||
|
self.set_up_grid(oneshare=True)
|
||||||
|
self.local_dir = os.path.join(self.basedir, "magic")
|
||||||
|
os.mkdir(self.local_dir)
|
||||||
|
self.abs_local_dir_u = abspath_expanduser_unicode(unicode(self.local_dir), long_path=False)
|
||||||
|
|
||||||
|
yield self.do_create_magic_folder(0)
|
||||||
|
(rc, stdout, stderr) = yield self.do_invite(0, self.alice_nickname)
|
||||||
|
invite_code = stdout.strip()
|
||||||
|
yield self.do_join(0, unicode(self.local_dir), invite_code)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def tearDown(self):
|
||||||
|
yield super(StatusMagicFolder, self).tearDown()
|
||||||
|
shutil.rmtree(self.basedir)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_status(self):
|
||||||
|
def json_for_cap(options, cap):
|
||||||
|
if cap.startswith('URI:DIR2:'):
|
||||||
|
return (
|
||||||
|
'dirnode',
|
||||||
|
{
|
||||||
|
"children": {
|
||||||
|
"foo": ('filenode', {
|
||||||
|
"size": 1234,
|
||||||
|
"metadata": {
|
||||||
|
"tahoe": {
|
||||||
|
"linkcrtime": 0.0,
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
},
|
||||||
|
"ro_uri": "read-only URI",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ('dirnode', {"children": {}})
|
||||||
|
jc = mock.patch(
|
||||||
|
"allmydata.scripts.magic_folder_cli._get_json_for_cap",
|
||||||
|
side_effect=json_for_cap,
|
||||||
|
)
|
||||||
|
|
||||||
|
def json_for_frag(options, fragment, method='GET', post_args=None):
|
||||||
|
return {}
|
||||||
|
jf = mock.patch(
|
||||||
|
"allmydata.scripts.magic_folder_cli._get_json_for_fragment",
|
||||||
|
side_effect=json_for_frag,
|
||||||
|
)
|
||||||
|
|
||||||
|
with jc, jf:
|
||||||
|
rc, stdout, stderr = yield self.do_status(0)
|
||||||
|
self.failUnlessEqual(rc, 0)
|
||||||
|
self.assertIn("default", stdout)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"foo (1.23 kB): good, version=1, created 47 years ago",
|
||||||
|
stdout,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_status_child_not_dirnode(self):
|
||||||
|
def json_for_cap(options, cap):
|
||||||
|
if cap.startswith('URI:DIR2'):
|
||||||
|
return (
|
||||||
|
'dirnode',
|
||||||
|
{
|
||||||
|
"children": {
|
||||||
|
"foo": ('filenode', {
|
||||||
|
"size": 1234,
|
||||||
|
"metadata": {
|
||||||
|
"tahoe": {
|
||||||
|
"linkcrtime": 0.0,
|
||||||
|
},
|
||||||
|
"version": 1,
|
||||||
|
},
|
||||||
|
"ro_uri": "read-only URI",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif cap == "read-only URI":
|
||||||
|
return {
|
||||||
|
"error": "bad stuff",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return ('dirnode', {"children": {}})
|
||||||
|
jc = mock.patch(
|
||||||
|
"allmydata.scripts.magic_folder_cli._get_json_for_cap",
|
||||||
|
side_effect=json_for_cap,
|
||||||
|
)
|
||||||
|
|
||||||
|
def json_for_frag(options, fragment, method='GET', post_args=None):
|
||||||
|
return {}
|
||||||
|
jf = mock.patch(
|
||||||
|
"allmydata.scripts.magic_folder_cli._get_json_for_fragment",
|
||||||
|
side_effect=json_for_frag,
|
||||||
|
)
|
||||||
|
|
||||||
|
with jc, jf:
|
||||||
|
rc, stdout, stderr = yield self.do_status(0)
|
||||||
|
self.failUnlessEqual(rc, 0)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"expected a dirnode",
|
||||||
|
stdout + stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_status_error_not_dircap(self):
|
||||||
|
def json_for_cap(options, cap):
|
||||||
|
if cap.startswith('URI:DIR2:'):
|
||||||
|
return (
|
||||||
|
'filenode',
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ('dirnode', {"children": {}})
|
||||||
|
jc = mock.patch(
|
||||||
|
"allmydata.scripts.magic_folder_cli._get_json_for_cap",
|
||||||
|
side_effect=json_for_cap,
|
||||||
|
)
|
||||||
|
|
||||||
|
def json_for_frag(options, fragment, method='GET', post_args=None):
|
||||||
|
return {}
|
||||||
|
jf = mock.patch(
|
||||||
|
"allmydata.scripts.magic_folder_cli._get_json_for_fragment",
|
||||||
|
side_effect=json_for_frag,
|
||||||
|
)
|
||||||
|
|
||||||
|
with jc, jf:
|
||||||
|
rc, stdout, stderr = yield self.do_status(0)
|
||||||
|
self.failUnlessEqual(rc, 2)
|
||||||
|
self.assertIn(
|
||||||
|
"magic_folder_dircap isn't a directory capability",
|
||||||
|
stdout + stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_status_nothing(self):
|
||||||
|
rc, stdout, stderr = yield self.do_status(0, name="blam")
|
||||||
|
self.assertIn("No such magic-folder 'blam'", stderr)
|
||||||
|
|
||||||
|
|
||||||
class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
||||||
def test_create_and_then_invite_join(self):
|
def test_create_and_then_invite_join(self):
|
||||||
self.basedir = "cli/MagicFolder/create-and-then-invite-join"
|
self.basedir = "cli/MagicFolder/create-and-then-invite-join"
|
||||||
self.set_up_grid(oneshare=True)
|
self.set_up_grid(oneshare=True)
|
||||||
local_dir = os.path.join(self.basedir, "magic")
|
local_dir = os.path.join(self.basedir, "magic")
|
||||||
|
os.mkdir(local_dir)
|
||||||
abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False)
|
abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False)
|
||||||
|
|
||||||
d = self.do_create_magic_folder(0)
|
d = self.do_create_magic_folder(0)
|
||||||
@ -230,6 +452,94 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
|||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_create_duplicate_name(self):
|
||||||
|
self.basedir = "cli/MagicFolder/create-dup"
|
||||||
|
self.set_up_grid(oneshare=True)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "create", "magic:", "--name", "foo",
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 0)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "create", "magic:", "--name", "foo",
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn(
|
||||||
|
"Already have a magic-folder named 'default'",
|
||||||
|
stderr
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_leave_wrong_folder(self):
|
||||||
|
self.basedir = "cli/MagicFolder/leave_wrong_folders"
|
||||||
|
yield self.set_up_grid(oneshare=True)
|
||||||
|
magic_dir = os.path.join(self.basedir, 'magic')
|
||||||
|
os.mkdir(magic_dir)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "create", "--name", "foo", "magic:", "my_name", magic_dir,
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 0)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "leave", "--name", "bar",
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertNotEqual(rc, 0)
|
||||||
|
self.assertIn(
|
||||||
|
"No such magic-folder 'bar'",
|
||||||
|
stdout + stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_leave_no_folder(self):
|
||||||
|
self.basedir = "cli/MagicFolder/leave_no_folders"
|
||||||
|
yield self.set_up_grid(oneshare=True)
|
||||||
|
magic_dir = os.path.join(self.basedir, 'magic')
|
||||||
|
os.mkdir(magic_dir)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "create", "--name", "foo", "magic:", "my_name", magic_dir,
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 0)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "leave", "--name", "foo",
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 0)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "leave", "--name", "foo",
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn(
|
||||||
|
"No magic-folders at all",
|
||||||
|
stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_leave_no_folders_at_all(self):
|
||||||
|
self.basedir = "cli/MagicFolder/leave_no_folders_at_all"
|
||||||
|
yield self.set_up_grid(oneshare=True)
|
||||||
|
|
||||||
|
rc, stdout, stderr = yield self.do_cli(
|
||||||
|
"magic-folder", "leave",
|
||||||
|
client_num=0,
|
||||||
|
)
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn(
|
||||||
|
"No magic-folders at all",
|
||||||
|
stderr,
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_invite_join(self):
|
def test_create_invite_join(self):
|
||||||
self.basedir = "cli/MagicFolder/create-invite-join"
|
self.basedir = "cli/MagicFolder/create-invite-join"
|
||||||
self.set_up_grid(oneshare=True)
|
self.set_up_grid(oneshare=True)
|
||||||
@ -297,8 +607,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
|||||||
def get_results(result):
|
def get_results(result):
|
||||||
(rc, out, err) = result
|
(rc, out, err) = result
|
||||||
self.failUnlessEqual(out, "")
|
self.failUnlessEqual(out, "")
|
||||||
self.failUnlessIn("This client has already joined a magic folder.", err)
|
self.failUnlessIn("This client already has a magic-folder", err)
|
||||||
self.failUnlessIn("Use the 'tahoe magic-folder leave' command first.", err)
|
|
||||||
self.failIfEqual(rc, 0)
|
self.failIfEqual(rc, 0)
|
||||||
d.addCallback(get_results)
|
d.addCallback(get_results)
|
||||||
return d
|
return d
|
||||||
@ -339,6 +648,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
|||||||
os.makedirs(self.basedir)
|
os.makedirs(self.basedir)
|
||||||
self.set_up_grid(oneshare=True)
|
self.set_up_grid(oneshare=True)
|
||||||
local_dir = os.path.join(self.basedir, "magic")
|
local_dir = os.path.join(self.basedir, "magic")
|
||||||
|
os.mkdir(local_dir)
|
||||||
abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False)
|
abs_local_dir_u = abspath_expanduser_unicode(unicode(local_dir), long_path=False)
|
||||||
|
|
||||||
self.invite_code = None
|
self.invite_code = None
|
||||||
@ -357,7 +667,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
|||||||
|
|
||||||
def check_success(result):
|
def check_success(result):
|
||||||
(rc, out, err) = result
|
(rc, out, err) = result
|
||||||
self.failUnlessEqual(rc, 0)
|
self.failUnlessEqual(rc, 0, out + err)
|
||||||
def check_failure(result):
|
def check_failure(result):
|
||||||
(rc, out, err) = result
|
(rc, out, err) = result
|
||||||
self.failIfEqual(rc, 0)
|
self.failIfEqual(rc, 0)
|
||||||
@ -367,9 +677,7 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
|||||||
d.addCallback(leave)
|
d.addCallback(leave)
|
||||||
d.addCallback(check_success)
|
d.addCallback(check_success)
|
||||||
|
|
||||||
collective_dircap_file = os.path.join(self.get_clientdir(i=0), u"private", u"collective_dircap")
|
magic_folder_db_file = os.path.join(self.get_clientdir(i=0), u"private", u"magicfolder_default.sqlite")
|
||||||
upload_dircap = os.path.join(self.get_clientdir(i=0), u"private", u"magic_folder_dircap")
|
|
||||||
magic_folder_db_file = os.path.join(self.get_clientdir(i=0), u"private", u"magicfolderdb.sqlite")
|
|
||||||
|
|
||||||
def check_join_if_file(my_file):
|
def check_join_if_file(my_file):
|
||||||
fileutil.write(my_file, "my file data")
|
fileutil.write(my_file, "my file data")
|
||||||
@ -377,10 +685,11 @@ class CreateMagicFolder(MagicFolderCLITestMixin, unittest.TestCase):
|
|||||||
d2.addCallback(check_failure)
|
d2.addCallback(check_failure)
|
||||||
return d2
|
return d2
|
||||||
|
|
||||||
for my_file in [collective_dircap_file, upload_dircap, magic_folder_db_file]:
|
for my_file in [magic_folder_db_file]:
|
||||||
d.addCallback(lambda ign, my_file: check_join_if_file(my_file), my_file)
|
d.addCallback(lambda ign, my_file: check_join_if_file(my_file), my_file)
|
||||||
d.addCallback(leave)
|
d.addCallback(leave)
|
||||||
d.addCallback(check_success)
|
# we didn't successfully join, so leaving should be an error
|
||||||
|
d.addCallback(check_failure)
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import allmydata
|
|||||||
import allmydata.frontends.magic_folder
|
import allmydata.frontends.magic_folder
|
||||||
import allmydata.util.log
|
import allmydata.util.log
|
||||||
|
|
||||||
from allmydata.node import OldConfigError, OldConfigOptionError, MissingConfigEntry, UnescapedHashError, _Config, read_config
|
from allmydata.node import OldConfigError, OldConfigOptionError, UnescapedHashError, _Config, read_config
|
||||||
from allmydata.frontends.auth import NeedRootcapLookupScheme
|
from allmydata.frontends.auth import NeedRootcapLookupScheme
|
||||||
from allmydata import client
|
from allmydata import client
|
||||||
from allmydata.storage_client import StorageFarmBroker
|
from allmydata.storage_client import StorageFarmBroker
|
||||||
@ -322,8 +322,8 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
|
|||||||
class MockMagicFolder(service.MultiService):
|
class MockMagicFolder(service.MultiService):
|
||||||
name = 'magic-folder'
|
name = 'magic-folder'
|
||||||
|
|
||||||
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, inotify=None,
|
def __init__(self, client, upload_dircap, collective_dircap, local_path_u, dbfile, umask, name,
|
||||||
uploader_delay=1.0, clock=None, downloader_delay=3):
|
inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3):
|
||||||
service.MultiService.__init__(self)
|
service.MultiService.__init__(self)
|
||||||
self.client = client
|
self.client = client
|
||||||
self._umask = umask
|
self._umask = umask
|
||||||
@ -349,15 +349,20 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
|
|||||||
|
|
||||||
basedir1 = "test_client.Basic.test_create_magic_folder_service1"
|
basedir1 = "test_client.Basic.test_create_magic_folder_service1"
|
||||||
os.mkdir(basedir1)
|
os.mkdir(basedir1)
|
||||||
|
os.mkdir(local_dir_u)
|
||||||
|
|
||||||
|
# which config-entry should be missing?
|
||||||
fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
|
fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
|
||||||
config + "local.directory = " + local_dir_utf8 + "\n")
|
config + "local.directory = " + local_dir_utf8 + "\n")
|
||||||
self.failUnlessRaises(MissingConfigEntry, client.create_client, basedir1)
|
self.failUnlessRaises(IOError, client.create_client, basedir1)
|
||||||
|
|
||||||
|
# local.directory entry missing .. but that won't be an error
|
||||||
|
# now, it'll just assume there are not magic folders
|
||||||
|
# .. hrm...should we make that an error (if enabled=true but
|
||||||
|
# there's not yaml AND no local.directory?)
|
||||||
fileutil.write(os.path.join(basedir1, "tahoe.cfg"), config)
|
fileutil.write(os.path.join(basedir1, "tahoe.cfg"), config)
|
||||||
fileutil.write(os.path.join(basedir1, "private", "magic_folder_dircap"), "URI:DIR2:blah")
|
fileutil.write(os.path.join(basedir1, "private", "magic_folder_dircap"), "URI:DIR2:blah")
|
||||||
fileutil.write(os.path.join(basedir1, "private", "collective_dircap"), "URI:DIR2:meow")
|
fileutil.write(os.path.join(basedir1, "private", "collective_dircap"), "URI:DIR2:meow")
|
||||||
self.failUnlessRaises(MissingConfigEntry, client.create_client, basedir1)
|
|
||||||
|
|
||||||
fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
|
fileutil.write(os.path.join(basedir1, "tahoe.cfg"),
|
||||||
config.replace("[magic_folder]\n", "[drop_upload]\n"))
|
config.replace("[magic_folder]\n", "[drop_upload]\n"))
|
||||||
@ -376,7 +381,7 @@ class Basic(testutil.ReallyEqualMixin, testutil.NonASCIIPathMixin, unittest.Test
|
|||||||
|
|
||||||
class Boom(Exception):
|
class Boom(Exception):
|
||||||
pass
|
pass
|
||||||
def BoomMagicFolder(client, upload_dircap, collective_dircap, local_path_u, dbfile,
|
def BoomMagicFolder(client, upload_dircap, collective_dircap, local_path_u, dbfile, name,
|
||||||
umask, inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3):
|
umask, inotify=None, uploader_delay=1.0, clock=None, downloader_delay=3):
|
||||||
raise Boom()
|
raise Boom()
|
||||||
self.patch(allmydata.frontends.magic_folder, 'MagicFolder', BoomMagicFolder)
|
self.patch(allmydata.frontends.magic_folder, 'MagicFolder', BoomMagicFolder)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import os, sys, time
|
import os, sys, time
|
||||||
import shutil, json
|
import shutil, json
|
||||||
|
from os.path import join, exists
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet import defer, task, reactor
|
from twisted.internet import defer, task, reactor
|
||||||
@ -8,7 +9,7 @@ from twisted.internet import defer, task, reactor
|
|||||||
from allmydata.interfaces import IDirectoryNode
|
from allmydata.interfaces import IDirectoryNode
|
||||||
from allmydata.util.assertutil import precondition
|
from allmydata.util.assertutil import precondition
|
||||||
|
|
||||||
from allmydata.util import fake_inotify, fileutil
|
from allmydata.util import fake_inotify, fileutil, configutil, yamlutil
|
||||||
from allmydata.util.encodingutil import get_filesystem_encoding, to_filepath
|
from allmydata.util.encodingutil import get_filesystem_encoding, to_filepath
|
||||||
from allmydata.util.consumer import download_to_data
|
from allmydata.util.consumer import download_to_data
|
||||||
from allmydata.test.no_network import GridTestMixin
|
from allmydata.test.no_network import GridTestMixin
|
||||||
@ -26,6 +27,259 @@ from allmydata.immutable.upload import Data
|
|||||||
_debug = False
|
_debug = False
|
||||||
|
|
||||||
|
|
||||||
|
class NewConfigUtilTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.basedir = abspath_expanduser_unicode(unicode(self.mktemp()))
|
||||||
|
os.mkdir(self.basedir)
|
||||||
|
self.local_dir = abspath_expanduser_unicode(unicode(self.mktemp()))
|
||||||
|
os.mkdir(self.local_dir)
|
||||||
|
privdir = join(self.basedir, "private")
|
||||||
|
os.mkdir(privdir)
|
||||||
|
|
||||||
|
self.poll_interval = 60
|
||||||
|
self.collective_dircap = u"a" * 32
|
||||||
|
self.magic_folder_dircap = u"b" * 32
|
||||||
|
|
||||||
|
self.folders = {
|
||||||
|
u"default": {
|
||||||
|
u"directory": self.local_dir,
|
||||||
|
u"upload_dircap": self.magic_folder_dircap,
|
||||||
|
u"collective_dircap": self.collective_dircap,
|
||||||
|
u"poll_interval": self.poll_interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# we need a bit of tahoe.cfg
|
||||||
|
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
|
||||||
|
f.write(
|
||||||
|
u"[magic_folder]\n"
|
||||||
|
u"enabled = True\n"
|
||||||
|
)
|
||||||
|
# ..and the yaml
|
||||||
|
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
|
||||||
|
with open(yaml_fname, "w") as f:
|
||||||
|
f.write(yamlutil.safe_dump({u"magic-folders": self.folders}))
|
||||||
|
|
||||||
|
def test_load(self):
|
||||||
|
folders = magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertEqual(['default'], list(folders.keys()))
|
||||||
|
|
||||||
|
def test_both_styles_of_config(self):
|
||||||
|
os.unlink(join(self.basedir, u"private", u"magic_folders.yaml"))
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"[magic_folder] is enabled but has no YAML file and no 'local.directory' option",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_obj(self):
|
||||||
|
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
|
||||||
|
with open(yaml_fname, "w") as f:
|
||||||
|
f.write('----\n')
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"should contain a dict",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_magic_folders(self):
|
||||||
|
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
|
||||||
|
with open(yaml_fname, "w") as f:
|
||||||
|
f.write('')
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"should contain a dict",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_magic_folders_not_dict(self):
|
||||||
|
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
|
||||||
|
with open(yaml_fname, "w") as f:
|
||||||
|
f.write('magic-folders: "foo"\n')
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"should be a dict",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"'magic-folders'",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_sub_obj(self):
|
||||||
|
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
|
||||||
|
with open(yaml_fname, "w") as f:
|
||||||
|
f.write("magic-folders:\n default: foo\n")
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"must itself be a dict",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_interval(self):
|
||||||
|
del self.folders[u"default"]["poll_interval"]
|
||||||
|
yaml_fname = join(self.basedir, u"private", u"magic_folders.yaml")
|
||||||
|
with open(yaml_fname, "w") as f:
|
||||||
|
f.write(yamlutil.safe_dump({u"magic-folders": self.folders}))
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"missing 'poll_interval'",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyConfigUtilTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# create a valid 'old style' magic-folder configuration
|
||||||
|
self.basedir = abspath_expanduser_unicode(unicode(self.mktemp()))
|
||||||
|
os.mkdir(self.basedir)
|
||||||
|
self.local_dir = abspath_expanduser_unicode(unicode(self.mktemp()))
|
||||||
|
os.mkdir(self.local_dir)
|
||||||
|
privdir = join(self.basedir, "private")
|
||||||
|
os.mkdir(privdir)
|
||||||
|
|
||||||
|
# state tests might need to know
|
||||||
|
self.poll_interval = 60
|
||||||
|
self.collective_dircap = u"a" * 32
|
||||||
|
self.magic_folder_dircap = u"b" * 32
|
||||||
|
|
||||||
|
# write fake config structure
|
||||||
|
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
|
||||||
|
f.write(
|
||||||
|
u"[magic_folder]\n"
|
||||||
|
u"enabled = True\n"
|
||||||
|
u"local.directory = {}\n"
|
||||||
|
u"poll_interval = {}\n".format(
|
||||||
|
self.local_dir,
|
||||||
|
self.poll_interval,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with open(join(privdir, "collective_dircap"), "w") as f:
|
||||||
|
f.write("{}\n".format(self.collective_dircap))
|
||||||
|
with open(join(privdir, "magic_folder_dircap"), "w") as f:
|
||||||
|
f.write("{}\n".format(self.magic_folder_dircap))
|
||||||
|
with open(join(privdir, "magicfolderdb.sqlite"), "w") as f:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_load_legacy_no_dir(self):
|
||||||
|
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
|
||||||
|
f.write(
|
||||||
|
u"[magic_folder]\n"
|
||||||
|
u"enabled = True\n"
|
||||||
|
u"local.directory = {}\n"
|
||||||
|
u"poll_interval = {}\n".format(
|
||||||
|
self.local_dir + 'foo',
|
||||||
|
self.poll_interval,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"there is no directory at that location",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_load_legacy_not_a_dir(self):
|
||||||
|
with open(join(self.basedir, u"tahoe.cfg"), "w") as f:
|
||||||
|
f.write(
|
||||||
|
u"[magic_folder]\n"
|
||||||
|
u"enabled = True\n"
|
||||||
|
u"local.directory = {}\n"
|
||||||
|
u"poll_interval = {}\n".format(
|
||||||
|
self.local_dir + "foo",
|
||||||
|
self.poll_interval,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with open(self.local_dir + "foo", "w") as f:
|
||||||
|
f.write("not a directory")
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"location is not a directory",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_load_legacy_and_new(self):
|
||||||
|
with open(join(self.basedir, u"private", u"magic_folders.yaml"), "w") as f:
|
||||||
|
f.write("---")
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
magic_folder.load_magic_folders(self.basedir)
|
||||||
|
self.assertIn(
|
||||||
|
"both old-style configuration and new-style",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_upgrade(self):
|
||||||
|
# test data is created in setUp; upgrade config
|
||||||
|
magic_folder._upgrade_magic_folder_config(self.basedir)
|
||||||
|
|
||||||
|
# ensure old stuff is gone
|
||||||
|
self.assertFalse(
|
||||||
|
exists(join(self.basedir, "private", "collective_dircap"))
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
exists(join(self.basedir, "private", "magic_folder_dircap"))
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
exists(join(self.basedir, "private", "magicfolderdb.sqlite"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure we've got the new stuff
|
||||||
|
self.assertTrue(
|
||||||
|
exists(join(self.basedir, "private", "magicfolder_default.sqlite"))
|
||||||
|
)
|
||||||
|
# what about config?
|
||||||
|
config = configutil.get_config(join(self.basedir, u"tahoe.cfg"))
|
||||||
|
self.assertFalse(config.has_option("magic_folder", "local.directory"))
|
||||||
|
|
||||||
|
def test_load_legacy(self):
|
||||||
|
folders = magic_folder.load_magic_folders(self.basedir)
|
||||||
|
|
||||||
|
self.assertEqual(['default'], list(folders.keys()))
|
||||||
|
self.assertTrue(
|
||||||
|
exists(join(self.basedir, "private", "collective_dircap"))
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
exists(join(self.basedir, "private", "magic_folder_dircap"))
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
exists(join(self.basedir, "private", "magicfolderdb.sqlite"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_load_legacy_upgrade(self):
|
||||||
|
magic_folder.maybe_upgrade_magic_folders(self.basedir)
|
||||||
|
folders = magic_folder.load_magic_folders(self.basedir)
|
||||||
|
|
||||||
|
self.assertEqual(['default'], list(folders.keys()))
|
||||||
|
# 'legacy' files should be gone
|
||||||
|
self.assertFalse(
|
||||||
|
exists(join(self.basedir, "private", "collective_dircap"))
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
exists(join(self.basedir, "private", "magic_folder_dircap"))
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
exists(join(self.basedir, "private", "magicfolderdb.sqlite"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MagicFolderDbTests(unittest.TestCase):
|
class MagicFolderDbTests(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -1034,10 +1288,11 @@ class SingleMagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, Reall
|
|||||||
return res
|
return res
|
||||||
d.addBoth(_disable_debugging)
|
d.addBoth(_disable_debugging)
|
||||||
d.addCallback(self.cleanup)
|
d.addCallback(self.cleanup)
|
||||||
|
shutil.rmtree(self.basedir, ignore_errors=True)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _createdb(self):
|
def _createdb(self):
|
||||||
dbfile = abspath_expanduser_unicode(u"magicfolderdb.sqlite", base=self.basedir)
|
dbfile = abspath_expanduser_unicode(u"magicfolder_default.sqlite", base=self.basedir)
|
||||||
mdb = magicfolderdb.get_magicfolderdb(dbfile, create_version=(magicfolderdb.SCHEMA_v1, 1))
|
mdb = magicfolderdb.get_magicfolderdb(dbfile, create_version=(magicfolderdb.SCHEMA_v1, 1))
|
||||||
self.failUnless(mdb, "unable to create magicfolderdb from %r" % (dbfile,))
|
self.failUnless(mdb, "unable to create magicfolderdb from %r" % (dbfile,))
|
||||||
self.failUnlessEqual(mdb.VERSION, 1)
|
self.failUnlessEqual(mdb.VERSION, 1)
|
||||||
@ -1051,7 +1306,7 @@ class SingleMagicFolderTestMixin(MagicFolderCLITestMixin, ShouldFailMixin, Reall
|
|||||||
|
|
||||||
def _wait_until_started(self, ign):
|
def _wait_until_started(self, ign):
|
||||||
#print "_wait_until_started"
|
#print "_wait_until_started"
|
||||||
self.magicfolder = self.get_client().getServiceNamed('magic-folder')
|
self.magicfolder = self.get_client().getServiceNamed('magic-folder-default')
|
||||||
self.fileops = FileOperationsHelper(self.magicfolder.uploader, self.inject_inotify)
|
self.fileops = FileOperationsHelper(self.magicfolder.uploader, self.inject_inotify)
|
||||||
self.up_clock = task.Clock()
|
self.up_clock = task.Clock()
|
||||||
self.down_clock = task.Clock()
|
self.down_clock = task.Clock()
|
||||||
@ -1348,24 +1603,24 @@ class MockTest(SingleMagicFolderTestMixin, unittest.TestCase):
|
|||||||
upload_dircap = n.get_uri()
|
upload_dircap = n.get_uri()
|
||||||
readonly_dircap = n.get_readonly_uri()
|
readonly_dircap = n.get_readonly_uri()
|
||||||
|
|
||||||
self.shouldFail(AssertionError, 'nonexistent local.directory', 'there is no directory',
|
self.shouldFail(ValueError, 'does not exist', 'does not exist',
|
||||||
MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077)
|
MagicFolder, client, upload_dircap, '', doesnotexist, magicfolderdb, 0077, 'default')
|
||||||
self.shouldFail(AssertionError, 'non-directory local.directory', 'is not a directory',
|
self.shouldFail(ValueError, 'is not a directory', 'is not a directory',
|
||||||
MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077)
|
MagicFolder, client, upload_dircap, '', not_a_dir, magicfolderdb, 0077, 'default')
|
||||||
self.shouldFail(AssertionError, 'bad upload.dircap', 'does not refer to a directory',
|
self.shouldFail(AssertionError, 'bad upload.dircap', 'does not refer to a directory',
|
||||||
MagicFolder, client, 'bad', '', errors_dir, magicfolderdb, 0077)
|
MagicFolder, client, 'bad', '', errors_dir, magicfolderdb, 0077, 'default')
|
||||||
self.shouldFail(AssertionError, 'non-directory upload.dircap', 'does not refer to a directory',
|
self.shouldFail(AssertionError, 'non-directory upload.dircap', 'does not refer to a directory',
|
||||||
MagicFolder, client, 'URI:LIT:foo', '', errors_dir, magicfolderdb, 0077)
|
MagicFolder, client, 'URI:LIT:foo', '', errors_dir, magicfolderdb, 0077, 'default')
|
||||||
self.shouldFail(AssertionError, 'readonly upload.dircap', 'is not a writecap to a directory',
|
self.shouldFail(AssertionError, 'readonly upload.dircap', 'is not a writecap to a directory',
|
||||||
MagicFolder, client, readonly_dircap, '', errors_dir, magicfolderdb, 0077)
|
MagicFolder, client, readonly_dircap, '', errors_dir, magicfolderdb, 0077, 'default')
|
||||||
self.shouldFail(AssertionError, 'collective dircap', 'is not a readonly cap to a directory',
|
self.shouldFail(AssertionError, 'collective dircap', 'is not a readonly cap to a directory',
|
||||||
MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077)
|
MagicFolder, client, upload_dircap, upload_dircap, errors_dir, magicfolderdb, 0077, 'default')
|
||||||
|
|
||||||
def _not_implemented():
|
def _not_implemented():
|
||||||
raise NotImplementedError("blah")
|
raise NotImplementedError("blah")
|
||||||
self.patch(magic_folder, 'get_inotify_module', _not_implemented)
|
self.patch(magic_folder, 'get_inotify_module', _not_implemented)
|
||||||
self.shouldFail(NotImplementedError, 'unsupported', 'blah',
|
self.shouldFail(NotImplementedError, 'unsupported', 'blah',
|
||||||
MagicFolder, client, upload_dircap, '', errors_dir, magicfolderdb, 0077)
|
MagicFolder, client, upload_dircap, '', errors_dir, magicfolderdb, 0077, 'default')
|
||||||
d.addCallback(_check_errors)
|
d.addCallback(_check_errors)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -109,6 +109,44 @@ class FakeUploader(service.Service):
|
|||||||
return (self.helper_furl, self.helper_connected)
|
return (self.helper_furl, self.helper_connected)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeStatus(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.status = []
|
||||||
|
|
||||||
|
def setServiceParent(self, p):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
|
||||||
|
class FakeStatusItem(object):
|
||||||
|
def __init__(self, p, history):
|
||||||
|
self.relpath_u = p
|
||||||
|
self.history = history
|
||||||
|
import mock
|
||||||
|
self.progress = mock.Mock()
|
||||||
|
self.progress.progress = 100.0
|
||||||
|
|
||||||
|
def status_history(self):
|
||||||
|
return self.history
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class FakeMagicFolder(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.uploader = FakeStatus()
|
||||||
|
self.downloader = FakeStatus()
|
||||||
|
|
||||||
|
def get_public_status(self):
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
[
|
||||||
|
'a magic-folder status message'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_one_ds():
|
def build_one_ds():
|
||||||
ds = DownloadStatus("storage_index", 1234)
|
ds = DownloadStatus("storage_index", 1234)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@ -243,7 +281,7 @@ class FakeClient(_Client):
|
|||||||
# don't upcall to Client.__init__, since we only want to initialize a
|
# don't upcall to Client.__init__, since we only want to initialize a
|
||||||
# minimal subset
|
# minimal subset
|
||||||
service.MultiService.__init__(self)
|
service.MultiService.__init__(self)
|
||||||
self._magic_folder = None
|
self._magic_folders = dict()
|
||||||
self.all_contents = {}
|
self.all_contents = {}
|
||||||
self.nodeid = "fake_nodeid"
|
self.nodeid = "fake_nodeid"
|
||||||
self.nickname = u"fake_nickname \u263A"
|
self.nickname = u"fake_nickname \u263A"
|
||||||
@ -281,6 +319,9 @@ class FakeClient(_Client):
|
|||||||
def get_long_tubid(self):
|
def get_long_tubid(self):
|
||||||
return "tubid"
|
return "tubid"
|
||||||
|
|
||||||
|
def get_auth_token(self):
|
||||||
|
return 'a fake debug auth token'
|
||||||
|
|
||||||
def startService(self):
|
def startService(self):
|
||||||
return service.MultiService.startService(self)
|
return service.MultiService.startService(self)
|
||||||
def stopService(self):
|
def stopService(self):
|
||||||
@ -936,6 +977,61 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
|
|||||||
d.addCallback(_check)
|
d.addCallback(_check)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_magicfolder_status_bad_token(self):
|
||||||
|
with self.assertRaises(Error):
|
||||||
|
yield self.POST(
|
||||||
|
'/magic_folder?t=json',
|
||||||
|
t='json',
|
||||||
|
name='default',
|
||||||
|
token='not the token you are looking for',
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_magicfolder_status_wrong_folder(self):
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
yield self.POST(
|
||||||
|
'/magic_folder?t=json',
|
||||||
|
t='json',
|
||||||
|
name='a non-existent magic-folder',
|
||||||
|
token=self.s.get_auth_token(),
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"Not Found",
|
||||||
|
str(ctx.exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_magicfolder_status_success(self):
|
||||||
|
self.s._magic_folders['default'] = mf = FakeMagicFolder()
|
||||||
|
mf.uploader.status = [
|
||||||
|
FakeStatusItem(u"rel/path", [('done', 12345)])
|
||||||
|
]
|
||||||
|
data = yield self.POST(
|
||||||
|
'/magic_folder?t=json',
|
||||||
|
t='json',
|
||||||
|
name='default',
|
||||||
|
token=self.s.get_auth_token(),
|
||||||
|
)
|
||||||
|
data = json.loads(data)
|
||||||
|
self.assertEqual(
|
||||||
|
data,
|
||||||
|
[
|
||||||
|
{"status": "done", "path": "rel/path", "kind": "upload", "percent_done": 100.0, "done_at": 12345},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_magicfolder_root_success(self):
|
||||||
|
self.s._magic_folders['default'] = mf = FakeMagicFolder()
|
||||||
|
mf.uploader.status = [
|
||||||
|
FakeStatusItem(u"rel/path", [('done', 12345)])
|
||||||
|
]
|
||||||
|
data = yield self.GET(
|
||||||
|
'/',
|
||||||
|
)
|
||||||
|
print(data)
|
||||||
|
|
||||||
def test_status(self):
|
def test_status(self):
|
||||||
h = self.s.get_history()
|
h = self.s.get_history()
|
||||||
dl_num = h.list_all_download_statuses()[0].get_counter()
|
dl_num = h.list_all_download_statuses()[0].get_counter()
|
||||||
|
@ -488,9 +488,12 @@ class TokenOnlyWebApi(resource.Resource):
|
|||||||
if t == u'json':
|
if t == u'json':
|
||||||
try:
|
try:
|
||||||
return self.post_json(req)
|
return self.post_json(req)
|
||||||
except Exception:
|
except WebError as e:
|
||||||
|
req.setResponseCode(e.code)
|
||||||
|
return json.dumps({"error": e.text})
|
||||||
|
except Exception as e:
|
||||||
message, code = humanize_failure(Failure())
|
message, code = humanize_failure(Failure())
|
||||||
req.setResponseCode(code)
|
req.setResponseCode(500 if code is None else code)
|
||||||
return json.dumps({"error": message})
|
return json.dumps({"error": message})
|
||||||
else:
|
else:
|
||||||
raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST)
|
raise WebError("'%s' invalid type for 't' arg" % (t,), http.BAD_REQUEST)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from allmydata.web.common import TokenOnlyWebApi
|
from allmydata.web.common import TokenOnlyWebApi, get_arg, WebError
|
||||||
|
|
||||||
|
|
||||||
class MagicFolderWebApi(TokenOnlyWebApi):
|
class MagicFolderWebApi(TokenOnlyWebApi):
|
||||||
@ -14,9 +14,18 @@ class MagicFolderWebApi(TokenOnlyWebApi):
|
|||||||
|
|
||||||
def post_json(self, req):
|
def post_json(self, req):
|
||||||
req.setHeader("content-type", "application/json")
|
req.setHeader("content-type", "application/json")
|
||||||
|
nick = get_arg(req, 'name', 'default')
|
||||||
|
|
||||||
|
try:
|
||||||
|
magic_folder = self.client._magic_folders[nick]
|
||||||
|
except KeyError:
|
||||||
|
raise WebError(
|
||||||
|
"No such magic-folder '{}'".format(nick),
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for item in self.client._magic_folder.uploader.get_status():
|
for item in magic_folder.uploader.get_status():
|
||||||
d = dict(
|
d = dict(
|
||||||
path=item.relpath_u,
|
path=item.relpath_u,
|
||||||
status=item.status_history()[-1][0],
|
status=item.status_history()[-1][0],
|
||||||
@ -27,7 +36,7 @@ class MagicFolderWebApi(TokenOnlyWebApi):
|
|||||||
d['percent_done'] = item.progress.progress
|
d['percent_done'] = item.progress.progress
|
||||||
data.append(d)
|
data.append(d)
|
||||||
|
|
||||||
for item in self.client._magic_folder.downloader.get_status():
|
for item in magic_folder.downloader.get_status():
|
||||||
d = dict(
|
d = dict(
|
||||||
path=item.relpath_u,
|
path=item.relpath_u,
|
||||||
status=item.status_history()[-1][0],
|
status=item.status_history()[-1][0],
|
||||||
|
@ -237,12 +237,13 @@ class Root(MultiFormatPage):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
def render_magic_folder(self, ctx, data):
|
def data_magic_folders(self, ctx, data):
|
||||||
if self.client._magic_folder is None:
|
return self.client._magic_folders.keys()
|
||||||
return T.p()
|
|
||||||
|
|
||||||
(ok, messages) = self.client._magic_folder.get_public_status()
|
|
||||||
|
|
||||||
|
def render_magic_folder_row(self, ctx, data):
|
||||||
|
magic_folder = self.client._magic_folders[data]
|
||||||
|
(ok, messages) = magic_folder.get_public_status()
|
||||||
|
ctx.fillSlots("magic_folder_name", data)
|
||||||
if ok:
|
if ok:
|
||||||
ctx.fillSlots("magic_folder_status", "yes")
|
ctx.fillSlots("magic_folder_status", "yes")
|
||||||
ctx.fillSlots("magic_folder_status_alt", "working")
|
ctx.fillSlots("magic_folder_status_alt", "working")
|
||||||
@ -250,12 +251,16 @@ class Root(MultiFormatPage):
|
|||||||
ctx.fillSlots("magic_folder_status", "no")
|
ctx.fillSlots("magic_folder_status", "no")
|
||||||
ctx.fillSlots("magic_folder_status_alt", "not working")
|
ctx.fillSlots("magic_folder_status_alt", "not working")
|
||||||
|
|
||||||
status = T.ul()
|
status = T.ul(class_="magic-folder-status")
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
status[T.li[str(msg)]]
|
status[T.li[str(msg)]]
|
||||||
|
|
||||||
return ctx.tag[status]
|
return ctx.tag[status]
|
||||||
|
|
||||||
|
def render_magic_folder(self, ctx, data):
|
||||||
|
if not self.client._magic_folders:
|
||||||
|
return T.p()
|
||||||
|
return ctx.tag
|
||||||
|
|
||||||
def render_services(self, ctx, data):
|
def render_services(self, ctx, data):
|
||||||
ul = T.ul()
|
ul = T.ul()
|
||||||
try:
|
try:
|
||||||
|
@ -53,6 +53,11 @@ body {
|
|||||||
.connection-status {
|
.connection-status {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.magic-folder-status {
|
||||||
|
clear: left;
|
||||||
|
margin-left: 40px; /* width of status-indicator + margins */
|
||||||
|
}
|
||||||
|
|
||||||
.furl {
|
.furl {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
@ -160,10 +160,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div n:render="magic_folder" class="row-fluid">
|
<div n:render="magic_folder" class="row-fluid">
|
||||||
<h2>
|
<h2>Magic Folders</h2>
|
||||||
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="magic_folder_status" />.png</n:attr><n:attr name="alt"><n:slot name="magic_folder_status_alt" /></n:attr></img></div>
|
<div n:render="sequence" n:data="magic_folders">
|
||||||
Magic Folder
|
<div n:pattern="item" n:render="magic_folder_row"><div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="magic_folder_status" />.png</n:attr><n:attr name="alt"><n:slot name="magic_folder_status_alt" /></n:attr></img></div><h3><n:slot name="magic_folder_name" /></h3></div>
|
||||||
</h2>
|
</div>
|
||||||
</div><!--/row-->
|
</div><!--/row-->
|
||||||
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user