Commit 7f3aad51 authored by Stavros Sachtouris's avatar Stavros Sachtouris Committed by Giorgos Korfiatis

Merge UI with backend, use backend in CLI

parent 1d2cf4f3
import cmd
import sys
import logging
from agkyra.syncer import setup, syncer
from agkyra.syncer.pithos_client import PithosFileClient
from agkyra.syncer.localfs_client import LocalfsFileClient
from agkyra import config
# from config import AgkyraConfig
LOG = logging.getLogger(__name__)
LOG.addHandler(logging.FileHandler('%s/agkyra.log' % config.AGKYRA_DIR))
LOG.setLevel(logging.CRITICAL)
SYNCER_LOG = logging.getLogger('agkyra.syncer')
SYNCER_LOG.addHandler(logging.FileHandler('%s/agkyra.log' % config.AGKYRA_DIR))
SYNCER_LOG.setLevel(logging.CRITICAL)
setup.GLOBAL_SETTINGS_NAME = config.AGKYRA_DIR
class AgkyraCLI(cmd.Cmd):
"""The CLI for """
cnf = config.AgkyraConfig()
is_shell = False
def init(self):
"""initialize syncer"""
# Read settings
sync = self.cnf.get('global', 'default_sync')
LOG.info('Using sync: %s' % sync)
cloud = self.cnf.get_sync(sync, 'cloud')
url = self.cnf.get_cloud(cloud, 'url')
token = self.cnf.get_cloud(cloud, 'token')
container = self.cnf.get_sync(sync, 'container')
directory = self.cnf.get_sync(sync, 'directory')
# Prepare syncer settings
self.settings = setup.SyncerSettings(
sync, url, token, container, directory)
LOG.info('Local: %s' % directory)
LOG.info('Remote: %s of %s' % (container, url))
# self.exclude = self.cnf.get_sync(sync, 'exclude')
# Init syncer
master = PithosFileClient(self.settings)
slave = LocalfsFileClient(self.settings)
self.syncer = syncer.FileSyncer(self.settings, master, slave)
def preloop(self):
"""This runs when the shell loads"""
print 'Loading Agkyra (sometimes this takes a while)'
if not self.is_shell:
self.is_shell = True
self.prompt = '\xe2\x9a\x93 '
self.init()
self.default('')
def print_option(self, section, name, option):
"""Print a configuration option"""
section = '%s.%s' % (section, name) if name else section
value = self.cnf.get(section, option)
print ' %s: %s' % (option, value)
def list_section(self, section, name):
"""list contents of a section"""
content = dict(self.cnf.items(section))
if section in 'global' and name:
self.print_option(section, '', name)
else:
if name:
content = content[name]
for option in content.keys():
self.print_option(section, name, option)
def list_section_type(self, section):
"""print the contents of a configuration section"""
names = ['', ] if section in ('global', ) else self.cnf.keys(section)
assert names, 'Section %s not found' % section
for name in names:
print section, name
self.list_section(section, name)
def list_sections(self):
"""List all configuration sections"""
for section in self.cnf.sections():
self.list_section_type(section)
def do_list(self, line):
"""List current settings (\"help list\" for details)
list global List all settings
list global <option> Get the value of this global option
list cloud List all clouds
list cloud <name> List all options of a cloud
list cloud <name> <option> Get the value of this cloud option
list sync List all syncs
list sync <name> List all options of a sync
list sync <name> <option> Get the value of this sync option
"""
args = line.split()
try:
{
0: self.list_sections,
1: self.list_section_type,
2: self.list_section,
3: self.print_option
}[len(args)](*args)
except Exception as e:
sys.stderr.write('%s\n' % e)
cmd.Cmd.do_help(self, 'list')
def set_global_setting(self, section, option, value):
assert section in ('global'), 'Syntax error'
self.cnf.set(section, option, value)
def set_setting(self, section, name, option, value):
assert section in self.sections(), 'Syntax error'
self.cnf.set('%s.%s' % (section, name), option, value)
def do_set(self, line):
"""Set a setting"""
args = line.split()
try:
{
3: self.set_global_setting,
4: self.set_setting
}[len(args)](*args)
self.cnf.write()
except Exception as e:
sys.stderr.write('%s\n' % e)
cmd.Cmd.do_help(self, 'set')
def do_start(self, line):
"""Start syncing"""
if not getattr(self, '_syncer_initialized', False):
self.syncer.probe_and_sync_all()
self._syncer_initialized = True
if self.syncer.paused:
self.syncer.start_decide()
def do_pause(self, line):
"""Pause syncing"""
if not self.syncer.paused:
self.syncer.pause_decide()
def do_status(self, line):
"""Get current status (running/paused, progress)"""
print 'paused' if self.syncer.paused else 'running'
# def do_shell(self, line):
# """Run system, shell commands"""
# if getattr(self, 'is_shell'):
# os.system(line)
# else:
# try:
# self.prompt = '\xe2\x9a\x93 '
# self.is_shell = True
# finally:
# self.init()
# self.cmdloop()
def do_help(self, line):
"""List commands with \"help\" or detailed help with \"help cmd\""""
if not line:
self.default(line)
cmd.Cmd.do_help(self, line)
def do_quit(self, line):
"""Quit Agkyra shell"""
return True
def default(self, line):
"""print help"""
sys.stderr.write('Usage:\t%s <command> [args]\n\n' % self.prompt)
for arg in [c for c in self.get_names() if c.startswith('do_')]:
sys.stderr.write('%s\t' % arg[3:])
method = getattr(self, arg)
sys.stderr.write(method.__doc__.split('\n')[0] + '\n')
sys.stderr.write('\n')
def emptyline(self):
if not self.is_shell:
return self.default('')
def run_onecmd(self, argv):
self.prompt = argv[0]
self.init()
self.onecmd(' '.join(argv[1:]))
# AgkyraCLI().run_onecmd(sys.argv)
# or run a shell with
# AgkyraCLI().cmdloop()
"""
A Sync is a triplete consisting of at least the following:
* a cloud (a reference to a cloud set in the same config file)
* a container
* a local directory
Other parameters may also be set in the context of a sync.
The sync is identified by the "sync_id", which is a string
The operations of a sync are similar to the operations of a cloud, as they are
implemented in kamaki.cli.config
"""
import os
from re import match
from ConfigParser import Error
from kamaki.cli import config
# import Config, CLOUD_PREFIX
from kamaki.cli.config import Config
from kamaki.cli.utils import escape_ctrl_chars
CLOUD_PREFIX = config.CLOUD_PREFIX
config.HEADER = '# Agkyra configuration file version XXX\n'
AGKYRA_DIR = os.environ.get('AGKYRA_DIR', os.path.expanduser('~/.agkyra'))
config.CONFIG_PATH = '%s%sconfig.rc' % (AGKYRA_DIR, os.path.sep)
# neutralize kamaki CONFIG_ENV for this session
config.CONFIG_ENV = ''
SYNC_PREFIX = 'sync'
config.DEFAULTS = {
'global': {
'agkyra_dir': AGKYRA_DIR,
},
CLOUD_PREFIX: {
# <cloud>: {
# 'url': '',
# 'token': '',
# whatever else may be useful in this context
# },
# ... more clouds
},
SYNC_PREFIX: {
# <sync>: {
# 'cloud': '',
# 'container': '',
# 'directory': ''
# },
# ... more syncs
},
}
class InvalidSyncNameError(Error):
"""A valid sync name must pass through this regex: ([~@#$:.-\w]+)"""
class AgkyraConfig(Config):
"""
Handle the config file for Agkyra, adding the notion of a sync
A Sync is a triplete consisting of at least the following:
* a cloud (a reference to a cloud set in the same config file)
* a container
* a local directory
Other parameters may also be set in the context of a sync.
The sync is identified by the sync id, which is a string
The operations of a sync are similar to the operations of a cloud, as they
are implemented in kamaki.cli.config
"""
def __init__(self, *args, **kwargs):
"""Enhance Config to read SYNC sections"""
Config.__init__(self, *args, **kwargs)
for section in self.sections():
r = self.sync_name(section)
if r:
for k, v in self.items(section):
self.set_sync(r, k, v)
self.remove_section(section)
@staticmethod
def sync_name(full_section_name):
"""Get the sync name if the section is a sync, None otherwise"""
if not full_section_name.startswith(SYNC_PREFIX + ' '):
return None
matcher = match(SYNC_PREFIX + ' "([~@#$.:\-\w]+)"', full_section_name)
if matcher:
return matcher.groups()[0]
else:
isn = full_section_name[len(SYNC_PREFIX) + 1:]
raise InvalidSyncNameError('Invalid Cloud Name %s' % isn)
def get(self, section, option):
"""Enhance Config.get to handle sync options"""
value = self._overrides.get(section, {}).get(option)
if value is not None:
return value
prefix = SYNC_PREFIX + '.'
if section.startswith(prefix):
return self.get_sync(section[len(prefix):], option)
return config.Config.get(self, section, option)
def set(self, section, option, value):
"""Enhance Config.set to handle sync options"""
self.assert_option(option)
prefix = SYNC_PREFIX + '.'
if section.startswith(prefix):
sync = self.sync_name(
'%s "%s"' % (SYNC_PREFIX, section[len(prefix):]))
return self.set_sync(sync, option, value)
return config.Config.set(self, section, option, value)
def get_sync(self, sync, option):
"""Get the option value from the given sync option
:raises KeyError: if the sync or the option do not exist
"""
r = self.get(SYNC_PREFIX, sync) if sync else None
if r:
return r[option]
raise KeyError('Sync "%s" does not exist' % sync)
def set_sync(self, sync, option, value):
"""Set the value of this option in the named sync.
If the sync or the option do not exist, create them.
"""
try:
d = self.get(SYNC_PREFIX, sync) or dict()
except KeyError:
d = dict()
self.assert_option(option)
d[option] = value
self.set(SYNC_PREFIX, sync, d)
def remove_from_sync(self, sync, option):
"""Remove a sync option"""
d = self.get(SYNC_PREFIX, sync)
if isinstance(d, dict):
d.pop(option)
def safe_to_print(self):
"""Enhance Config.safe_to_print to handle syncs"""
dump = Config.safe_to_print(self)
for r, d in self.items(SYNC_PREFIX, include_defaults=False):
dump += u'\n[%s "%s"]\n' % (SYNC_PREFIX, escape_ctrl_chars(r))
for k, v in d.items():
dump += u'%s = %s\n' % (
escape_ctrl_chars(k), escape_ctrl_chars(v))
return dump
# if __name__ == '__main__':
# cnf = AgkyraConfig()
# config.Config.pretty_print(cnf)
# cnf.set_sync('1', 'cloud', '~okeanos')
# print cnf.get_sync('1', 'container')
# cnf.set_sync('1', 'lala', 123)
# cnf.remove_from_sync('1', 'lala')
# cnf.write()
from wsgiref.simple_server import make_server
# from ws4py.websocket import EchoWebSocket
from agkyra.protocol import WebSocketProtocol
from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.client import WebSocketBaseClient
from tempfile import NamedTemporaryFile
import subprocess
import json
from os.path import abspath
from threading import Thread
from hashlib import sha1
import os
import logging
LOG = logging.getLogger(__name__)
class GUI(WebSocketBaseClient):
"""Launch the GUI when the helper server is ready"""
def __init__(self, addr, gui_exec_path, gui_id):
"""Initialize the GUI Launcher"""
super(GUI, self).__init__(addr)
self.addr = addr
self.gui_exec_path = gui_exec_path
self.gui_id = gui_id
self.start = self.connect
def run_gui(self):
"""Launch the GUI and keep it running, clean up afterwards.
If the GUI is terminated for some reason, the WebSocket is closed and
the temporary file with GUI settings is deleted.
In windows, the file must be closed before the GUI is launched.
"""
# NamedTemporaryFile creates a file accessible only to current user
LOG.debug('Create temporary file')
with NamedTemporaryFile(delete=False) as fp:
json.dump(dict(gui_id=self.gui_id, address=self.addr), fp)
# subprocess.call blocks the execution
LOG.debug('RUN: %s %s' % (self.gui_exec_path, fp.name))
subprocess.call([
'/home/saxtouri/node-webkit-v0.11.6-linux-x64/nw',
# self.gui_exec_path,
abspath('gui/gui.nw'),
fp.name,
'--data-path', abspath('~/.agkyra')])
LOG.debug('GUI process closed, remove temp file')
os.remove(fp.name)
def handshake_ok(self):
"""If handshake is OK, the helper is UP, so the GUI can be launched"""
self.run_gui()
LOG.debug('Close GUI wrapper connection')
self.close()
class HelperServer(object):
"""Agkyra Helper Server sets a WebSocket server with the Helper protocol
It also provided methods for running and killing the Helper server
:param gui_id: Only the GUI with this ID is allowed to chat with the Helper
"""
def __init__(self, port=0):
"""Setup the helper server"""
self.gui_id = sha1(os.urandom(128)).hexdigest()
WebSocketProtocol.gui_id = self.gui_id
server = make_server(
'', port,
server_class=WSGIServer,
handler_class=WebSocketWSGIRequestHandler,
app=WebSocketWSGIApplication(handler_cls=WebSocketProtocol))
server.initialize_websockets_manager()
self.server, self.port = server, server.server_port
def start(self):
"""Start the helper server in a thread"""
Thread(target=self.server.serve_forever).start()
def shutdown(self):
"""Shutdown the server (needs another thread) and join threads"""
t = Thread(target=self.server.shutdown)
t.start()
t.join()
def run(gui_exec_path):
"""Prepare helper and GUI and run them in the proper order"""
server = HelperServer()
addr = 'ws://localhost:%s' % server.port
gui = GUI(addr, gui_exec_path, server.gui_id)
LOG.info('Start helper server')
server.start()
try:
LOG.info('Start GUI')
gui.start()
except KeyboardInterrupt:
LOG.info('Shutdown GUI')
gui.close()
LOG.info('Shutdown helper server')
server.shutdown()
if __name__ == '__main__':
logging.basicConfig(filename='agkyra.log', level=logging.DEBUG)
run(abspath('gui/app'))
This diff is collapsed.
<!DOCTYPE html>
<head>
<title>About Agkyra</title>
<link rel="stylesheet" href="static/stylesheets/normalize.css" />
<link rel="stylesheet" href="static/stylesheets/main.css" />
<script src="static/js/jquery.js"></script>
<script src="static/js/common.js"></script>
<style>
.box {
margin: 0 auto;
width: 90%;
height: 256px;
overflow: auto;
background: #e0e0e0;
}
</style>
</head>
<body>
<div class="row home js-main">
<img src="static/images/logo.png" style="width: 64px; height: 64px;" />
<h3>Agkyra... it syncs</h3>
<p><b>Agkyra</b> is a minimal syncing client for Pithos+.<br/>It syncs a Pithos+ container with a local folder.</p>
<p class="disclaimer">Developed and supported by the Okeanos/Synnefo development team of GRNET<br/><b>contact: okeanos-dev@grnet.gr</b></p>
<h3 class="disclaimer">(C) 2015: Greek Research and Technology Network<br/>Licensed under:</h3>
<embed class="box" src="COPYRIGHT" />
</div>
<footer class="footer js-footer">
<nav class="row">
<p class="disclaimer">The project is co-financed by Greece and the European Union</p>
<ul class="logos">
<li id="eu">
<a href="#" title="European Union">European Union</a>
</li>
<li id="dg">
<a href="#" title="Digital Greece">Digital Greece</a>
</li>
<li id="nsrf">
<a href="#" title="NSRF">NSRF</a>
</li>
<li id="grnet">
<a href="#" title="GRNet">GRNet</a>
</li>
</ul>
</nav>
</footer>
</body>
</html>
This diff is collapsed.
<!DOCTYPE html>
<html>
<head><title>GUI for Agkyra Pithos+ Syncing Client</title></head>
<body>
<script src="protocol.js"></script>
<script src="settings.js"></script>
<script type="text/javascript">
// Setup GUI
var windows = {
"settings": null,
"about": null,
"index": gui.Window.get()
}
function closeWindows() {
for (win in windows) if (windows[win]) windows[win].close();
}
// GUI components
var tray = new gui.Tray({
// tooltip: 'Paused (0% synced)',
title: 'Agkyra syncs with Pithos+',
icon: 'images/tray.png'
});
var menu = new gui.Menu();
// Progress and Pause
var start_syncing = 'Start Syncing';
var start_icon = 'images/play.png';
var pause_syncing = 'Pause Syncing';
var paused = true;
progress_item = new gui.MenuItem({
// progress menu item
label: 'Initializing',
type: 'normal',
enabled: false
});
menu.append(progress_item);
menu.append(new gui.MenuItem({type: 'separator'}));
pause_item = new gui.MenuItem({
// pause menu item
icon: 'images/play_pause.png',
label: '',
type: 'normal',
click: function() {
if (paused) {post_start(socket);} else {post_pause(socket);}
}
});
pause_item.enabled = false;
menu.append(pause_item);
// Update progress
window.setInterval(function() {
var status = globals['status'];
var new_progress = progress_item.label;
var new_pause = pause_item.label;
var menu_modified = false;
if (status['paused'] !== null) {
switch(pause_item.label) {
case pause_syncing: if (status['paused']) {
// Update to "Paused - start syncing"
paused = true;
new_pause = start_syncing;
menu_modified = true;
} // else continue syncing
new_progress = status['progress'] + '%' + ' synced';
break;
case start_syncing: if (status['paused']) return;