class SyncDaemon(object): """Interface to Ubuntu One's SyncDaemon.""" def __init__(self, dbus_class=DBusInterface): logger.info("SyncDaemon interface started!") # set up dbus and related stuff self.dbus = dbus_class(self) # attributes for GUI, definition and filling self.current_state = State() self.folders = None self.shares_to_me = None self.shares_to_others = None self.public_files = None self.queue_content = QueueContent(home=user.home) # callbacks for GUI to hook in self.status_changed_callback = NO_OP self.on_started_callback = NO_OP self.on_stopped_callback = NO_OP self.on_connected_callback = NO_OP self.on_disconnected_callback = NO_OP self.on_online_callback = NO_OP self.on_offline_callback = NO_OP self.on_folders_changed_callback = NO_OP self.on_shares_to_me_changed_callback = NO_OP self.on_shares_to_others_changed_callback = NO_OP self.on_public_files_changed_callback = NO_OP self.on_metadata_ready_callback = mandatory_callback( 'on_metadata_ready_callback') self.on_initial_data_ready_callback = NO_OP self.on_initial_online_data_ready_callback = NO_OP self.on_share_op_error_callback = mandatory_callback( 'on_share_op_error_callback') self.on_folder_op_error_callback = mandatory_callback( 'on_folder_op_error_callback') self.on_public_op_error_callback = mandatory_callback( 'on_public_op_error_callback') self.on_node_ops_changed_callback = NO_OP self.on_internal_ops_changed_callback = NO_OP self.on_transfers_callback = NO_OP # poller self.transfers_poller = Poller(TRANSFER_POLL_INTERVAL, self.get_current_transfers) self._check_started() @defer.inlineCallbacks def _check_started(self): """Check if started and load initial data if yes.""" # load initial data if ubuntuone-client already started started = yield self.dbus.is_sd_started() if started: self.current_state.set(is_started=True) self._get_initial_data() else: self.current_state.set(is_started=False) def shutdown(self): """Shut down the SyncDaemon.""" logger.info("SyncDaemon interface going down") self.transfers_poller.run(False) self.dbus.shutdown() @defer.inlineCallbacks def get_current_transfers(self): """Get downloads and uploads.""" uploads = yield self.dbus.get_current_uploads() downloads = yield self.dbus.get_current_downloads() self.on_transfers_callback(uploads + downloads) @defer.inlineCallbacks def _get_initial_data(self): """Get the initial SD data.""" logger.info("Getting offline initial data") status_data = yield self.dbus.get_status() self._send_status_changed(*status_data) # queue content stuff shares_real_dir = yield self.dbus.get_real_shares_dir() shares_link_dir = yield self.dbus.get_link_shares_dir() self.queue_content.set_shares_dirs(shares_link_dir, shares_real_dir) content = yield self.dbus.get_queue_content() self.queue_content.set_content(content) self.transfers_poller.run(self.queue_content.transferring) self.folders = yield self.dbus.get_folders() self.shares_to_me = yield self.dbus.get_shares_to_me() self.shares_to_others = yield self.dbus.get_shares_to_others() # let frontend know that we have all the initial offline data logger.info("All initial offline data is ready") self.on_initial_data_ready_callback() logger.info("Getting online initial data") self.public_files = yield self.dbus.get_public_files() # let frontend know that we have all the initial online data logger.info("All initial online data is ready") self.on_initial_online_data_ready_callback() @defer.inlineCallbacks def on_sd_public_files_changed(self, pf=None, is_public=False): """Update the Public Files list.""" data = yield self.dbus.get_public_files() logger.info("Got new Public Files list (%d items)", len(data)) self.public_files = data self.on_public_files_changed_callback(self.public_files) @defer.inlineCallbacks def on_sd_shares_changed(self): """Shares changed, ask for new information.""" logger.info("SD Shares changed") # to me new_to_me = yield self.dbus.get_shares_to_me() if new_to_me != self.shares_to_me: self.shares_to_me = new_to_me self.on_shares_to_me_changed_callback(new_to_me) # to others new_to_others = yield self.dbus.get_shares_to_others() if new_to_others != self.shares_to_others: self.shares_to_others = new_to_others self.on_shares_to_others_changed_callback(new_to_others) @defer.inlineCallbacks def on_sd_folders_changed(self): """Folders changed, ask for new information.""" logger.info("SD Folders changed") self.folders = yield self.dbus.get_folders() self.on_folders_changed_callback(self.folders) def on_sd_status_changed(self, *status_data): """The Status of SD changed..""" logger.info("SD Status changed") self._send_status_changed(*status_data) def _send_status_changed(self, name, description, is_error, is_connected, is_online, queues, connection): """Send status changed signal.""" kwargs = dict(name=name, description=description, is_error=is_error, is_connected=is_connected, is_online=is_online, queues=queues, connection=connection) # check status changes to call other callbacks if is_connected and not self.current_state.is_connected: self.on_connected_callback() if not is_connected and self.current_state.is_connected: self.on_disconnected_callback() if is_online and not self.current_state.is_online: self.on_online_callback() if not is_online and self.current_state.is_online: self.on_offline_callback() # state of SD if name == "SHUTDOWN": state = STATE_STOPPED self.current_state.set(is_started=False) self.on_stopped_callback() elif name in ('READY', 'WAITING'): if connection == 'With User With Network': state = STATE_CONNECTING else: state = STATE_DISCONNECTED elif name == 'STANDOFF': state = STATE_DISCONNECTED else: if is_connected: if name == "QUEUE_MANAGER": if queues == "IDLE": state = STATE_IDLE else: state = STATE_WORKING else: state = STATE_CONNECTING else: state = STATE_STARTING # check if it's the first time we flag starting if self.current_state.state != STATE_STARTING: self.current_state.set(is_started=True) self.on_started_callback() self._get_initial_data() kwargs['state'] = state xs = sorted(kwargs.iteritems()) logger.debug(" new status: %s", ', '.join('%s=%r' % i for i in xs)) # set current state to new values and call status changed cb self.current_state.set(**kwargs) self.status_changed_callback(**kwargs) def on_sd_queue_added(self, op_name, op_id, op_data): """A command was added to the Request Queue.""" logger.info("Queue content: added %r [%s] %s", op_name, op_id, op_data) r = self.queue_content.add(op_name, op_id, op_data) if r == NODE_OP: self.on_node_ops_changed_callback(self.queue_content.node_ops) elif r == INTERNAL_OP: self.on_internal_ops_changed_callback( self.queue_content.internal_ops) self.transfers_poller.run(self.queue_content.transferring) def on_sd_queue_removed(self, op_name, op_id, op_data): """A command was removed from the Request Queue.""" logger.info("Queue content: removed %r [%s] %s", op_name, op_id, op_data) r = self.queue_content.remove(op_name, op_id, op_data) if r == NODE_OP: self.on_node_ops_changed_callback(self.queue_content.node_ops) elif r == INTERNAL_OP: self.on_internal_ops_changed_callback( self.queue_content.internal_ops) self.transfers_poller.run(self.queue_content.transferring) def start(self): """Start the SyncDaemon.""" logger.info("Starting u1.SD") d = self.dbus.start() self._get_initial_data() return d def quit(self): """Stop the SyncDaemon and makes it quit.""" logger.info("Stopping u1.SD") return self.dbus.quit() def connect(self): """Tell the SyncDaemon that the user wants it to connect.""" logger.info("Telling u1.SD to connect") return self.dbus.connect() def disconnect(self): """Tell the SyncDaemon that the user wants it to disconnect.""" logger.info("Telling u1.SD to disconnect") return self.dbus.disconnect() @defer.inlineCallbacks def get_metadata(self, path): """Get the metadata for given path.""" resp = yield self.dbus.get_metadata(os.path.realpath(path)) if resp == NOT_SYNCHED_PATH: self.on_metadata_ready_callback(path, resp) return # have data! store it in raw, and process some result = dict(raw_result=resp) # stat if resp['stat'] == u'None': stat = None else: items = re.match(".*\((.*)\)", resp['stat']).groups()[0] items = [x.split("=") for x in items.split(", ")] stat = dict((a, int(b[:-1] if b[-1] == 'L' else b)) for a, b in items) result['stat'] = stat # changed is_partial = resp['info_is_partial'] != u'False' if resp['local_hash'] == resp['server_hash']: if is_partial: logger.warning("Bad 'changed' values: %r", resp) changed = None else: changed = CHANGED_NONE else: if is_partial: changed = CHANGED_SERVER else: changed = CHANGED_LOCAL result['changed'] = changed # path processed_path = resp['path'] if processed_path.startswith(user.home): processed_path = "~" + processed_path[len(user.home):] result['path'] = processed_path self.on_metadata_ready_callback(path, result) @defer.inlineCallbacks def get_free_space(self, volume_id): """Get the free space for a volume.""" free_space = yield self.dbus.get_free_space(volume_id) defer.returnValue(int(free_space)) def _answer_share(self, share_id, method, action_name): """Effectively accept or reject a share.""" def error(failure): """Operation failed.""" if failure.check(ShareOperationError): error = failure.value.error logger.info("%s share %s finished with error: %s", action_name, share_id, error) self.on_share_op_error_callback(share_id, error) else: logger.error("Unexpected error when %s share %s: %s %s", action_name.lower(), share_id, failure.type, failure.value) def success(_): """Operation finished ok.""" logger.info("%s share %s finished successfully", action_name, share_id) logger.info("%s share %s started", action_name, share_id) d = method(share_id) d.addCallbacks(success, error) def accept_share(self, share_id): """Accept a share.""" self._answer_share(share_id, self.dbus.accept_share, "Accepting") def reject_share(self, share_id): """Reject a share.""" self._answer_share(share_id, self.dbus.reject_share, "Rejecting") def subscribe_share(self, share_id): """Subscribe a share.""" self._answer_share(share_id, self.dbus.subscribe_share, "Subscribing") def unsubscribe_share(self, share_id): """Unsubscribe a share.""" self._answer_share(share_id, self.dbus.unsubscribe_share, "Unsubscribing") def send_share_invitation(self, path, mail_address, sh_name, access_level): """Send a share invitation.""" def error(failure): """Operation failed.""" if failure.check(ShareOperationError): error = failure.value.error logger.info("Sending share invitation finished with error %s " "(path=%r mail_address=%s share_name=%r " "access_level=%s)", error, path, mail_address, sh_name, access_level) else: logger.error("Unexpected error when sending share invitation " "%s %s (path=%r mail_address=%s share_name=%r " "access_level=%s)", failure.type, failure.value, path, mail_address, sh_name, access_level) def success(_): """Operation finished ok.""" logger.info("Sending share invitation finished successfully " "(path=%r mail_address=%s share_name=%r " "access_level=%s", path, mail_address, sh_name, access_level) logger.info("Sending share invitation: path=%r mail_address=%s " "share_name=%r access_level=%s", path, mail_address, sh_name, access_level) d = self.dbus.send_share_invitation(path, mail_address, sh_name, access_level) d.addCallbacks(success, error) @defer.inlineCallbacks def _folder_operation(self, operation, value, op_name): """Generic folder operation.""" try: result = yield operation(value) except FolderOperationError, e: logger.info("%s folder (on %r) finished with error: %s", op_name, value, e) self.on_folder_op_error_callback(e) else:
class DeliverNodeDataTestCase(unittest.TestCase): """Send the node data without the home.""" def setUp(self): """Set up the test.""" self.qc = QueueContent(home='/a/b') self.qc.set_shares_dirs('/a/b/link', '/a/b/real') def test_share_link_inside_home(self): """Assure the share link is inside home.""" self.assertRaises(ValueError, self.qc.set_shares_dirs, share_link='/a/k', share_real='/a/b/r') def test_share_real_inside_home(self): """Assure the share real is inside home.""" self.assertRaises(ValueError, self.qc.set_shares_dirs, share_link='/a/b/r', share_real='/a/k') def test_none(self): """Test getting with nothing.""" self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME) self.assertEqual(self.qc.node_ops[0][1], {}) def test_one_home(self): """Test getting with one in home.""" self.qc.add('MakeFile', '67', {'path': '/a/b/foo/fighters'}) self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME) node = self.qc.node_ops[0][1]['foo'] self.assertEqual(node.last_modified, None) self.assertEqual(node.kind, KIND_DIR) self.assertEqual(node.operations, []) self.assertEqual(node.done, None) self.assertEqual(len(node.children), 1) def test_two_home(self): """Test getting with two in home.""" self.qc.set_content([('MakeFile', '67', {'path': '/a/b/foo/fighters'})]) self.qc.set_content([('MakeFile', '99', {'path': '/a/b/bar'})]) self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME) self.assertEqual(len(self.qc.node_ops[0][1]), 2) # first node node = self.qc.node_ops[0][1]['foo'] self.assertEqual(node.last_modified, None) self.assertEqual(node.kind, KIND_DIR) self.assertEqual(node.operations, []) self.assertEqual(node.done, None) self.assertEqual(len(node.children), 1) # second node node = self.qc.node_ops[0][1]['bar'] self.assertTrue(isinstance(node.last_modified, float)) self.assertEqual(node.kind, KIND_FILE) expected = [('99', 'MakeFile', {'path': '/a/b/bar', '__done__': False})] self.assertEqual(node.operations, expected) self.assertEqual(node.done, False) self.assertEqual(len(node.children), 0) def test_one_share(self): """Test getting with one share.""" self.qc.set_content([('MakeFile', '12', {'path': '/a/b/real/foo'})]) self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME) node = self.qc.node_ops[0][1]['link'] self.assertEqual(node.last_modified, None) self.assertEqual(node.kind, KIND_DIR) self.assertEqual(node.operations, []) self.assertEqual(node.done, None) self.assertEqual(len(node.children), 1) node = node.children['foo'] self.assertTrue(isinstance(node.last_modified, float)) self.assertEqual(node.kind, KIND_FILE) expected = [('12', 'MakeFile', {'path': '/a/b/real/foo', '__done__': False})] self.assertEqual(node.operations, expected) self.assertEqual(node.done, False) self.assertEqual(len(node.children), 0) def test_several_mixed(self): """Test mixing two nodes in the same share, other share, and home.""" self.qc.set_content([('MakeDir', '0', {'path': '/a/b/j'})]) self.qc.set_content([('MakeDir', '1', {'path': '/a/b/real/foo/bar1'})]) self.qc.set_content([('MakeDir', '2', {'path': '/a/b/real/foo/bar2'})]) self.qc.set_content([('MakeDir', '3', {'path': '/a/b/real/othr/baz'})]) self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME) self.assertIn('j', self.qc.node_ops[0][1]) link_dir = self.qc.node_ops[0][1]['link'] self.assertIn('othr', link_dir.children) foo_dir = link_dir.children['foo'] self.assertIn('bar1', foo_dir.children) self.assertIn('bar2', foo_dir.children)