diff --git a/agkyra/__init__.py b/agkyra/__init__.py index a82995de2c7cf195c736939cdf7aff1dd0de4d06..11d27f8c7c435c335afaa298cf06e2cc1b202e0c 100644 --- a/agkyra/__init__.py +++ b/agkyra/__init__.py @@ -1 +1 @@ - __version__ = '0.1' +__version__ = '0.1' diff --git a/agkyra/cli.py b/agkyra/cli.py index 6e6e0c7a88c42f61082571d4d8fccc3e5b4170e4..d8c04129fde28c48e89aa31469b260e22d330ff1 100644 --- a/agkyra/cli.py +++ b/agkyra/cli.py @@ -1,16 +1,20 @@ import cmd import sys import logging -import json -from titanic import setup, syncer -from titanic.pithos_client import PithosFileClient -from titanic.localfs_client import FilesystemFileClient -import config +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 -logging.basicConfig(filename='agkyra.log', level=logging.DEBUG) 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 @@ -41,11 +45,12 @@ class AgkyraCLI(cmd.Cmd): # Init syncer master = PithosFileClient(self.settings) - slave = FilesystemFileClient(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 ' @@ -128,14 +133,20 @@ class AgkyraCLI(cmd.Cmd): def do_start(self, line): """Start syncing""" - self.syncer.run() + 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 'I have no idea' + print 'paused' if self.syncer.paused else 'running' # def do_shell(self, line): # """Run system, shell commands""" @@ -181,4 +192,4 @@ class AgkyraCLI(cmd.Cmd): # AgkyraCLI().run_onecmd(sys.argv) # or run a shell with -AgkyraCLI().cmdloop() +# AgkyraCLI().cmdloop() diff --git a/agkyra/gui.py b/agkyra/gui.py index 5e0068bc0a193eb5ffcec41c818e29fdb41f92bc..25c6804ea7a6abfaae53e8df4d936e190f20a6b0 100644 --- a/agkyra/gui.py +++ b/agkyra/gui.py @@ -1,6 +1,6 @@ from wsgiref.simple_server import make_server # from ws4py.websocket import EchoWebSocket -from protocol import WebSocketProtocol +from agkyra.protocol import WebSocketProtocol from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication from ws4py.client import WebSocketBaseClient @@ -20,11 +20,10 @@ 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): + def __init__(self, addr, 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 @@ -39,11 +38,10 @@ class GUI(WebSocketBaseClient): 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)) + LOG.debug('RUN: %s' % (fp.name)) subprocess.call([ - '/home/saxtouri/node-webkit-v0.11.6-linux-x64/nw', - # self.gui_exec_path, - abspath('gui/gui.nw'), + abspath('agkyra/nwjs/nw'), + abspath('agkyra/gui.nw'), fp.name, '--data-path', abspath('~/.agkyra')]) LOG.debug('GUI process closed, remove temp file') @@ -85,11 +83,11 @@ class HelperServer(object): t.join() -def run(gui_exec_path): +def run(): """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) + gui = GUI(addr, server.gui_id) LOG.info('Start helper server') server.start() diff --git a/agkyra/gui/menu.html b/agkyra/gui/menu.html index 7ba6e4f50e46a70ec5870b37a55a64c05eb61a50..29c67d1643e0c664255e4b8817db2639fc78fd6a 100644 --- a/agkyra/gui/menu.html +++ b/agkyra/gui/menu.html @@ -4,6 +4,7 @@ <body> <script src="protocol.js"></script> <script src="settings.js"></script> + <script src="static/js/jquery.js"></script> <script type="text/javascript"> // Setup GUI @@ -42,7 +43,7 @@ menu.append(new gui.MenuItem({type: 'separator'})); pause_item = new gui.MenuItem({ // pause menu item icon: 'images/play_pause.png', - label: '', + label: 'NOT READY', type: 'normal', click: function() { if (paused) {post_start(socket);} else {post_pause(socket);} @@ -51,84 +52,42 @@ pause_item = new gui.MenuItem({ 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; - // else update to "Syncing - pause syncing" - paused = false; - new_pause = pause_syncing; - new_progress = status['progress'] + '%' + ' synced'; - menu_modified = true; - break; - default: - if (status['paused']) {new_pause = start_syncing; paused=true;} - else {new_pause = pause_syncing; paused=false;} - new_progress = status['progress'] + '%' + ' synced'; - pause_item.enabled = true; - menu_modified = true; - } - } - if (new_pause != pause_item.label) { - pause_item.label = new_pause; - menu_modified = true; - } - if (new_progress != progress_item.label) { - progress_item.label = new_progress; - menu_modified = true; - } - if (menu_modified) { - if (paused) progress_item.label += ' - paused'; - tray.menu = menu; - } - get_status(socket); -}, 1500); - // Menu actions contents -menu.append(new gui.MenuItem({ +var local_folder_menu = new gui.MenuItem({ label: 'Open local folder', icon: 'images/folder.png', + enabled: false, click: function () { var dir = globals['settings']['directory']; console.log('Open ' + dir); gui.Shell.showItemInFolder(dir); } -})); +}) +menu.append(local_folder_menu); -menu.append(new gui.MenuItem({ +var pithos_page_menu = new gui.MenuItem({ label: 'Launch Pithos+ page', icon: 'images/pithos.png', + enabled: false, click: function () { - var pithos_ui = globals['settings']['pithos_ui']; - console.log('Visit ' + pithos_ui); - gui.Shell.openExternal(pithos_ui); + console.log('Visit ' + get_pithos_ui()); + gui.Shell.openExternal(get_pithos_ui()); } -})); +}); +menu.append(pithos_page_menu); // Settings and About menu.append(new gui.MenuItem({type: 'separator'})); -menu.append(new gui.MenuItem({ +var settings_menu = new gui.MenuItem({ label: 'Settings', icon: 'images/settings.png', + enabled: false, click: function () { export_settings(globals.settings); if (windows['settings']) windows['settings'].close(); windows['settings'] = gui.Window.open("settings.html", { toolbar: false, focus: true, - width: 841, height: 520, + width: 841, height: 520 }); windows['settings'].on('closed', function() { new_settings = import_settings(); @@ -137,7 +96,8 @@ menu.append(new gui.MenuItem({ get_settings(socket); }); }, -})); +}); +menu.append(settings_menu); menu.append(new gui.MenuItem({ label: 'About', @@ -159,6 +119,79 @@ menu.append(new gui.MenuItem({ click: function() {post_shutdown(socket);} })); + +// Update progress +var client_ready = false; +window.setInterval(function() { + var menu_modified = false; + if (!client_ready) { + if (!globals.authenticated) return; + client_ready = true; + } + + if (client_ready) { + if (!settings_menu.enabled) { + if (globals.settings.url) refresh_endpoints(globals.settings.url); + settings_menu.enabled = true; + tray.menu = menu; + } + if (globals.settings.url && !pithos_page_menu.enabled) { + if (get_pithos_ui() != null) { + pithos_page_menu.enabled = true; + tray.menu = menu; + } else { refresh_endpoints(globals.settings.url); } + } + if (!local_folder_menu.enabled) { + if (globals.settings.directory) { + local_folder_menu.enabled = true; + tray.menu = menu; + } + } + } + + var status = globals['status']; + var new_progress = progress_item.label; + var new_pause = pause_item.label; + 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; + // else update to "Syncing - pause syncing" + paused = false; + new_pause = pause_syncing; + new_progress = status['progress'] + '%' + ' synced'; + menu_modified = true; + break; + default: + if (status['paused']) {new_pause = start_syncing; paused=true;} + else {new_pause = pause_syncing; paused=false;} + new_progress = status['progress'] + '%' + ' synced'; + pause_item.enabled = true; + menu_modified = true; + } + } + if (new_pause != pause_item.label) { + pause_item.label = new_pause; + menu_modified = true; + } + if (new_progress != progress_item.label) { + progress_item.label = new_progress; + menu_modified = true; + } + if (menu_modified) { + if (paused) progress_item.label += ' - paused'; + tray.menu = menu; + } + get_status(socket); +}, 1500); + tray.menu = menu; </script> diff --git a/agkyra/gui/protocol.js b/agkyra/gui/protocol.js index 44f79a25ee36993c2c7dafd63a513e66152aac23..58bce87961be826dd0af836d6f2eade9ed1ef925 100644 --- a/agkyra/gui/protocol.js +++ b/agkyra/gui/protocol.js @@ -16,10 +16,10 @@ var globals = { 'url': null, 'container': null, 'directory': null, - 'pithos_ui': null, 'exclude': null }, - 'status': {"progress": null, "paused": null} + 'status': {"progress": null, "paused": null}, + 'authenticated': false, } // Protocol: requests ::: responses @@ -65,12 +65,13 @@ socket.onopen = function() { } socket.onmessage = function(e) { var r = JSON.parse(e.data) - //console.log('RECV: ' + r['action']) + console.log('RECV: ' + r['action']) switch(r['action']) { case 'post gui_id': if (r['ACCEPTED'] === 202) { get_settings(this); get_status(this); + globals.authenticated = true; } else { console.log('Helper: ' + JSON.stringify(r)); closeWindows(); @@ -85,7 +86,7 @@ socket.onmessage = function(e) { } break; case 'get settings': - console.log(r); + // console.log(r); globals['settings'] = r; break; case 'put settings': diff --git a/agkyra/gui/settings.html b/agkyra/gui/settings.html index adf0661b388360d5984a8dcf5dcaff6c552af405..85596c660d46ed60866c4d989e79f3db194755bc 100644 --- a/agkyra/gui/settings.html +++ b/agkyra/gui/settings.html @@ -9,6 +9,7 @@ <script type="text/javascript"> var fs = require('fs'); var exclude = null; + remove_tokens = null; $(document).ready(function() { var url = get_setting('url'); if (url) $('#cloud-url').val(url); @@ -18,16 +19,11 @@ if (container) $('#container').val(container); var directory = get_setting('directory'); if (directory) $('#directory').html(directory); - exclude = get_setting('exclude'); - if (exclude) $('#exclude').val( - fs.readFileSync(exclude, encoding='utf-8')); - var pithos_ui = get_setting('pithos_ui'); - if (pithos_ui) { - $('#get_creds').show(); - } else { - console.log('No pithos view, remove credential button'); - $('#get_creds').hide(); - } + var exclude = get_setting('exclude'); + if (exclude) try { + $('#exclude').val( + fs.readFileSync(exclude, encoding='utf-8')); + } catch (err) {console.log(err);} }); function update_exclude(new_content) { @@ -45,30 +41,79 @@ //set_setting('token', $(this).val()) } + function remove_cookies(win, url) { + var removed_at_least_one = false + win.cookies.getAll({url: url}, function(cookies) { + $.each(cookies, function(i, cookie) { + console.log('I have a cookie to remove ' + cookie.name + ', ' + cookie.domain); + win.cookies.remove({url: url, name: cookie.name} ); + removed_at_least_one = true; + }); + }); + return removed_at_least_one; + } + var gui = require('nw.gui'); - function extract_cookie(url) { + var cred_win = null; + var logout_win = null; + var can_close_cred = false; + var got_cookie = false; + var can_close_logout = false; + var show_creds = true; + function get_credentials() { var cookie_name = '_pithos2_a'; - var logout_url = 'https://accounts.okeanos.grnet.gr/ui/logout' - var w = gui.Window.open(logout_url, { - focus: false, width: 20, height: 20 - }); - w.close(); - var w = gui.Window.open(logout_url, { - focus: true, width: 520, height: 920 + var lurl = get_account_ui() + '/logout?next=' + get_pithos_ui() + + show_creds = false; + $('#get_creds').hide(); + + got_cookie = false; + cred_win = gui.Window.open(lurl, { + focus: true, width: 820, height: 580, toolbar: false }); - w.cookies.onChanged.addListener(function(info) { + + cred_win.cookies.onChanged.addListener(function(info) { if (info.cookie.name === cookie_name) { console.log('Succesfully logged in'); extract_credentials(info.cookie); - w.close(); + got_cookie = true; } }); - } + cred_win.on('loaded', function() { + if (got_cookie) can_close_cred = true; + }); - function get_credentials() { - var pithos_ui = get_setting('pithos_ui'); - extract_cookie(pithos_ui); + cred_win.on('closed', function() { + logout_win = gui.Window.open( + get_account_ui() + '/logout', + {focus: true, width:20, height: 20 }); + logout_win.hide(); + logout_win.on('loaded', function() { + while( + remove_cookies(logout_win, get_account_ui()) || + remove_cookies(logout_win, get_pithos_ui())) {} + can_close_logout = true; + }); + }); } + + window.setInterval(function() { + // Refresh get_creds visibility, until refresh_endpoints + // changes are in effect + if (get_pithos_ui() && show_creds) { + $('#get_creds').show(); + } else {$('#get_creds').hide(); } + // Garbage collector + if (can_close_logout) { + can_close_logout = false; + logout_win.close(); + show_creds = true; + } + else if (can_close_cred) { + can_close_cred = false; + cred_win.close(); + } + }, 500); </script> </head> <body> @@ -88,7 +133,7 @@ </div> <div class="small-9 columns"> <input type="text" id="cloud-url" placeholder="Authentication URL" - onchange="set_setting('url', $(this).val())"> + onchange="set_setting('url', $(this).val()); refresh_endpoints($(this).val());"> <small class="error" style="visibility: hidden">Invalid entry</small> </div> </div> @@ -107,7 +152,7 @@ <div class="clearfix"> <a id="get_creds" class="button right" style="display: none;" - onclick="get_credentials();">Get credentials</a> + onclick="get_credentials();">Login to get credentials</a> </div> </fieldset> diff --git a/agkyra/gui/settings.js b/agkyra/gui/settings.js index 0df6d76633f5de0bff08bf8621ef9166344800f5..a3d1e91b6fe8c1f0acea8a2e056663ffe177d630 100644 --- a/agkyra/gui/settings.js +++ b/agkyra/gui/settings.js @@ -17,3 +17,33 @@ function get_setting(key) { function set_setting(key, val) { global.settings[key] = val; } + +function refresh_endpoints(identity_url) { + $.post(identity_url + '/tokens', function(data) { + var endpoints = data.access.serviceCatalog + global.pithos_ui = null; + global.account_ui = null; + $.each(endpoints, function(i, endpoint) { + switch(endpoint.type) { + case 'object-store': try { + global.pithos_ui = endpoint['endpoints'][0]['SNF:uiURL']; + } catch(err) { console.log('Failed to get pithos_ui ' + err); } + break; + case 'account': try { + global.account_ui = endpoint['endpoints'][0]['SNF:uiURL']; + } catch(err) { console.log('Failed to get account_ui ' + err); } + break; + } + }); + }); +} + +function get_pithos_ui() { + if (global.pithos_ui) {return global.pithos_ui;} + else {return null;} +} + +function get_account_ui() { + if (global.account_ui) {return global.account_ui;} + else {return null;} +} diff --git a/agkyra/protocol.py b/agkyra/protocol.py index 8ed1f2089321b0d5e13bbcb0eb746ef2ee1b6734..1641bd30d13ca7b0713d3e5fa54205d2a61923ec 100644 --- a/agkyra/protocol.py +++ b/agkyra/protocol.py @@ -2,10 +2,8 @@ from ws4py.websocket import WebSocket import json import logging from os.path import abspath -from titanic import syncer -from config import AgkyraConfig -from kamaki.clients.astakos import AstakosClient -from kamaki.clients.pithos import PithosClient +from agkyra.syncer import syncer +from agkyra.config import AgkyraConfig LOG = logging.getLogger(__name__) @@ -72,33 +70,30 @@ class WebSocketProtocol(WebSocket): cnf = AgkyraConfig() def _load_settings(self): + LOG.debug('Start loading settings') sync = self.cnf.get('global', 'default_sync') cloud = self.cnf.get_sync(sync, 'cloud') - url = self.cnf.get_cloud(cloud, 'url') - token = self.cnf.get_cloud(cloud, 'token') - - astakos = AstakosClient(url, token) - self.settings['url'], self.settings['token'] = url, token - try: - endpoints = astakos.get_endpoints()['access']['serviceCatalog'] - for endpoint in endpoints: - if endpoint['type'] == PithosClient.service_type: - pithos_ui = endpoint['endpoints'][0]['SNF:uiURL'] - self.settings['pithos_ui'] = pithos_ui - break - except Exception as e: - LOG.debug('Failed to retrieve pithos_ui: %s' % e) + self.settings['url'] = self.cnf.get_cloud(cloud, 'url') + except Exception: + self.settings['url'] = None + try: + self.settings['token'] = self.cnf.get_cloud(cloud, 'token') + except Exception: + self.settings['url'] = None for option in ('container', 'directory', 'exclude'): self.settings[option] = self.cnf.get_sync(sync, option) + LOG.debug('Finished loading settings') + def _dump_settings(self): + LOG.debug('Saving settings') sync = self.cnf.get('global', 'default_sync') cloud = self.cnf.get_sync(sync, 'cloud') - old_url = self.cnf.get_cloud(cloud, 'url') + old_url = self.cnf.get_cloud(cloud, 'url') or '' while old_url != self.settings['url']: cloud = '%s_%s' % (cloud, sync) try: @@ -113,6 +108,9 @@ class WebSocketProtocol(WebSocket): for option in ('directory', 'container', 'exclude'): self.cnf.set_sync(sync, option, self.settings[option]) + self.cnf.write() + LOG.debug('Settings saved') + # Syncer-related methods def get_status(self): from random import randint @@ -121,7 +119,6 @@ class WebSocketProtocol(WebSocket): return self.status def get_settings(self): - self._load_settings() return self.settings def set_settings(self, new_settings): @@ -137,7 +134,6 @@ class WebSocketProtocol(WebSocket): # WebSocket connection methods def opened(self): LOG.debug('Helper: connection established') - self._load_settings() def closed(self, *args): LOG.debug('Helper: connection closed') @@ -149,7 +145,6 @@ class WebSocketProtocol(WebSocket): # Protocol handling methods def _post(self, r): """Handle POST requests""" - LOG.debug('CALLED with %s' % r) if self.accepted: action = r['path'] if action == 'shutdown': @@ -161,6 +156,7 @@ class WebSocketProtocol(WebSocket): }[action]() self.send_json({'OK': 200, 'action': 'post %s' % action}) elif r['gui_id'] == self.gui_id: + self._load_settings() self.accepted = True self.send_json({'ACCEPTED': 202, 'action': 'post gui_id'}) else: @@ -170,16 +166,16 @@ class WebSocketProtocol(WebSocket): def _put(self, r): """Handle PUT requests""" - if not self.accepted: - action = r['path'] - self.send_json({'UNAUTHORIZED': 401, 'action': 'put %s' % action}) - self.terminate() - else: + if self.accepted: LOG.debug('put %s' % r) action = r.pop('path') self.set_settings(r) r.update({'CREATED': 201, 'action': 'put %s' % action}) self.send_json(r) + else: + action = r['path'] + self.send_json({'UNAUTHORIZED': 401, 'action': 'put %s' % action}) + self.terminate() def _get(self, r): """Handle GET requests"""