mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-07-29 12:09:37 +00:00
Add Tahoe-LAFS module
- Created basic plinth app which starts an introducer and a storage node on the FreedomBox. - Prompt user to set a domain name before creating Tahoe-LAFS nodes. - Support adding and removing of introducers to the storage node. - Serve Tahoe-LAFS from a different port. - Start all nodes and introducers at system startup. - Add utility class YAMLFile with test cases.
This commit is contained in:
parent
1b42d3bf53
commit
5ad180fcc9
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ plinth/tests/coverage/report/
|
||||
.idea/
|
||||
.DS_Store
|
||||
*.box
|
||||
.eggs
|
||||
|
||||
3
LICENSES
3
LICENSES
@ -44,6 +44,7 @@ otherwise.
|
||||
- static/themes/default/img/network-wireless.svg :: [[http://tango.freedesktop.org/][Public Domain]]
|
||||
- static/themes/default/icons/deluge.png :: [[https://upload.wikimedia.org/wikipedia/commons/thumb/8/85//Deluge-Logo.svg/2000px-Deluge-Logo.svg.png][GPL]]
|
||||
- static/themes/default/icons/diaspora.png :: [[https://upload.wikimedia.org/wikipedia/commons/thumb/8/85//Deluge-Logo.svg/2000px-Deluge-Logo.svg.png][Publc Domain]]
|
||||
- static/themes/default/icons/ejabberd.png :: [[https://www.ejabberd.im/][GPL-2]]
|
||||
- static/themes/default/icons/infinoted.png :: [[https://github.com/gobby/gobby/blob/master/COPYING][ISC]]
|
||||
- static/themes/default/icons/ikiwiki.png :: [[https://ikiwiki.info/][GPL-2+]]
|
||||
- static/themes/default/icons/jsxc.png :: -
|
||||
@ -58,6 +59,6 @@ otherwise.
|
||||
- static/themes/default/icons/roundcube.png :: [[https://roundcube.net/][GPL-3+]]
|
||||
- static/themes/default/icons/shaarli.png :: [[https://github.com/shaarli/Shaarli][zlib/libpng]]
|
||||
- static/themes/default/icons/syncthing.png :: [[https://github.com/syncthing/syncthing/][Mozilla Public License Version 2.0]]
|
||||
- static/themes/default/icons/tahoe.png :: [[https://github.com/thekishanraval/Logos][GPLv3+]]
|
||||
- static/themes/default/icons/transmission.png :: [[https://transmissionbt.com/][GPL]]
|
||||
- static/themes/default/icons/ttrss.png :: [[https://tt-rss.org/gitlab/fox/tt-rss][GPL]]
|
||||
- static/themes/default/icons/ejabberd.png :: [[https://www.ejabberd.im/][GPL-2]]
|
||||
|
||||
260
actions/tahoe-lafs
Executable file
260
actions/tahoe-lafs
Executable file
@ -0,0 +1,260 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- mode: python -*-
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Configuration helper for Tahoe-LAFS.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import augeas
|
||||
import grp
|
||||
import json
|
||||
import pwd
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import os
|
||||
import ruamel.yaml
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth.modules.tahoe import (
|
||||
introducer_name,
|
||||
introducers_file,
|
||||
storage_node_name,
|
||||
tahoe_home,
|
||||
introducer_furl_file)
|
||||
from plinth.modules.tahoe.errors import TahoeConfigurationError
|
||||
from plinth.utils import YAMLFile
|
||||
|
||||
|
||||
domain_name_file = os.path.join(tahoe_home, 'domain_name')
|
||||
|
||||
DEFAULT_FILE = '/etc/default/tahoe-lafs'
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Return parsed command line arguments as dictionary."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||
|
||||
subparsers.add_parser('enable', help='Enable Tahoe-LAFS')
|
||||
subparsers.add_parser('disable', help='Disable Tahoe-LAFS')
|
||||
setup = subparsers.add_parser('setup',
|
||||
help='Set domain name for Tahoe-LAFS')
|
||||
setup.add_argument('--domain-name',
|
||||
help='The domain name to be used by Tahoe-LAFS')
|
||||
subparsers.add_parser('create-introducer',
|
||||
help='Create and start the introducer node')
|
||||
subparsers.add_parser('create-storage-node',
|
||||
help='Create and start the storage node')
|
||||
subparsers.add_parser('autostart',
|
||||
help="Automatically start all introducers and "
|
||||
"storage nodes on system startup")
|
||||
intro_parser_add = subparsers.add_parser(
|
||||
'add-introducer', help="Add an introducer to the storage node's list "
|
||||
"of introducers.")
|
||||
intro_parser_add.add_argument(
|
||||
'--introducer', help="Add an introducer to the storage node's list "
|
||||
"of introducers Param introducer must be a tuple "
|
||||
"of (pet_name, furl)")
|
||||
intro_parser_remove = subparsers.add_parser(
|
||||
'remove-introducer', help="Rename the introducer entry in the "
|
||||
"introducers.yaml file specified by the "
|
||||
"param")
|
||||
intro_parser_remove.add_argument(
|
||||
'--pet-name', help='The domain name that will be used by '
|
||||
'Tahoe-LAFS')
|
||||
subparsers.add_parser('get-introducers',
|
||||
help="Return a dictionary of all introducers and "
|
||||
"their furls added to the storage node running "
|
||||
"on this FreedomBox.")
|
||||
subparsers.add_parser('get-local-introducer',
|
||||
help="Return the name and furl of the introducer "
|
||||
"created on this FreedomBox")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def subcommand_setup(arguments):
|
||||
"""Actions to be performed after installing Tahoe-LAFS."""
|
||||
# Create tahoe group if needed.
|
||||
try:
|
||||
grp.getgrnam('tahoe-lafs')
|
||||
except KeyError:
|
||||
subprocess.run(['addgroup', 'tahoe-lafs'], check=True)
|
||||
|
||||
# Create tahoe user if needed.
|
||||
try:
|
||||
pwd.getpwnam('tahoe-lafs')
|
||||
except KeyError:
|
||||
subprocess.run(
|
||||
[
|
||||
'adduser', '--system', '--ingroup', 'tahoe-lafs',
|
||||
'--home', '/var/lib/tahoe-lafs',
|
||||
'--gecos', 'Tahoe-LAFS distributed file system', 'tahoe-lafs'
|
||||
],
|
||||
check=True)
|
||||
|
||||
if not os.path.exists(tahoe_home):
|
||||
os.makedirs(tahoe_home, mode=0o755)
|
||||
|
||||
shutil.chown(tahoe_home, user='tahoe-lafs', group='tahoe-lafs')
|
||||
|
||||
if not os.path.exists(domain_name_file):
|
||||
with open(domain_name_file, 'w') as dnf:
|
||||
dnf.write(arguments.domain_name)
|
||||
|
||||
|
||||
def subcommand_autostart(_):
|
||||
"""Automatically start all introducers and storage nodes on system startup.
|
||||
"""
|
||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||
aug.set('/augeas/load/Shellvars/lens', 'Shellvars.lns')
|
||||
aug.set('/augeas/load/Shellvars/incl[last() + 1]', DEFAULT_FILE)
|
||||
aug.load()
|
||||
|
||||
aug.set('/files' + DEFAULT_FILE + '/AUTOSTART', 'all')
|
||||
aug.save()
|
||||
|
||||
|
||||
def get_configured_domain_name():
|
||||
"""Extract and return the domain name from the domain name file.
|
||||
|
||||
Throws TahoeConfigurationError if the domain name file is not found.
|
||||
"""
|
||||
if not os.path.exists(domain_name_file):
|
||||
raise TahoeConfigurationError
|
||||
else:
|
||||
with open(domain_name_file) as dnf:
|
||||
return dnf.read().rstrip()
|
||||
|
||||
|
||||
def subcommand_create_introducer(_):
|
||||
"""Create a Tahoe-LAFS introducer on this FreedomBox."""
|
||||
os.chdir(tahoe_home)
|
||||
|
||||
if not os.path.exists(os.path.join(tahoe_home, introducer_name)):
|
||||
subprocess.check_call([
|
||||
'tahoe', 'create-introducer', '--port=3456',
|
||||
'--location=tcp:{}:3456'.format(get_configured_domain_name()),
|
||||
introducer_name
|
||||
])
|
||||
|
||||
subprocess.call(['tahoe', 'start', introducer_name])
|
||||
|
||||
|
||||
def subcommand_create_storage_node(_):
|
||||
"""Create a Tahoe-LAFS storage node on this FreedomBox."""
|
||||
os.chdir(tahoe_home)
|
||||
|
||||
if not os.path.exists(os.path.join(tahoe_home, storage_node_name)):
|
||||
subprocess.check_call([
|
||||
'tahoe', 'create-node', '--nickname=\"storage_node\"',
|
||||
'--webport=1234',
|
||||
'--hostname={}'.format(get_configured_domain_name()),
|
||||
storage_node_name
|
||||
])
|
||||
with open(
|
||||
os.path.join(tahoe_home, introducer_name, 'private',
|
||||
introducer_name + '.furl'), 'r') as furl_file:
|
||||
furl = furl_file.read().rstrip()
|
||||
conf_dict = {'introducers': {introducer_name: {'furl': furl}}}
|
||||
conf_yaml = ruamel.yaml.dump(
|
||||
conf_dict, Dumper=ruamel.yaml.RoundTripDumper)
|
||||
with open(
|
||||
os.path.join(tahoe_home, storage_node_name, 'private',
|
||||
'introducers.yaml'), 'w') as introducers_file:
|
||||
introducers_file.write(conf_yaml)
|
||||
|
||||
subprocess.call(['tahoe', 'start', storage_node_name])
|
||||
|
||||
|
||||
def subcommand_add_introducer(arguments):
|
||||
"""Add an introducer to the storage node's list of introducers.
|
||||
|
||||
Param introducer must be a tuple of (pet_name, furl).
|
||||
"""
|
||||
with YAMLFile(introducers_file, restart_storage_node) as conf:
|
||||
pet_name, furl = arguments.introducer.split(",")
|
||||
conf['introducers'][pet_name] = {'furl': furl}
|
||||
|
||||
|
||||
def subcommand_remove_introducer(arguments):
|
||||
"""Rename the introducer entry in the introducers.yaml file specified
|
||||
by the param pet_name
|
||||
"""
|
||||
with YAMLFile(introducers_file, restart_storage_node) as conf:
|
||||
del conf['introducers'][arguments.pet_name]
|
||||
|
||||
|
||||
def subcommand_get_introducers(_):
|
||||
"""Return a dictionary of all introducers and their furls.
|
||||
|
||||
The ones added to the storage node running on this FreedomBox.
|
||||
"""
|
||||
with open(introducers_file, 'r') as intro_conf:
|
||||
conf = ruamel.yaml.round_trip_load(intro_conf)
|
||||
|
||||
introducers = []
|
||||
for pet_name in conf['introducers'].keys():
|
||||
introducers.append((pet_name, conf['introducers'][pet_name]['furl']))
|
||||
|
||||
print(json.dumps(introducers))
|
||||
|
||||
|
||||
def subcommand_get_local_introducer(_):
|
||||
"""Return the name and furl of the introducer created on this FreedomBox
|
||||
"""
|
||||
with open(introducer_furl_file, 'r') as furl_file:
|
||||
furl = furl_file.read().rstrip()
|
||||
|
||||
print(json.dumps((introducer_name, furl)))
|
||||
|
||||
|
||||
def subcommand_enable(_):
|
||||
"""Enable web configuration and reload."""
|
||||
action_utils.service_enable('tahoe-lafs')
|
||||
action_utils.webserver_enable('tahoe-plinth')
|
||||
|
||||
|
||||
def subcommand_disable(_):
|
||||
"""Disable web configuration and reload."""
|
||||
action_utils.webserver_disable('tahoe-plinth')
|
||||
action_utils.service_disable('tahoe-lafs')
|
||||
|
||||
|
||||
def restart_storage_node():
|
||||
"""Called after exiting context of editing introducers file."""
|
||||
try:
|
||||
subprocess.run(['tahoe', 'restart', 'storage_node'], check=True)
|
||||
except subprocess.CalledProcessError as err:
|
||||
print('Failed to restart storage_node with new configuration: %s', err)
|
||||
|
||||
|
||||
def main():
|
||||
"""Parse arguments and perform all duties."""
|
||||
arguments = parse_arguments()
|
||||
|
||||
subcommand = arguments.subcommand.replace('-', '_')
|
||||
subcommand_method = globals()['subcommand_' + subcommand]
|
||||
subcommand_method(arguments)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
14
data/etc/apache2/conf-available/tahoe-plinth.conf
Normal file
14
data/etc/apache2/conf-available/tahoe-plinth.conf
Normal file
@ -0,0 +1,14 @@
|
||||
# Tahoe-LAFS Storage Node web interface
|
||||
|
||||
Listen 5678
|
||||
|
||||
# XXX: SSL is not configured?
|
||||
# TODO: Use subdomain?
|
||||
<VirtualHost *:5678>
|
||||
<Location "/">
|
||||
Include includes/freedombox-auth-ldap.conf
|
||||
Require ldap-group cn=admin,ou=groups,dc=thisbox
|
||||
|
||||
ProxyPass http://localhost:1234/
|
||||
</Location>
|
||||
</VirtualHost>
|
||||
1
data/etc/plinth/modules-enabled/tahoe
Normal file
1
data/etc/plinth/modules-enabled/tahoe
Normal file
@ -0,0 +1 @@
|
||||
plinth.modules.tahoe
|
||||
7
data/usr/lib/firewalld/services/tahoe-plinth.xml
Normal file
7
data/usr/lib/firewalld/services/tahoe-plinth.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<service>
|
||||
<short>Tahoe-LAFS</short>
|
||||
<description>Tahoe-LAFS is a distributed file storage system</description>
|
||||
<port protocol="tcp" port="5678"/>
|
||||
<port protocol="tcp" port="3456"/>
|
||||
</service>
|
||||
@ -98,7 +98,6 @@ import subprocess
|
||||
from plinth import cfg
|
||||
from plinth.errors import ActionError
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -118,7 +117,17 @@ def superuser_run(action, options=None, input=None, async=False):
|
||||
return _run(action, options, input, async, True)
|
||||
|
||||
|
||||
def _run(action, options=None, input=None, async=False, run_as_root=False):
|
||||
def run_as_user(action, options=None, input=None, async=False,
|
||||
become_user=None):
|
||||
"""Run a command as a different user.
|
||||
|
||||
If become_user is None, run as current user.
|
||||
"""
|
||||
return _run(action, options, input, async, False, become_user)
|
||||
|
||||
|
||||
def _run(action, options=None, input=None, async=False, run_as_root=False,
|
||||
become_user=None):
|
||||
"""Safely run a specific action as a normal user or root.
|
||||
|
||||
Actions are pulled from the actions directory.
|
||||
@ -159,6 +168,8 @@ def _run(action, options=None, input=None, async=False, run_as_root=False):
|
||||
# Contract 1: commands can run via sudo.
|
||||
if run_as_root:
|
||||
cmd = ['sudo', '-n'] + cmd
|
||||
elif become_user:
|
||||
cmd = ['sudo', '-n', '-u', become_user] + cmd
|
||||
|
||||
LOGGER.info('Executing command - %s', cmd)
|
||||
|
||||
|
||||
@ -22,9 +22,26 @@ Common forms for use by modules.
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth import utils
|
||||
|
||||
|
||||
class ServiceForm(forms.Form):
|
||||
"""Generic configuration form for a service."""
|
||||
is_enabled = forms.BooleanField(
|
||||
label=_('Enable application'),
|
||||
required=False)
|
||||
|
||||
|
||||
class DomainSelectionForm(forms.Form):
|
||||
"""Form for selecting a domain name to be used for
|
||||
distributed federated applications
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['domain_name'].choices = utils.get_domain_names()
|
||||
|
||||
domain_name = forms.ChoiceField(
|
||||
label=_('Select the domain name to be used for this application'),
|
||||
choices=[]
|
||||
)
|
||||
|
||||
@ -18,7 +18,6 @@ import os
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth.modules import names
|
||||
from plinth.utils import format_lazy
|
||||
from plinth import actions, action_utils, frontpage, \
|
||||
service as service_module
|
||||
@ -66,7 +65,7 @@ description = [
|
||||
'<a href="https://diaspora.{host}">diaspora.{host}</a> path on the '
|
||||
'web server.'.format(host=get_configured_domain_name()) if is_setup()
|
||||
else 'Please register a domain name for your FreedomBox to be able to'
|
||||
' federate with other diaspora* pods.')
|
||||
' federate with other diaspora* pods.')
|
||||
]
|
||||
|
||||
|
||||
@ -112,19 +111,6 @@ def setup(helper, old_version=None):
|
||||
helper.call('post', add_shortcut)
|
||||
|
||||
|
||||
def get_domain_names():
|
||||
"""Return the domain name(s)"""
|
||||
results = []
|
||||
|
||||
for domain_type, domains in names.domains.items():
|
||||
if domain_type == 'hiddenservice':
|
||||
continue
|
||||
for domain in domains:
|
||||
results.append((domain, domain))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def add_shortcut():
|
||||
"""Add shortcut to diaspora on the Plinth homepage"""
|
||||
if is_setup():
|
||||
|
||||
@ -25,6 +25,7 @@ from django.views.generic import FormView
|
||||
from plinth import actions
|
||||
from plinth.modules import diaspora
|
||||
from plinth.modules.diaspora.forms import DiasporaForm
|
||||
from plinth.utils import get_domain_names
|
||||
from plinth.views import ServiceView
|
||||
|
||||
|
||||
@ -48,7 +49,7 @@ class DiasporaSetupView(FormView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['description'] = self.description
|
||||
context['title'] = self.title
|
||||
context['domain_names'] = diaspora.get_domain_names()
|
||||
context['domain_names'] = get_domain_names()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@ -31,8 +31,6 @@ from plinth import actions
|
||||
from plinth import frontpage
|
||||
from plinth import service as service_module
|
||||
from plinth.menu import main_menu
|
||||
from plinth.modules import names
|
||||
|
||||
|
||||
version = 1
|
||||
|
||||
@ -139,20 +137,6 @@ def diagnose():
|
||||
return results
|
||||
|
||||
|
||||
def get_domain_names():
|
||||
"""Return the domain name(s)."""
|
||||
results = []
|
||||
|
||||
for domain_type, domains in names.domains.items():
|
||||
if domain_type == 'hiddenservice':
|
||||
continue
|
||||
|
||||
for domain in domains:
|
||||
results.append((domain, domain))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_configured_domain_name():
|
||||
"""Return the currently configured domain name."""
|
||||
if not is_setup():
|
||||
|
||||
@ -22,7 +22,7 @@ Forms for configuring matrix-synapse.
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth.modules import matrixsynapse
|
||||
from plinth.utils import get_domain_names
|
||||
|
||||
|
||||
class MatrixSynapseForm(forms.Form):
|
||||
@ -35,4 +35,4 @@ class MatrixSynapseForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the form object."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['domain_name'].choices = matrixsynapse.get_domain_names()
|
||||
self.fields['domain_name'].choices = get_domain_names()
|
||||
|
||||
@ -27,6 +27,7 @@ from plinth import actions
|
||||
from plinth import views
|
||||
from plinth.modules import matrixsynapse
|
||||
from plinth.modules.matrixsynapse.forms import MatrixSynapseForm
|
||||
from plinth.utils import get_domain_names
|
||||
|
||||
|
||||
class SetupView(FormView):
|
||||
@ -49,7 +50,7 @@ class SetupView(FormView):
|
||||
|
||||
context['title'] = matrixsynapse.title
|
||||
context['description'] = matrixsynapse.description
|
||||
context['domain_names'] = matrixsynapse.get_domain_names()
|
||||
context['domain_names'] = get_domain_names()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
218
plinth/modules/tahoe/__init__.py
Normal file
218
plinth/modules/tahoe/__init__.py
Normal file
@ -0,0 +1,218 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Plinth module to configure Tahoe-LAFS.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from plinth import action_utils
|
||||
from plinth import actions
|
||||
from plinth import cfg
|
||||
from plinth import frontpage
|
||||
from plinth import service as service_module
|
||||
from plinth.menu import main_menu
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from .errors import TahoeConfigurationError
|
||||
|
||||
version = 1
|
||||
|
||||
managed_services = ['tahoe-lafs']
|
||||
|
||||
managed_packages = ['tahoe-lafs']
|
||||
|
||||
title = _('Distributed File Storage (Tahoe-LAFS)')
|
||||
|
||||
service = None
|
||||
|
||||
tahoe_home = '/var/lib/tahoe-lafs'
|
||||
introducer_name = 'introducer'
|
||||
storage_node_name = 'storage_node'
|
||||
domain_name_file = os.path.join(tahoe_home, 'domain_name')
|
||||
introducers_file = os.path.join(
|
||||
tahoe_home, '{}/private/introducers.yaml'.format(storage_node_name))
|
||||
introducer_furl_file = os.path.join(
|
||||
tahoe_home, '{0}/private/{0}.furl'.format(introducer_name))
|
||||
|
||||
|
||||
def is_setup():
|
||||
"""Check whether Tahoe-LAFS is setup"""
|
||||
return os.path.exists(domain_name_file)
|
||||
|
||||
|
||||
def get_configured_domain_name():
|
||||
"""Extract and return the domain name from the domain name file.
|
||||
Throws TahoeConfigurationError if the domain name file is not found.
|
||||
"""
|
||||
if not os.path.exists(domain_name_file):
|
||||
raise TahoeConfigurationError
|
||||
else:
|
||||
with open(domain_name_file) as dnf:
|
||||
return dnf.read().rstrip()
|
||||
|
||||
|
||||
description = [
|
||||
_('Tahoe-LAFS is a decentralized secure file storage system. '
|
||||
'It uses provider independent security to store files over a '
|
||||
'distributed network of storage nodes. Even if some of the nodes fail, '
|
||||
'your files can be retrieved from the remaining nodes.'),
|
||||
format_lazy(
|
||||
_('This {box_name} hosts a storage node and an introducer by default. '
|
||||
'Additional introducers can be added, which will introduce this '
|
||||
'node to the other storage nodes.'),
|
||||
box_name=_(cfg.box_name)),
|
||||
]
|
||||
|
||||
|
||||
def init():
|
||||
"""Intialize the module."""
|
||||
menu = main_menu.get('apps')
|
||||
menu.add_urlname(title, 'glyphicon-hdd', 'tahoe:index')
|
||||
|
||||
global service
|
||||
setup_helper = globals()['setup_helper']
|
||||
if setup_helper.get_state() != 'needs-setup' and is_setup():
|
||||
service = service_module.Service(
|
||||
managed_services[0],
|
||||
title,
|
||||
ports=['tahoe-plinth'],
|
||||
is_external=True,
|
||||
is_enabled=is_enabled,
|
||||
enable=enable,
|
||||
disable=disable,
|
||||
is_running=is_running)
|
||||
|
||||
if is_enabled():
|
||||
add_shortcut()
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
helper.install(managed_packages)
|
||||
|
||||
|
||||
def post_setup(configured_domain_name):
|
||||
"""Actions to be performed after installing tahoe-lafs package."""
|
||||
actions.superuser_run('tahoe-lafs',
|
||||
['setup', '--domain-name', configured_domain_name])
|
||||
actions.superuser_run('tahoe-lafs', ['enable'])
|
||||
actions.run_as_user('tahoe-lafs', ['create-introducer'],
|
||||
become_user='tahoe-lafs')
|
||||
actions.run_as_user('tahoe-lafs', ['create-storage-node'],
|
||||
become_user='tahoe-lafs')
|
||||
actions.superuser_run('tahoe-lafs', ['autostart'])
|
||||
|
||||
global service
|
||||
if service is None:
|
||||
service = service_module.Service(
|
||||
managed_services[0],
|
||||
title,
|
||||
ports=['tahoe-plinth'],
|
||||
is_external=True,
|
||||
is_enabled=is_enabled,
|
||||
enable=enable,
|
||||
disable=disable,
|
||||
is_running=is_running)
|
||||
service.notify_enabled(None, True)
|
||||
add_shortcut()
|
||||
|
||||
|
||||
def add_shortcut():
|
||||
"""Helper method to add a shortcut to the front page."""
|
||||
# BUG: Current logo appears squashed on front page.
|
||||
frontpage.add_shortcut(
|
||||
'tahoe-lafs', title,
|
||||
url='https://{}:5678'.format(get_configured_domain_name()),
|
||||
login_required=True)
|
||||
|
||||
|
||||
def is_running():
|
||||
"""Return whether the service is running."""
|
||||
return action_utils.service_is_running(managed_services[0])
|
||||
|
||||
|
||||
def is_enabled():
|
||||
"""Return whether the module is enabled."""
|
||||
return (action_utils.service_is_enabled(managed_services[0]) and
|
||||
action_utils.webserver_is_enabled('tahoe-plinth'))
|
||||
|
||||
|
||||
def enable():
|
||||
"""Enable the module."""
|
||||
actions.superuser_run('tahoe-lafs', ['enable'])
|
||||
add_shortcut()
|
||||
|
||||
|
||||
def disable():
|
||||
"""Enable the module."""
|
||||
actions.superuser_run('tahoe-lafs', ['disable'])
|
||||
frontpage.remove_shortcut('tahoe-lafs')
|
||||
|
||||
|
||||
def diagnose():
|
||||
"""Run diagnostics and return the results."""
|
||||
return [action_utils.diagnose_url(
|
||||
'http://localhost:5678', kind='4', check_certificate=False),
|
||||
action_utils.diagnose_url(
|
||||
'http://localhost:5678', kind='6', check_certificate=False),
|
||||
action_utils.diagnose_url(
|
||||
'http://{}:5678'.format(get_configured_domain_name()),
|
||||
kind='4',
|
||||
check_certificate=False)]
|
||||
|
||||
|
||||
def add_introducer(introducer):
|
||||
"""Add an introducer to the storage node's list of introducers.
|
||||
Param introducer must be a tuple of (pet_name, furl)
|
||||
"""
|
||||
actions.run_as_user('tahoe-lafs',
|
||||
['add-introducer',
|
||||
"--introducer",
|
||||
",".join(introducer)],
|
||||
become_user='tahoe-lafs')
|
||||
|
||||
|
||||
def remove_introducer(pet_name):
|
||||
"""Rename the introducer entry in the introducers.yaml file specified by
|
||||
the param pet_name.
|
||||
"""
|
||||
actions.run_as_user('tahoe-lafs',
|
||||
['remove-introducer', '--pet-name', pet_name],
|
||||
become_user='tahoe-lafs')
|
||||
|
||||
|
||||
def get_introducers():
|
||||
"""Return a dictionary of all introducers and their furls added to the
|
||||
storage node running on this FreedomBox.
|
||||
"""
|
||||
introducers = actions.run_as_user('tahoe-lafs', ['get-introducers'],
|
||||
become_user='tahoe-lafs')
|
||||
|
||||
return json.loads(introducers)
|
||||
|
||||
|
||||
def get_local_introducer():
|
||||
"""Return the name and furl of the introducer created on this FreedomBox.
|
||||
"""
|
||||
introducer = actions.run_as_user('tahoe-lafs', ['get-local-introducer'],
|
||||
become_user='tahoe-lafs')
|
||||
|
||||
return json.loads(introducer)
|
||||
27
plinth/modules/tahoe/errors.py
Normal file
27
plinth/modules/tahoe/errors.py
Normal file
@ -0,0 +1,27 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Errors for Tahoe-LAFS module
|
||||
"""
|
||||
|
||||
from plinth.errors import PlinthError
|
||||
|
||||
|
||||
class TahoeConfigurationError(PlinthError):
|
||||
"""Tahoe-LAFS has not been configured for domain name."""
|
||||
pass
|
||||
110
plinth/modules/tahoe/templates/tahoe-post-setup.html
Normal file
110
plinth/modules/tahoe/templates/tahoe-post-setup.html
Normal file
@ -0,0 +1,110 @@
|
||||
{% extends "service.html" %}
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block description %}
|
||||
|
||||
{% for paragraph in description %}
|
||||
<p>{{ paragraph|safe }}</p>
|
||||
{% endfor %}
|
||||
|
||||
<p>
|
||||
{% url 'config:index' as index_url %}
|
||||
{% blocktrans trimmed with domain_name=domain_name %}
|
||||
The Tahoe-LAFS server domain is set to <b>{{ domain_name }}</b>.
|
||||
Changing the FreedomBox domain name needs a reinstall of
|
||||
Tahoe-LAFS and you WILL LOSE DATA. You can access Tahoe-LAFS at
|
||||
<a href="https://{{domain_name}}:5678">https://{{domain_name}}:5678</a>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block configuration %}
|
||||
<h3>{% trans "Configuration" %}</h3>
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% trans "Update setup" %}"/>
|
||||
</form>
|
||||
<br/>
|
||||
|
||||
<h4>{% trans "Local introducer" %}</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Pet Name" %}</th>
|
||||
<th> furl </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td> {{ local_introducer.0 }} </td>
|
||||
<td> {{ local_introducer.1 }} </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<form class="form" method="post" action="/plinth/apps/tahoe-lafs/add_introducer">
|
||||
{% csrf_token %}
|
||||
<h4>{% trans "Add new introducer" %}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{% trans "Pet Name" %}:</label>
|
||||
<input type="text" class="form-control" name="pet_name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>furl:</label>
|
||||
<textarea class="form-control" rows="5" name="furl"></textarea>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% trans "Add" %}"/>
|
||||
</form>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4>{% trans "Connected introducers" %}</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Pet Name" %}</th>
|
||||
<th> furl </th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for introducer, furl in introducers %}
|
||||
<tr>
|
||||
<td> {{ introducer }} </td>
|
||||
<td> {{ furl }} </td>
|
||||
<form class="form" method="post"
|
||||
action="/plinth/apps/tahoe-lafs/remove_introducer/{{introducer}}" id="introducer_list">
|
||||
{% csrf_token %}
|
||||
<td><button type="submit" class="btn btn-danger">
|
||||
{% trans "Remove" %}
|
||||
</button></td>
|
||||
</form>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
62
plinth/modules/tahoe/templates/tahoe-pre-setup.html
Normal file
62
plinth/modules/tahoe/templates/tahoe-pre-setup.html
Normal file
@ -0,0 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% block pagetitle %}
|
||||
<h2>{{ title }}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% for paragraph in description %}
|
||||
<p>{{ paragraph|safe }}</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
<p>
|
||||
{% url 'config:index' as index_url %}
|
||||
{% if domain_names|length == 0 %}
|
||||
No domain(s) are set. You can setup your domain on the system at
|
||||
<a href="{{ index_url }}">{% trans "Configure" %}</a> page.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% block status %}
|
||||
{% endblock %}
|
||||
|
||||
{% block diagnostics %}
|
||||
{% endblock %}
|
||||
|
||||
{% block configuration %}
|
||||
{% if domain_names|length > 0 %}
|
||||
<h3>{% trans Configuration %}</h3>
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary"
|
||||
value="{% trans "Update setup" %}"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
37
plinth/modules/tahoe/urls.py
Normal file
37
plinth/modules/tahoe/urls.py
Normal file
@ -0,0 +1,37 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
URLs for the Tahoe-LAFS module.
|
||||
"""
|
||||
|
||||
from . import views
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import TahoeSetupView, TahoeServiceView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^apps/tahoe-lafs/setup$', TahoeSetupView.as_view(),
|
||||
name='setup'),
|
||||
url(r'^apps/tahoe-lafs/add_introducer$', views.add_introducer,
|
||||
name="add-introducer"),
|
||||
url(r'^apps/tahoe-lafs/remove_introducer/(?P<introducer>[0-9a-zA-Z_]+)$',
|
||||
views.remove_introducer, name="remove-introducer"),
|
||||
url(r'^apps/tahoe-lafs/$', TahoeServiceView.as_view(),
|
||||
name='index')
|
||||
]
|
||||
83
plinth/modules/tahoe/views.py
Normal file
83
plinth/modules/tahoe/views.py
Normal file
@ -0,0 +1,83 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Views for the Tahoe-LAFS module
|
||||
"""
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import FormView
|
||||
|
||||
from plinth.forms import DomainSelectionForm
|
||||
from plinth.modules import tahoe
|
||||
from plinth.utils import get_domain_names
|
||||
from plinth.views import ServiceView
|
||||
|
||||
|
||||
class TahoeSetupView(FormView):
|
||||
"""Show tahoe-lafs setup page."""
|
||||
template_name = 'tahoe-pre-setup.html'
|
||||
form_class = DomainSelectionForm
|
||||
description = tahoe.description
|
||||
title = tahoe.title
|
||||
success_url = reverse_lazy('tahoe:index')
|
||||
|
||||
def form_valid(self, form):
|
||||
domain_name = form.cleaned_data['domain_name']
|
||||
tahoe.post_setup(domain_name)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['description'] = self.description
|
||||
context['title'] = self.title
|
||||
context['domain_names'] = get_domain_names()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class TahoeServiceView(ServiceView):
|
||||
"""Show tahoe-lafs service page."""
|
||||
service_id = tahoe.managed_services[0]
|
||||
template_name = 'tahoe-post-setup.html'
|
||||
description = tahoe.description
|
||||
diagnostics_module_name = 'tahoe'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not tahoe.is_setup():
|
||||
return redirect('tahoe:setup')
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['domain_name'] = tahoe.get_configured_domain_name()
|
||||
context['introducers'] = tahoe.get_introducers()
|
||||
context['local_introducer'] = tahoe.get_local_introducer()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def add_introducer(request):
|
||||
if request.method == 'POST':
|
||||
tahoe.add_introducer((request.POST['pet_name'], request.POST['furl']))
|
||||
return redirect('tahoe:index')
|
||||
|
||||
|
||||
def remove_introducer(request, introducer):
|
||||
if request.method == 'POST':
|
||||
tahoe.remove_introducer(introducer)
|
||||
return redirect('tahoe:index')
|
||||
@ -19,11 +19,14 @@
|
||||
Test module for Plinth's utilities.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock, MagicMock
|
||||
|
||||
from django.test import TestCase
|
||||
import ruamel.yaml
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from plinth.utils import YAMLFile
|
||||
from plinth.utils import is_user_admin
|
||||
|
||||
|
||||
@ -81,3 +84,44 @@ class TestIsAdminUser(TestCase):
|
||||
mock.assert_called_once_with()
|
||||
session_mock.__getitem__.assert_called_once_with(
|
||||
'cache_user_is_admin')
|
||||
|
||||
|
||||
class TestYAMLFileUtil(TestCase):
|
||||
"""Check updating YAML files"""
|
||||
|
||||
kv_pair = {'key': 'value'}
|
||||
|
||||
def test_update_empty_yaml_file(self):
|
||||
"""
|
||||
Update an empty YAML file with content.
|
||||
"""
|
||||
fp = tempfile.NamedTemporaryFile()
|
||||
conf = {'property1': self.kv_pair}
|
||||
|
||||
with YAMLFile(fp.name) as file_conf:
|
||||
for key in conf.keys():
|
||||
file_conf[key] = conf[key]
|
||||
|
||||
with open(fp.name, 'r') as retrieved_conf:
|
||||
assert retrieved_conf.read() == ruamel.yaml.round_trip_dump(conf)
|
||||
|
||||
def test_update_non_empty_yaml_file(self):
|
||||
"""
|
||||
Update a non-empty YAML file with modifications
|
||||
"""
|
||||
fp = tempfile.NamedTemporaryFile()
|
||||
|
||||
with open(fp.name, 'w') as conf_file:
|
||||
conf_file.write(
|
||||
ruamel.yaml.round_trip_dump({
|
||||
'property1': self.kv_pair
|
||||
}))
|
||||
|
||||
with YAMLFile(fp.name) as file_conf:
|
||||
file_conf['property2'] = self.kv_pair
|
||||
|
||||
with open(fp.name, 'r') as retrieved_conf:
|
||||
file_conf = ruamel.yaml.round_trip_load(retrieved_conf)
|
||||
print(file_conf)
|
||||
assert file_conf == {'property1': self.kv_pair,
|
||||
'property2': self.kv_pair}
|
||||
|
||||
@ -20,7 +20,10 @@ Miscellaneous utility methods.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import ruamel.yaml
|
||||
from django.utils.functional import lazy
|
||||
from plinth.modules import names
|
||||
|
||||
|
||||
def import_from_gi(library, version):
|
||||
@ -63,3 +66,49 @@ def is_user_admin(request, cached=False):
|
||||
user_is_admin = request.user.groups.filter(name='admin').exists()
|
||||
request.session['cache_user_is_admin'] = user_is_admin
|
||||
return user_is_admin
|
||||
|
||||
|
||||
def get_domain_names():
|
||||
"""Return the domain name(s)"""
|
||||
domain_names = []
|
||||
|
||||
for domain_type, domains in names.domains.items():
|
||||
if domain_type == 'hiddenservice':
|
||||
continue
|
||||
for domain in domains:
|
||||
domain_names.append((domain, domain))
|
||||
|
||||
return domain_names
|
||||
|
||||
|
||||
class YAMLFile(object):
|
||||
"""A context management class for updating YAML files"""
|
||||
def __init__(self, yaml_file, post_exit=None):
|
||||
"""Return a context object for the YAML file.
|
||||
|
||||
Parameters:
|
||||
yaml_file - the YAML file to update.
|
||||
post_exit - a function that will be called after updating the YAML file.
|
||||
"""
|
||||
self.yaml_file = yaml_file
|
||||
self.post_exit = post_exit
|
||||
self.conf = None
|
||||
|
||||
def __enter__(self):
|
||||
with open(self.yaml_file, 'r') as intro_conf:
|
||||
if not self.is_file_empty():
|
||||
self.conf = ruamel.yaml.round_trip_load(intro_conf)
|
||||
else:
|
||||
self.conf = {}
|
||||
|
||||
return self.conf
|
||||
|
||||
def __exit__(self, typ, value, traceback):
|
||||
with open(self.yaml_file, 'w') as intro_conf:
|
||||
ruamel.yaml.round_trip_dump(self.conf, intro_conf)
|
||||
|
||||
if self.post_exit:
|
||||
self.post_exit()
|
||||
|
||||
def is_file_empty(self):
|
||||
return os.stat(self.yaml_file).st_size == 0
|
||||
|
||||
BIN
static/themes/default/icons/tahoe-lafs.png
Normal file
BIN
static/themes/default/icons/tahoe-lafs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Loading…
x
Reference in New Issue
Block a user