def setUp(self): appdir = 'nxdrive/tests/resources/esky_app' version_finder = 'nxdrive/tests/resources/esky_versions' self.esky_app = MockEsky(appdir, version_finder=version_finder) self.manager = MockManager() self.updater = AppUpdater(self.manager, esky_app=self.esky_app, local_update_site=True)
def _create_updater(self, update_check_delay): # Enable the capacity to extend the AppUpdater self._app_updater = AppUpdater( self, version_finder=self.get_version_finder(), check_interval=update_check_delay) self.started.connect(self._app_updater._thread.start) return self._app_updater
def setUp(self): location = dirname(__file__) appdir = location + '/resources/esky_app' version_finder = location + '/resources/esky_versions' self.esky_app = MockEsky(appdir, version_finder=version_finder) self.manager = MockManager() self.updater = AppUpdater(self.manager, esky_app=self.esky_app, local_update_site=True)
def _create_updater(self, update_check_delay): if (update_check_delay == 0): log.info("Update check delay is 0, disabling autoupdate") self._app_updater = FakeUpdater() return self._app_updater # Enable the capacity to extend the AppUpdater self._app_updater = AppUpdater(self, version_finder=self.get_version_finder(), check_interval=update_check_delay) self.started.connect(self._app_updater._thread.start) return self._app_updater
class Application(QApplication): """Main Nuxeo drive application controlled by a system tray icon + menu""" sync_thread = None def __init__(self, controller, options, argv=()): super(Application, self).__init__(list(argv)) self.controller = controller self.options = options self.binding_info = {} # Put communication channel in place for intra and inter-thread # communication for UI change notifications self.communicator = Communicator() self.communicator.icon.connect(self.set_icon_state) self.communicator.stop.connect(self.handle_stop) self.communicator.change.connect(self.handle_change) self.communicator.invalid_credentials.connect( self.handle_invalid_credentials) self.communicator.update_check.connect( self.refresh_update_status) # Timer to spin the transferring icon self.icon_spin_timer = QtCore.QTimer() self.icon_spin_timer.timeout.connect(self.spin_transferring_icon) self.icon_spin_count = 0 # Application update self.updater = None self.update_status = None self.update_version = None self.restart_updated_app = False # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) # Current state self.state = 'disabled' # Last state before suspend self.last_state = 'enabled' self.setup_systray() # Application update notification if self.controller.is_updated(): notify_updated(self.controller.get_version()) # Check if actions is required, separate method so it can be override self.init_checks() def init_checks(self): if self.controller.is_credentials_update_required(): # Prompt for settings if needed (performs a check for application # update) self.settings() else: # Initial check for application update (then periodic checks will # be done by the synchronizer thread) self.refresh_update_status() # Start long running synchronization thread self.start_synchronization_thread() def get_systray_menu(self): return SystrayMenu(self, self.controller.list_server_bindings()) def refresh_update_status(self): # TODO: first read update site URL from local configuration # See https://jira.nuxeo.com/browse/NXP-14403 server_bindings = self.controller.list_server_bindings() if not server_bindings: log.warning("Found no server binding, thus no update site URL," " can't check for application update") elif self.state != 'paused': # Let's refresh_update_info of the first server binding sb = server_bindings[0] self.controller.refresh_update_info(sb.local_folder) # Use server binding's update site URL as a version finder to # build / update the application updater. update_url = sb.update_url server_version = sb.server_version if update_url is None or server_version is None: log.warning("Update site URL or server version unavailable," " as a consequence update features won't be" " available") return if self.updater is None: # Build application updater if it doesn't exist try: self.updater = AppUpdater(version_finder=update_url) except Exception as e: log.warning(e) return else: # If application updater exists, simply update its version # finder self.updater.set_version_finder(update_url) # Set update status and update version self.update_status, self.update_version = ( self.updater.get_update_status( self.controller.get_version(), server_version)) if self.update_status == UPDATE_STATUS_UNAVAILABLE_SITE: # Update site unavailable log.warning("Update site is unavailable, as a consequence" " update features won't be available") elif self.update_status in [UPDATE_STATUS_MISSING_INFO, UPDATE_STATUS_MISSING_VERSION]: # Information or version missing in update site log.warning("Some information or version file is missing in" " the update site, as a consequence update" " features won't be available") else: # Update information successfully fetched log.info("Fetched information from update site %s: update" " status = '%s', update version = '%s'", self.updater.get_update_site(), self.update_status, self.update_version) if self._is_update_required(): # Current client version not compatible with server # version, upgrade or downgrade needed. # Let's stop synchronization thread. log.info("As current client version is not compatible with" " server version, an upgrade or downgrade is" " needed. Synchronization thread won't start" " until then.") self.stop_sync_thread() elif (self._is_update_available() and self.controller.is_auto_update()): # Update available and auto-update checked, let's process # update log.info("An application update is available and" " auto-update is checked") self.action_update(auto_update=True) return elif (self._is_update_available() and not self.controller.is_auto_update()): # Update available and auto-update not checked, let's just # update the systray icon and menu and let the user # explicitly choose to update log.info("An update is available and auto-update is not" " checked, let's just update the systray icon and" " menu and let the user explicitly choose to" " update") else: # Application is up-to-date log.info("Application is up-to-date") self.state = self._get_current_active_state() self.update_running_icon() self.communicator.menu.emit() def _is_update_required(self): return self.update_status in [UPDATE_STATUS_UPGRADE_NEEDED, UPDATE_STATUS_DOWNGRADE_NEEDED] def _is_update_available(self): return self.update_status == UPDATE_STATUS_UPDATE_AVAILABLE def set_icon_state(self, state): """Execute systray icon change operations triggered by state change The synchronization thread can update the state info but cannot directly call QtGui widget methods. This should be executed by the main thread event loop, hence the delegation to this method that is triggered by a signal to allow for message passing between the 2 threads. Return True of the icon has changed state. """ if self.get_icon_state() == state: # Nothing to update return False # Handle animated transferring icon if state == 'transferring': self.icon_spin_timer.start(150) else: self.icon_spin_timer.stop() icon = find_icon('nuxeo_drive_systray_icon_%s_18.png' % state) if icon is not None: self._tray_icon.setIcon(QtGui.QIcon(icon)) else: log.warning('Icon not found: %s', icon) self._icon_state = state log.debug('Updated icon state to: %s', state) return True def get_icon_state(self): return getattr(self, '_icon_state', None) def spin_transferring_icon(self): icon = find_icon('nuxeo_drive_systray_icon_transferring_%s.png' % (self.icon_spin_count + 1)) self._tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_spin_count = (self.icon_spin_count + 1) % 10 def suspend_resume(self): if self.state != 'paused': # Suspend sync if self._is_sync_thread_started(): # A sync thread is active, first update last state, current # state, icon and menu. self.last_state = self.state # If sync thread is asleep (waiting for next sync batch) set # current state to 'paused' directly, else set current state # to 'suspending' waiting for feedback from sync thread. if self.state == 'asleep': self.state = 'paused' else: self.state = 'suspending' self.update_running_icon() self.communicator.menu.emit() # Suspend the synchronizer thread: it will call # notify_sync_suspended() then wait until it gets notified by # a call to resume(). self.sync_thread.suspend() else: log.debug('No active synchronization thread, suspending sync' ' has no effect, keeping current state: %s', self.state) else: # Update state, icon and menu self.state = self.last_state self.update_running_icon() self.communicator.menu.emit() # Resume sync self.sync_thread.resume() def action_quit(self): self.quit_app_after_sync_stopped = True self.restart_updated_app = False self._stop() def action_update(self, auto_update=False): updated = False if auto_update: try: updated = self.updater.update(self.update_version) except Exception as e: log.error(e, exc_info=True) log.warning("An error occurred while trying to automatically" " update Nuxeo Drive to version %s, setting" " 'Auto update' to False", self.update_version) self.controller.set_auto_update(False) else: updated = prompt_update(self.controller, self._is_update_required(), self.controller.get_version(), self.update_version, self.updater) if updated: log.info("Will quit Nuxeo Drive and restart updated version %s", self.update_version) self.quit_app_after_sync_stopped = True self.restart_updated_app = True self._stop() def stop_sync_thread(self): self.quit_app_after_sync_stopped = False self._stop() def _stop(self): if self._is_sync_thread_started(): # A sync thread is active, first update state, icon and menu if self.quit_app_after_sync_stopped: self.state = 'stopping' self.update_running_icon() self.communicator.menu.emit() # Ask the controller to stop: the synchronization loop will break # and call notify_sync_stopped() which will finally emit a signal # to handle_stop() to quit the application. self.controller.stop() # Notify synchronization thread in case it was suspended self.sync_thread.resume() else: # Quit directly self.handle_stop() @QtCore.pyqtSlot() def handle_stop(self): if self.quit_app_after_sync_stopped: log.info('Quitting Nuxeo Drive') # Close thread-local Session log.debug("Calling Controller.dispose() from Qt Application to" " close thread-local Session") self.controller.dispose() if self.restart_updated_app: # Restart application by loading updated executable into # current process log.debug("Exiting Qt application") self.quit() current_version = self.updater.get_active_version() updated_version = self.update_version log.info("Current application version: %s", current_version) log.info("Updated application version: %s", updated_version) executable = sys.executable # TODO NXP-13818: better handle this! if sys.platform == 'darwin': executable = executable.replace('python', self.get_mac_app()) log.info("Current executable is: %s", executable) updated_executable = executable.replace(current_version, updated_version) log.info("Updated executable is: %s", updated_executable) args = [updated_executable] args.extend(sys.argv[1:]) log.info("Opening subprocess with args: %r", args) subprocess.Popen(args) else: self.quit() def get_mac_app(self): return 'ndrive' def update_running_icon(self): if self.state not in ['enabled', 'update_available', 'transferring']: self.communicator.icon.emit(self.state) return infos = self.binding_info.values() if len(infos) > 0 and any(i.online for i in infos): self.communicator.icon.emit(self.state) else: self.communicator.icon.emit('disabled') def notify_change(self, doc_pair, old_state): self.communicator.change.emit(doc_pair, old_state) def handle_change(self, doc_pair, old_state): pass def notify_local_folders(self, server_bindings): """Cleanup unbound server bindings if any""" local_folders = [sb.local_folder for sb in server_bindings] refresh = False for registered_folder in self.binding_info.keys(): if registered_folder not in local_folders: del self.binding_info[registered_folder] refresh = True for sb in server_bindings: if sb.local_folder not in self.binding_info: self.binding_info[sb.local_folder] = BindingInfo(sb) refresh = True if refresh: log.debug(u'Detected changes in the list of local folders: %s', u", ".join(local_folders)) self.update_running_icon() self.communicator.menu.emit() def get_binding_info(self, server_binding): local_folder = server_binding.local_folder if local_folder not in self.binding_info: self.binding_info[local_folder] = BindingInfo(server_binding) return self.binding_info[local_folder] def notify_sync_started(self): log.debug('Synchronization started') # Update state, icon and menu self.state = self._get_current_active_state() self.update_running_icon() self.communicator.menu.emit() def notify_sync_stopped(self): log.debug('Synchronization stopped') self.sync_thread = None # Send stop signal self.communicator.stop.emit() def notify_sync_asleep(self): # Update state to 'asleep' when sync thread is going to sleep # (waiting for next sync batch) self.state = 'asleep' def notify_sync_woken_up(self): # Update state to current active state when sync thread is woken up and # was not suspended if self.state != 'paused': self.state = self._get_current_active_state() else: self.last_state = self._get_current_active_state() def notify_sync_suspended(self): log.debug('Synchronization suspended') # Update state, icon and menu self.state = 'paused' self.update_running_icon() self.communicator.menu.emit() def notify_online(self, server_binding): info = self.get_binding_info(server_binding) if not info.online: # Mark binding as offline and update UI log.debug('Switching to online mode for: %s', server_binding.local_folder) info.online = True self.update_running_icon() self.communicator.menu.emit() def notify_offline(self, server_binding, exception): info = self.get_binding_info(server_binding) code = getattr(exception, 'code', None) if code is not None: reason = "Server returned HTTP code %r" % code else: reason = str(exception) local_folder = server_binding.local_folder if info.online: # Mark binding as offline and update UI log.debug('Switching to offline mode (reason: %s) for: %s', reason, local_folder) info.online = False self.state = 'disabled' self.update_running_icon() self.communicator.menu.emit() if code == 401: log.debug('Detected invalid credentials for: %s', local_folder) self.communicator.invalid_credentials.emit(local_folder) def notify_pending(self, server_binding, n_pending, or_more=False): # Update icon if n_pending > 0: self.state = 'transferring' else: self.state = self._get_current_active_state() self.update_running_icon() if server_binding is not None: local_folder = server_binding.local_folder info = self.get_binding_info(server_binding) if n_pending != info.n_pending: log.debug("%d pending operations for: %s", n_pending, local_folder) if n_pending == 0 and info.n_pending > 0: current_time = time.time() log.debug("Updating last ended synchronization date" " to %s for: %s", time.strftime(TIME_FORMAT_PATTERN, time.localtime(current_time)), local_folder) server_binding.last_ended_sync_date = current_time self.controller.get_session().commit() self.communicator.menu.emit() # Update pending stats info.n_pending = n_pending info.has_more_pending = or_more if not info.online: log.debug("Switching to online mode for: %s", local_folder) # Mark binding as online and update UI info.online = True self.update_running_icon() self.communicator.menu.emit() def notify_check_update(self): log.debug('Checking for application update') self.communicator.update_check.emit() def _get_current_active_state(self): if self._is_update_available(): return 'update_available' elif self._is_update_required(): return 'disabled' elif self.state == 'paused': return 'paused' else: return 'enabled' def setup_systray(self): self._tray_icon = QtGui.QSystemTrayIcon() self._tray_icon.setToolTip('Nuxeo Drive') self.update_running_icon() self._tray_icon.show() self.tray_icon_menu = self.get_systray_menu() self._tray_icon.setContextMenu(self.tray_icon_menu) self.communicator.menu.connect(self.update_menu) def update_menu(self): self.tray_icon_menu.update_menu(self.controller.list_server_bindings()) @QtCore.pyqtSlot(str) def handle_invalid_credentials(self, local_folder): sb = self.controller.get_server_binding(unicode(local_folder)) sb.invalidate_credentials() self.controller.get_session().commit() self.communicator.menu.emit() def settings(self): sb_settings = self.controller.get_server_binding_settings() proxy_settings = self.controller.get_proxy_settings() general_settings = self.controller.get_general_settings() version = self.controller.get_version() settings_accepted = prompt_settings(self.controller, sb_settings, proxy_settings, general_settings, version) if settings_accepted: # Check for application udpate self.refresh_update_status() # Start synchronization thread if needed self.start_synchronization_thread() return settings_accepted def start_synchronization_thread(self): # Make sure an application update is not required and synchronization # thread is not already started before actually starting it if (not self._is_update_required() and not self._is_sync_thread_started()): delay = getattr(self.options, 'delay', 5.0) max_sync_step = getattr(self.options, 'max_sync_step', 10) update_check_delay = getattr(self.options, 'update_check_delay', 3600) # Controller and its database session pool are thread safe, # hence reuse it directly self.controller.synchronizer.register_frontend(self) self.controller.synchronizer.delay = delay self.controller.synchronizer.max_sync_step = max_sync_step self.controller.synchronizer.update_check_delay = ( update_check_delay) self.sync_thread = SynchronizerThread(self.controller) log.info("Starting new synchronization thread %r", self.sync_thread) self.sync_thread.start() log.info("Synchronization thread %r started", self.sync_thread) def _is_sync_thread_started(self): return self.sync_thread is not None and self.sync_thread.isAlive() def event(self, event): """Handle URL scheme events under OSX""" log.trace("Received Qt application event") if hasattr(event, 'url'): url = str(event.url().toString()) log.debug("Event URL: %s", url) try: info = parse_protocol_url(url) log.debug("URL info: %r", info) if info is not None: log.debug("Received nxdrive URL scheme event: %s", url) if info.get('command') == 'edit': # This is a quick operation, no need to fork a QThread self.controller.launch_file_editor( info['server_url'], info['item_id']) except: log.error("Error handling URL event: %s", url, exc_info=True) return super(Application, self).event(event)
def refresh_update_status(self): # TODO: first read update site URL from local configuration # See https://jira.nuxeo.com/browse/NXP-14403 server_bindings = self.controller.list_server_bindings() if not server_bindings: log.warning("Found no server binding, thus no update site URL," " can't check for application update") elif self.state != 'paused': # Let's refresh_update_info of the first server binding sb = server_bindings[0] self.controller.refresh_update_info(sb.local_folder) # Use server binding's update site URL as a version finder to # build / update the application updater. update_url = sb.update_url server_version = sb.server_version if update_url is None or server_version is None: log.warning("Update site URL or server version unavailable," " as a consequence update features won't be" " available") return if self.updater is None: # Build application updater if it doesn't exist try: self.updater = AppUpdater(version_finder=update_url) except Exception as e: log.warning(e) return else: # If application updater exists, simply update its version # finder self.updater.set_version_finder(update_url) # Set update status and update version self.update_status, self.update_version = ( self.updater.get_update_status( self.controller.get_version(), server_version)) if self.update_status == UPDATE_STATUS_UNAVAILABLE_SITE: # Update site unavailable log.warning("Update site is unavailable, as a consequence" " update features won't be available") elif self.update_status in [UPDATE_STATUS_MISSING_INFO, UPDATE_STATUS_MISSING_VERSION]: # Information or version missing in update site log.warning("Some information or version file is missing in" " the update site, as a consequence update" " features won't be available") else: # Update information successfully fetched log.info("Fetched information from update site %s: update" " status = '%s', update version = '%s'", self.updater.get_update_site(), self.update_status, self.update_version) if self._is_update_required(): # Current client version not compatible with server # version, upgrade or downgrade needed. # Let's stop synchronization thread. log.info("As current client version is not compatible with" " server version, an upgrade or downgrade is" " needed. Synchronization thread won't start" " until then.") self.stop_sync_thread() elif (self._is_update_available() and self.controller.is_auto_update()): # Update available and auto-update checked, let's process # update log.info("An application update is available and" " auto-update is checked") self.action_update(auto_update=True) return elif (self._is_update_available() and not self.controller.is_auto_update()): # Update available and auto-update not checked, let's just # update the systray icon and menu and let the user # explicitly choose to update log.info("An update is available and auto-update is not" " checked, let's just update the systray icon and" " menu and let the user explicitly choose to" " update") else: # Application is up-to-date log.info("Application is up-to-date") self.state = self._get_current_active_state() self.update_running_icon() self.communicator.menu.emit()
class TestUpdater(unittest.TestCase): def setUp(self): appdir = 'nxdrive/tests/resources/esky_app' version_finder = 'nxdrive/tests/resources/esky_versions' self.esky_app = MockEsky(appdir, version_finder=version_finder) self.manager = MockManager() self.updater = AppUpdater(self.manager, esky_app=self.esky_app, local_update_site=True) def test_version_compare(self): # Compare server versions # Releases self.assertEquals(version_compare('5.9.3', '5.9.3'), 0) self.assertEquals(version_compare('5.9.3', '5.9.2'), 1) self.assertEquals(version_compare('5.9.2', '5.9.3'), -1) self.assertEquals(version_compare('5.9.3', '5.8'), 1) self.assertEquals(version_compare('5.8', '5.6.0'), 1) self.assertEquals(version_compare('5.9.1', '5.9.0.1'), 1) self.assertEquals(version_compare('6.0', '5.9.3'), 1) self.assertEquals(version_compare('5.10', '5.1.2'), 1) # Date-based self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.9.4-I20140415_0120'), 0) self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.9.4-I20140410_0120'), 1) self.assertEquals( version_compare('5.9.4-I20140515_0120', '5.9.4-I20140415_0120'), 1) self.assertEquals( version_compare('5.9.4-I20150102_0120', '5.9.4-I20143112_0120'), 1) self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.9.3-I20140415_0120'), 1) # Releases and date-based self.assertEquals(version_compare('5.9.4-I20140415_0120', '5.9.3'), 1) self.assertEquals(version_compare('5.9.4-I20140415_0120', '5.9.4'), -1) self.assertEquals(version_compare('5.9.4-I20140415_0120', '5.9.5'), -1) self.assertEquals(version_compare('5.9.3', '5.9.4-I20140415_0120'), -1) self.assertEquals(version_compare('5.9.4', '5.9.4-I20140415_0120'), 1) self.assertEquals(version_compare('5.9.5', '5.9.4-I20140415_0120'), 1) # Snapshots self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.4-SNAPSHOT'), 0) self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.3-SNAPSHOT'), 1) self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.8-SNAPSHOT'), 1) self.assertEquals(version_compare('5.9.3-SNAPSHOT', '5.9.4-SNAPSHOT'), -1) self.assertEquals(version_compare('5.8-SNAPSHOT', '5.9.4-SNAPSHOT'), -1) # Releases and snapshots self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.3'), 1) self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.4'), -1) self.assertEquals(version_compare('5.9.4-SNAPSHOT', '5.9.5'), -1) self.assertEquals(version_compare('5.9.3', '5.9.4-SNAPSHOT'), -1) self.assertEquals(version_compare('5.9.4', '5.9.4-SNAPSHOT'), 1) self.assertEquals(version_compare('5.9.5', '5.9.4-SNAPSHOT'), 1) # Date-based and snapshots self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.9.3-SNAPSHOT'), 1) self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.9.5-SNAPSHOT'), -1) self.assertEquals( version_compare('5.9.3-SNAPSHOT', '5.9.4-I20140415_0120'), -1) self.assertEquals( version_compare('5.9.5-SNAPSHOT', '5.9.4-I20140415_0120'), 1) # Can't decide, consider as equal self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.9.4-SNAPSHOT'), 0) self.assertEquals( version_compare('5.9.4-SNAPSHOT', '5.9.4-I20140415_0120'), 0) # Hotfixes self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-HF14'), 0) self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-HF13'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-HF15'), -1) self.assertEquals(version_compare('5.8.0-HF14', '5.6.0-HF35'), 1) self.assertEquals(version_compare('5.6.0-H35', '5.8.0-HF14'), -1) # Releases and hotfixes self.assertEquals(version_compare('5.8.0-HF14', '5.6'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.8'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.9.1'), -1) self.assertEquals(version_compare('5.6', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.8', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.9.1', '5.8.0-HF14'), 1) # Date-based and hotfixes self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.8.0-HF14'), 1) self.assertEquals( version_compare('5.8.1-I20140415_0120', '5.8.0-HF14'), 1) self.assertEquals( version_compare('5.8.0-I20140415_0120', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.8-I20140415_0120', '5.8.0-HF14'), -1) self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.10.0-HF01'), -1) self.assertEquals( version_compare('5.8.0-HF14', '5.9.4-I20140415_0120'), -1) self.assertEquals( version_compare('5.8.0-HF14', '5.8.1-I20140415_0120'), -1) self.assertEquals( version_compare('5.8.0-HF14', '5.8.0-I20140415_0120'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.8-I20140415_0120'), 1) self.assertEquals( version_compare('5.10.0-HF01', '5.9.4-I20140415_0120'), 1) # Snaphsots and hotfixes self.assertEquals(version_compare('5.8.0-HF14', '5.7.1-SNAPSHOT'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.8.0-SNAPSHOT'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.8-SNAPSHOT'), 1) self.assertEquals(version_compare('5.8.0-HF14', '5.9.1-SNAPSHOT'), -1) self.assertEquals(version_compare('5.7.1-SNAPSHOT', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.8.0-SNAPSHOT', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.8-SNAPSHOT', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.9.1-SNAPSHOT', '5.8.0-HF14'), 1) # Snapshot hotfixes self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), 0) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF13-SNAPSHOT'), 1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF15-SNAPSHOT'), -1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.6.0-HF35-SNAPSHOT'), 1) self.assertEquals( version_compare('5.6.0-H35-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1) # Releases and snapshot hotfixes self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.6'), 1) self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8'), 1) self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.9.1'), -1) self.assertEquals(version_compare('5.6', '5.8.0-HF14-SNAPSHOT'), -1) self.assertEquals(version_compare('5.8', '5.8.0-HF14-SNAPSHOT'), -1) self.assertEquals(version_compare('5.9.1', '5.8.0-HF14-SNAPSHOT'), 1) # Date-based and snapshot hotfixes self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.8.0-HF14-SNAPSHOT'), 1) self.assertEquals( version_compare('5.8.0-I20140415_0120', '5.8.0-HF14-SNAPSHOT'), -1) self.assertEquals( version_compare('5.9.4-I20140415_0120', '5.10.0-HF01-SNAPSHOT'), -1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.9.4-I20140415_0120'), -1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-I20140415_0120'), 1) self.assertEquals( version_compare('5.10.0-HF01-SNAPSHOT', '5.9.4-I20140415_0120'), 1) # Snaphsots and snapshot hotfixes self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.7.1-SNAPSHOT'), 1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.8-SNAPSHOT'), 1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-SNAPSHOT'), 1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.9.1-SNAPSHOT'), -1) self.assertEquals( version_compare('5.7.1-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1) self.assertEquals( version_compare('5.8-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1) self.assertEquals( version_compare('5.8.0-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), -1) self.assertEquals( version_compare('5.9.1-SNAPSHOT', '5.8.0-HF14-SNAPSHOT'), 1) # Hotfixes and snapshot hotfixes self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.6.0-HF35'), 1) self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF13'), 1) self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF14'), -1) self.assertEquals(version_compare('5.8.0-HF14-SNAPSHOT', '5.8.0-HF15'), -1) self.assertEquals( version_compare('5.8.0-HF14-SNAPSHOT', '5.10.0-HF01'), -1) # Compare client versions self.assertEquals(version_compare('0.1', '1.0'), -1) self.assertEquals(version_compare('1.0', '1.0'), 0) self.assertEquals(version_compare('1.3.0424', '1.3.0424'), 0) self.assertEquals(version_compare('1.3.0524', '1.3.0424'), 1) self.assertEquals(version_compare('1.4', '1.3.0524'), 1) self.assertEquals(version_compare('1.4.0622', '1.3.0524'), 1) self.assertEquals(version_compare('1.10', '1.1.2'), 1) self.assertEquals(version_compare('2.1.0528', '1.10'), 1) self.assertEquals(version_compare('2.0.0626', '2.0.806'), -1) self.assertEquals(version_compare('2.0.0805', '2.0.806'), -1) self.assertEquals(version_compare('2.0.0905', '2.0.806'), 1) self.assertEquals(version_compare('2.0.805', '2.0.1206'), -1) def test_get_active_version(self): # Active version is None because Esky instance is built from a # directory, see Esky._init_from_appdir self.assertIsNone(self.updater.get_active_version()) def test_get_current_latest_version(self): self.assertEquals(self.updater.get_current_latest_version(), '1.3.0424') def test_find_versions(self): versions = self.updater.find_versions() self.assertEquals(versions, ['1.3.0524', '1.4.0622']) def test_get_server_min_version(self): # Unexisting version self.assertRaises(MissingUpdateSiteInfo, self.updater.get_server_min_version, '4.6.2012') self.assertEquals(self.updater.get_server_min_version('1.3.0424'), '5.8') self.assertEquals(self.updater.get_server_min_version('1.3.0524'), '5.9.1') self.assertEquals(self.updater.get_server_min_version('1.4.0622'), '5.9.2') def test_get_client_min_version(self): # Unexisting version self.assertRaises(MissingUpdateSiteInfo, self.updater._get_client_min_version, '5.6') self.assertEquals(self.updater._get_client_min_version('5.8'), '1.2.0110') self.assertEquals(self.updater._get_client_min_version('5.9.1'), '1.3.0424') self.assertEquals(self.updater._get_client_min_version('5.9.2'), '1.3.0424') self.assertEquals(self.updater._get_client_min_version('5.9.3'), '1.4.0622') self.assertEquals(self.updater._get_client_min_version('5.9.4'), '1.5.0715') def _get_latest_compatible_version(self, version): self.manager.clean_engines() self.manager.add_engine(version) return self.updater.get_latest_compatible_version() def test_get_latest_compatible_version(self): # No update info available for server version self.assertRaises(MissingUpdateSiteInfo, self._get_latest_compatible_version, '5.6') # No compatible client version with server version self.assertRaises(MissingCompatibleVersion, self._get_latest_compatible_version, '5.9.4') # Compatible versions self.assertEqual(self._get_latest_compatible_version('5.9.3'), '1.4.0622') self.assertEqual(self._get_latest_compatible_version('5.9.2'), '1.4.0622') self.assertEqual(self._get_latest_compatible_version('5.9.1'), '1.3.0524') self.assertEqual(self._get_latest_compatible_version('5.8'), '1.3.0424') def _get_update_status(self, client_version, server_version, add_version=None): self.manager.set_version(client_version) self.manager.clean_engines() self.manager.add_engine(server_version) if add_version is not None: self.manager.add_engine(add_version) return self.updater._get_update_status() def test_get_update_status(self): # No update info available (missing client version info) status = self._get_update_status('1.2.0207', '5.9.3') self.assertEquals(status, (UPDATE_STATUS_MISSING_INFO, None)) # No update info available (missing server version info) status = self._get_update_status('1.3.0424', '5.6') self.assertEquals(status, (UPDATE_STATUS_MISSING_INFO, None)) # No compatible client version with server version status = self._get_update_status('1.4.0622', '5.9.4') self.assertEquals(status, (UPDATE_STATUS_MISSING_VERSION, None)) # Upgraded needed status = self._get_update_status('1.3.0424', '5.9.3') self.assertEquals(status, (UPDATE_STATUS_UPGRADE_NEEDED, '1.4.0622')) status = self._get_update_status('1.3.0524', '5.9.3') self.assertEquals(status, (UPDATE_STATUS_UPGRADE_NEEDED, '1.4.0622')) # Downgrade needed status = self._get_update_status('1.3.0524', '5.8') self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0424')) status = self._get_update_status('1.4.0622', '5.8') self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0424')) status = self._get_update_status('1.4.0622', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0524')) # Upgrade available status = self._get_update_status('1.3.0424', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.3.0524')) status = self._get_update_status('1.3.0424', '5.9.2') self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.4.0622')) status = self._get_update_status('1.3.0524', '5.9.2') self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.4.0622')) # Up-to-date status = self._get_update_status('1.3.0424', '5.8') self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None)) status = self._get_update_status('1.3.0524', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None)) status = self._get_update_status('1.4.0622', '5.9.2') self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None)) status = self._get_update_status('1.4.0622', '5.9.3') self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None)) # Test multi server status = self._get_update_status('1.3.0524', '5.9.2', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None)) # Force upgrade for the 5.9.3 server status = self._get_update_status('1.3.0524', '5.9.2', '5.9.3') self.assertEquals(status, (UPDATE_STATUS_UPGRADE_NEEDED, '1.4.0622')) # No compatible version with 5.9.1 and 5.9.3 status = self._get_update_status('1.3.0524', '5.9.1', '5.9.3') self.assertEquals(status, (UPDATE_STATUS_MISSING_VERSION, None)) # Need to downgrade for 5.8 server status = self._get_update_status('1.3.0524', '5.8', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_DOWNGRADE_NEEDED, '1.3.0424')) # Up to date once downgrade status = self._get_update_status('1.3.0424', '5.8', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_UP_TO_DATE, None)) # Limit the range of upgrade because of 5.9.1 server status = self._get_update_status('1.3.0424', '5.9.2', '5.9.1') self.assertEquals(status, (UPDATE_STATUS_UPDATE_AVAILABLE, '1.3.0524'))
class Manager(QtCore.QObject): ''' classdocs ''' proxyUpdated = QtCore.pyqtSignal(object) clientUpdated = QtCore.pyqtSignal(object, object) engineNotFound = QtCore.pyqtSignal(object) newEngine = QtCore.pyqtSignal(object) dropEngine = QtCore.pyqtSignal(object) initEngine = QtCore.pyqtSignal(object) aboutToStart = QtCore.pyqtSignal(object) started = QtCore.pyqtSignal() stopped = QtCore.pyqtSignal() suspended = QtCore.pyqtSignal() resumed = QtCore.pyqtSignal() _singleton = None @staticmethod def get(): return Manager._singleton def __init__(self, options): ''' Constructor ''' if Manager._singleton is not None: raise Exception("Only one instance of Manager can be create") Manager._singleton = self super(Manager, self).__init__() # Let's bypass HTTPS verification unless --consider-ssl-errors is passed # since many servers unfortunately have invalid certificates. # See https://www.python.org/dev/peps/pep-0476/ # and https://jira.nuxeo.com/browse/NXDRIVE-506 if not options.consider_ssl_errors: log.warn("--consider-ssl-errors option is False, won't verify HTTPS certificates") import ssl try: _create_unverified_https_context = ssl._create_unverified_context except AttributeError: log.info("Legacy Python that doesn't verify HTTPS certificates by default") else: log.info("Handle target environment that doesn't support HTTPS verification:" " globally disable verification by monkeypatching the ssl module though highly discouraged") ssl._create_default_https_context = _create_unverified_https_context else: log.info("--consider-ssl-errors option is True, will verify HTTPS certificates") self._autolock_service = None self.nxdrive_home = os.path.expanduser(options.nxdrive_home) self.nxdrive_home = os.path.realpath(self.nxdrive_home) if not os.path.exists(self.nxdrive_home): os.mkdir(self.nxdrive_home) self.remote_watcher_delay = options.delay self._nofscheck = options.nofscheck self._debug = options.debug self._engine_definitions = None self._engine_types = dict() from nxdrive.engine.next.engine_next import EngineNext from nxdrive.engine.engine import Engine self._engine_types["NXDRIVE"] = Engine self._engine_types["NXDRIVENEXT"] = EngineNext self._engines = None self.proxies = None self.proxy_exceptions = None self._app_updater = None self._dao = None self._create_dao() if options.proxy_server is not None: proxy = ProxySettings() proxy.from_url(options.proxy_server) proxy.save(self._dao) # Now we can update the logger if needed if options.log_level_file is not None: # Set the log_level_file option handler = self._get_file_log_handler() if handler is not None: handler.setLevel(options.log_level_file) # Store it in the database self._dao.update_config("log_level_file", str(handler.level)) else: # No log_level provide, use the one from db default is INFO self._update_logger(int(self._dao.get_config("log_level_file", "20"))) # Add auto lock on edit res = self._dao.get_config("direct_edit_auto_lock") if res is None: self._dao.update_config("direct_edit_auto_lock", "1") # Persist update URL infos self._dao.update_config("update_url", options.update_site_url) self._dao.update_config("beta_update_url", options.beta_update_site_url) self.refresh_proxies() self._os = AbstractOSIntegration.get(self) # Create DirectEdit self._create_autolock_service() self._create_direct_edit(options.protocol_url) # Create notification service self._script_engine = None self._script_object = None self._create_notification_service() self._started = False # Pause if in debug self._pause = self.is_debug() self.device_id = self._dao.get_config("device_id") self.updated = False # self.update_version() if self.device_id is None: self.generate_device_id() self.load() # Create the application update verification thread self._create_updater(options.update_check_delay) # Force language if options.force_locale is not None: self.set_config("locale", options.force_locale) # Setup analytics tracker self._tracker = None if self.get_tracking(): self._create_tracker() def _get_file_log_handler(self): # Might store it in global static return FILE_HANDLER def get_metrics(self): result = dict() result["version"] = self.get_version() result["auto_start"] = self.get_auto_start() result["auto_update"] = self.get_auto_update() result["beta_channel"] = self.get_beta_channel() result["device_id"] = self.get_device_id() result["tracker_id"] = self.get_tracker_id() result["tracking"] = self.get_tracking() result["qt_version"] = QtCore.QT_VERSION_STR result["pyqt_version"] = QtCore.PYQT_VERSION_STR result["python_version"] = platform.python_version() result["platform"] = platform.system() result["appname"] = self.get_appname() return result def open_help(self): self.open_local_file("http://doc.nuxeo.com/display/USERDOC/Nuxeo+Drive") def get_log_level(self): handler = self._get_file_log_handler() if handler: return handler.level return logging.getLogger().getEffectiveLevel() def set_log_level(self, log_level): self._dao.update_config("log_level_file", str(log_level)) self._update_logger(log_level) def _update_logger(self, log_level): logging.getLogger().setLevel( min(log_level, logging.getLogger().getEffectiveLevel())) handler = self._get_file_log_handler() if handler: handler.setLevel(log_level) def get_osi(self): return self._os def _handle_os(self): # Be sure to register os self._os.register_contextual_menu() self._os.register_protocol_handlers() if self.get_auto_start(): self._os.register_startup() def get_appname(self): return "Nuxeo Drive" def is_debug(self): return self._debug def is_checkfs(self): return not self._nofscheck def get_device_id(self): return self.device_id def get_notification_service(self): return self._notification_service def _create_notification_service(self): # Dont use it for now from nxdrive.notification import DefaultNotificationService self._notification_service = DefaultNotificationService(self) return self._notification_service def get_autolock_service(self): return self._autolock_service def _create_autolock_service(self): from nxdrive.autolocker import ProcessAutoLockerWorker self._autolock_service = ProcessAutoLockerWorker(30, self) self.started.connect(self._autolock_service._thread.start) return self._autolock_service def _create_tracker(self): from nxdrive.engine.tracker import Tracker self._tracker = Tracker(self) # Start the tracker when we launch self.started.connect(self._tracker._thread.start) return self._tracker def get_tracker_id(self): if self.get_tracking() and self._tracker is not None: return self._tracker.uid return "" def get_tracker(self): return self._tracker def _get_db(self): return os.path.join(normalized_path(self.nxdrive_home), "manager.db") def get_dao(self): return self._dao def _migrate(self): from nxdrive.engine.dao.sqlite import ManagerDAO self._dao = ManagerDAO(self._get_db()) old_db = os.path.join(normalized_path(self.nxdrive_home), "nxdrive.db") if os.path.exists(old_db): import sqlite3 from nxdrive.engine.dao.sqlite import CustomRow conn = sqlite3.connect(old_db) conn.row_factory = CustomRow c = conn.cursor() cfg = c.execute("SELECT * FROM device_config LIMIT 1").fetchone() if cfg is not None: self.device_id = cfg.device_id self._dao.update_config("device_id", cfg.device_id) self._dao.update_config("proxy_config", cfg.proxy_config) self._dao.update_config("proxy_type", cfg.proxy_type) self._dao.update_config("proxy_server", cfg.proxy_server) self._dao.update_config("proxy_port", cfg.proxy_port) self._dao.update_config("proxy_authenticated", cfg.proxy_authenticated) self._dao.update_config("proxy_username", cfg.proxy_username) self._dao.update_config("auto_update", cfg.auto_update) # Copy first server binding rows = c.execute("SELECT * FROM server_bindings").fetchall() if not rows: return first_row = True for row in rows: row.url = row.server_url log.debug("Binding server from Nuxeo Drive V1: [%s, %s]", row.url, row.remote_user) row.username = row.remote_user row.password = None row.token = row.remote_token row.no_fscheck = True engine = self.bind_engine(self._get_default_server_type(), row["local_folder"], self._get_engine_name(row.url), row, starts=False) log.trace("Resulting server binding remote_token %r", row.remote_token) if first_row: first_engine_def = row first_engine = engine first_row = False else: engine.dispose_db() # Copy filters for first engine as V1 only supports filtering for the first server binding filters = c.execute("SELECT * FROM filters") for filter_obj in filters: if first_engine_def.local_folder != filter_obj.local_folder: continue log.trace("Filter Row from DS1 %r", filter_obj) first_engine.add_filter(filter_obj["path"]) first_engine.dispose_db() def _create_dao(self): from nxdrive.engine.dao.sqlite import ManagerDAO if not os.path.exists(self._get_db()): try: self._migrate() return except Exception as e: log.error(e, exc_info=True) self._dao = ManagerDAO(self._get_db()) def _create_updater(self, update_check_delay): if (update_check_delay == 0): log.info("Update check delay is 0, disabling autoupdate") self._app_updater = FakeUpdater() return self._app_updater # Enable the capacity to extend the AppUpdater self._app_updater = AppUpdater(self, version_finder=self.get_version_finder(), check_interval=update_check_delay) self.started.connect(self._app_updater._thread.start) return self._app_updater def get_version_finder(self, refresh_engines=False): # Used by extended application to inject version finder if self.get_beta_channel(): log.debug('Update beta channel activated') update_site_url = self._get_beta_update_url(refresh_engines) else: update_site_url = self._get_update_url(refresh_engines) if update_site_url is None: update_site_url = DEFAULT_UPDATE_SITE_URL if not update_site_url.endswith('/'): update_site_url += '/' return update_site_url def _get_update_url(self, refresh_engines): update_url = self._dao.get_config("update_url", DEFAULT_UPDATE_SITE_URL) # If update site URL is not overridden in config.ini nor through the command line, refresh engine update infos # and use first engine configuration if update_url == DEFAULT_UPDATE_SITE_URL: try: if refresh_engines: self._refresh_engine_update_infos() engines = self.get_engines() if engines: first_engine = engines.itervalues().next() update_url = first_engine.get_update_url() log.debug('Update site URL has not been overridden in config.ini nor through the command line,' ' using configuration from first engine [%s]: %s', first_engine._name, update_url) except URLError as e: log.error('Cannot refresh engine update infos, using default update site URL', exc_info=True) return update_url def _get_beta_update_url(self, refresh_engines): beta_update_url = self._dao.get_config("beta_update_url") if beta_update_url is None: try: if refresh_engines: self._refresh_engine_update_infos() engines = self.get_engines() if engines: for engine in engines.itervalues(): beta_update_url = engine.get_beta_update_url() if beta_update_url is not None: log.debug('Beta update site URL has not been defined in config.ini nor through the command' ' line, using configuration from engine [%s]: %s', engine._name, beta_update_url) return beta_update_url except URLError: log.exception('Cannot refresh engine update infos, not using beta update site URL') return beta_update_url def is_beta_channel_available(self): return self._get_beta_update_url(False) is not None def get_updater(self): return self._app_updater def refresh_update_status(self): if self.get_updater() is not None: self.get_updater().refresh_status() def _refresh_engine_update_infos(self): log.debug('Refreshing engine infos') engines = self.get_engines() if engines: for engine in engines.itervalues(): engine.get_update_infos() def _create_direct_edit(self, url): from nxdrive.direct_edit import DirectEdit self._direct_edit = DirectEdit(self, os.path.join(normalized_path(self.nxdrive_home), "edit"), url) self.started.connect(self._direct_edit._thread.start) return self._direct_edit def get_direct_edit(self): return self._direct_edit def is_paused(self): return self._pause def resume(self, euid=None): if not self._pause: return self._pause = False for uid, engine in self._engines.items(): if euid is not None and euid != uid: continue log.debug("Resume engine %s", uid) engine.resume() self.resumed.emit() def suspend(self, euid=None): if self._pause: return self._pause = True for uid, engine in self._engines.items(): if euid is not None and euid != uid: continue log.debug("Suspend engine %s", uid) engine.suspend() self.suspended.emit() def stop(self, euid=None): for uid, engine in self._engines.items(): if euid is not None and euid != uid: continue if engine.is_started(): log.debug("Stop engine %s", uid) engine.stop() self.stopped.emit() def start(self, euid=None): self._started = True for uid, engine in self._engines.items(): if euid is not None and euid != uid: continue if not self._pause: self.aboutToStart.emit(engine) log.debug("Launch engine %s", uid) try: engine.start() except Exception as e: log.debug("Could not start the engine: %s [%r]", uid, e) log.debug("Emitting started") # Check only if manager is started self._handle_os() self.started.emit() def load(self): if self._engine_definitions is None: self._engine_definitions = self._dao.get_engines() in_error = dict() self._engines = dict() for engine in self._engine_definitions: if not engine.engine in self._engine_types: log.warn("Can't find engine %s anymore", engine.engine) if not engine.engine in in_error: in_error[engine.engine] = True self.engineNotFound.emit(engine) self._engines[engine.uid] = self._engine_types[engine.engine](self, engine, remote_watcher_delay=self.remote_watcher_delay) self._engines[engine.uid].online.connect(self._force_autoupdate) self.initEngine.emit(self._engines[engine.uid]) def _get_default_nuxeo_drive_name(self): return 'Nuxeo Drive' def _force_autoupdate(self): if (self._app_updater.get_next_poll() > 60 and self._app_updater.get_last_poll() > 1800): self._app_updater.force_poll() def get_default_nuxeo_drive_folder(self): # TODO: Factorize with utils.default_nuxeo_drive_folder """Find a reasonable location for the root Nuxeo Drive folder This folder is user specific, typically under the home folder. Under Windows, try to locate My Documents as a home folder, using the win32com shell API if allowed, else falling back on a manual detection. Note that we need to decode the path returned by os.path.expanduser with the local encoding because the value of the HOME environment variable is read as a byte string. Using os.path.expanduser(u'~') fails if the home path contains non ASCII characters since Unicode coercion attempts to decode the byte string as an ASCII string. """ if sys.platform == "win32": from win32com.shell import shell, shellcon try: my_documents = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, None, 0) except: # In some cases (not really sure how this happens) the current user # is not allowed to access its 'My Documents' folder path through # the win32com shell API, which raises the following error: # com_error: (-2147024891, 'Access is denied.', None, None) # We noticed that in this case the 'Location' tab is missing in the # Properties window of 'My Documents' accessed through the # Explorer. # So let's fall back on a manual (and poor) detection. # WARNING: it's important to check 'Documents' first as under # Windows 7 there also exists a 'My Documents' folder invisible in # the Explorer and cmd / powershell but visible from Python. # First try regular location for documents under Windows 7 and up log.debug("Access denied to win32com shell API: SHGetFolderPath," " falling back on manual detection of My Documents") my_documents = os.path.expanduser(r'~\Documents') my_documents = unicode(my_documents.decode(ENCODING)) if not os.path.exists(my_documents): # Compatibility for Windows XP my_documents = os.path.expanduser(r'~\My Documents') my_documents = unicode(my_documents.decode(ENCODING)) if os.path.exists(my_documents): nuxeo_drive_folder = self._increment_local_folder(my_documents, self._get_default_nuxeo_drive_name()) log.debug("Will use '%s' as default Nuxeo Drive folder location under Windows", nuxeo_drive_folder) return nuxeo_drive_folder # Fall back on home folder otherwise user_home = os.path.expanduser('~') user_home = unicode(user_home.decode(ENCODING)) nuxeo_drive_folder = self._increment_local_folder(user_home, self._get_default_nuxeo_drive_name()) log.debug("Will use '%s' as default Nuxeo Drive folder location", nuxeo_drive_folder) return nuxeo_drive_folder def _increment_local_folder(self, basefolder, name): nuxeo_drive_folder = os.path.join(basefolder, name) num = 2 while (not self.check_local_folder_available(nuxeo_drive_folder)): nuxeo_drive_folder = os.path.join(basefolder, name + " " + str(num)) num = num + 1 if num > 10: return "" return nuxeo_drive_folder def get_configuration_folder(self): return self.nxdrive_home def open_local_file(self, file_path): """Launch the local OS program on the given file / folder.""" log.debug('Launching editor on %s', file_path) if sys.platform == 'win32': os.startfile(file_path) elif sys.platform == 'darwin': subprocess.Popen(['open', file_path]) else: try: subprocess.Popen(['xdg-open', file_path]) except OSError: # xdg-open should be supported by recent Gnome, KDE, Xfce log.error("Failed to find and editor for: '%s'", file_path) def check_version_updated(self): last_version = self._dao.get_config("client_version") if last_version != self.get_version(): self.clientUpdated.emit(last_version, self.get_version()) def generate_device_id(self): self.device_id = uuid.uuid1().hex self._dao.update_config("device_id", self.device_id) def get_proxy_settings(self, device_config=None): """Fetch proxy settings from database""" return ProxySettings(dao=self._dao) def list_server_bindings(self): if self._engines is None: self.load() result = [] for definition in self._engine_definitions: row = definition row.server_version = None row.update_url = "" self._engines[row.uid].complete_binder(row) result.append(row) return result def get_config(self, value, default=None): return self._dao.get_config(value, default) def set_config(self, key, value): return self._dao.update_config(key, value) def get_direct_edit_auto_lock(self): return self._dao.get_config("direct_edit_auto_lock", "1") == "1" def set_direct_edit_auto_lock(self, value): self._dao.update_config("direct_edit_auto_lock", value) def get_auto_update(self): # By default auto update return self._dao.get_config("auto_update", "1") == "1" def set_auto_update(self, value): self._dao.update_config("auto_update", value) def get_auto_start(self): return self._dao.get_config("auto_start", "1") == "1" def _get_binary_name(self): return 'ndrive' def generate_report(self, path=None): from nxdrive.report import Report report = Report(self, path) report.generate() return report.get_path() def find_exe_path(self): """Introspect the Python runtime to find the frozen Windows exe""" import nxdrive nxdrive_path = os.path.realpath(os.path.dirname(nxdrive.__file__)) log.trace("nxdrive_path: %s", nxdrive_path) # Detect frozen win32 executable under Windows executable = sys.executable if "appdata" in executable: executable = os.path.join(os.path.dirname(executable), "..","..",os.path.basename( sys.executable)) exe_path = os.path.abspath(executable) if os.path.exists(exe_path): log.trace("Returning exe path: %s", exe_path) return exe_path # Detect OSX frozen app if nxdrive_path.endswith(OSX_SUFFIX): log.trace("Detected OS X frozen app") exe_path = nxdrive_path.replace(OSX_SUFFIX, "Contents/MacOS/" + self._get_binary_name()) if os.path.exists(exe_path): log.trace("Returning exe path: %s", exe_path) return exe_path # Fall-back to the regular method that should work both the ndrive script exe_path = sys.argv[0] log.trace("Returning default exe path: %s", exe_path) return exe_path def set_auto_start(self, value): self._dao.update_config("auto_start", value) if value: self._os.register_startup() else: self._os.unregister_startup() def get_beta_channel(self): return self._dao.get_config("beta_channel", "0") == "1" def set_beta_channel(self, value): self._dao.update_config("beta_channel", value) # Trigger update status refresh self.refresh_update_status() def get_tracking(self): return self._dao.get_config("tracking", "1") == "1" and not self.get_version().endswith("-dev") def set_tracking(self, value): self._dao.update_config("tracking", value) if value: self._create_tracker() elif self._tracker is not None: self._tracker._thread.quit() self._tracker = None def validate_proxy_settings(self, proxy_settings): try: import urllib2 proxies, _ = get_proxies_for_handler(proxy_settings) # Try google website url = "http://www.google.com" opener = urllib2.build_opener(urllib2.ProxyHandler(proxies), urllib2.HTTPBasicAuthHandler(), urllib2.HTTPHandler) urllib2.install_opener(opener) conn = urllib2.urlopen(url) conn.read() except Exception as e: log.error("Exception setting proxy : %s", e) return False return True def set_proxy_settings(self, proxy_settings, force=False): if force or self.validate_proxy_settings(proxy_settings): proxy_settings.save(self._dao) self.refresh_proxies(proxy_settings) log.info("Proxy settings successfully updated: %r", proxy_settings) return "" else: return "PROXY_INVALID" def refresh_proxies(self, proxy_settings=None, device_config=None): """Refresh current proxies with the given settings""" # If no proxy settings passed fetch them from database proxy_settings = (proxy_settings if proxy_settings is not None else self.get_proxy_settings()) self.proxies, self.proxy_exceptions = get_proxies_for_handler( proxy_settings) self.proxyUpdated.emit(proxy_settings) def get_proxies(self): return self.proxies def get_engine(self, local_folder): if self._engines is None: self.load() for engine_def in self._engine_definitions: if local_folder.startswith(engine_def.local_folder): return self._engines[engine_def.uid] return None def edit(self, engine, remote_ref): """Find the local file if any and start OS editor on it.""" doc_pair = engine.get_dao().get_normal_state_from_remote(remote_ref) if doc_pair is None: log.warning('Could not find local file for engine %s and remote_ref %s', engine.get_uid(), remote_ref) return # TODO: check synchronization of this state first # Find the best editor for the file according to the OS configuration local_client = engine.get_local_client() self.open_local_file(local_client._abspath(doc_pair.local_path)) def _get_default_server_type(self): return "NXDRIVE" def bind_server(self, local_folder, url, username, password, token=None, name=None, start_engine=True, check_credentials=True): from collections import namedtuple if name is None: name = self._get_engine_name(url) binder = namedtuple('binder', ['username', 'password', 'token', 'url', 'no_check', 'no_fscheck']) binder.username = username binder.password = password binder.token = token binder.no_check = not check_credentials binder.no_fscheck = False binder.url = url return self.bind_engine(self._get_default_server_type(), local_folder, name, binder, starts=start_engine) def _get_engine_name(self, server_url): import urlparse urlp = urlparse.urlparse(server_url) return urlp.hostname def check_local_folder_available(self, local_folder): if self._engine_definitions is None: return True if not local_folder.endswith('/'): local_folder = local_folder + '/' for engine in self._engine_definitions: other = engine.local_folder if not other.endswith('/'): other = other + '/' if (other.startswith(local_folder) or local_folder.startswith(other)): return False return True def update_engine_path(self, uid, local_folder): # Dont update the engine by itself, should be only used by engine.update_engine_path if uid in self._engine_definitions: self._engine_definitions[uid].local_folder = local_folder self._dao.update_engine_path(uid, local_folder) def bind_engine(self, engine_type, local_folder, name, binder, starts=True): """Bind a local folder to a remote nuxeo server""" if name is None and hasattr(binder, 'url'): name = self._get_engine_name(binder.url) if hasattr(binder, 'url'): url = binder.url if '#' in url: # Last part of the url is the engine type engine_type = url.split('#')[1] binder.url = url.split('#')[0] log.debug("Engine type has been specified in the url: %s will be used", engine_type) if not self.check_local_folder_available(local_folder): raise FolderAlreadyUsed() if not engine_type in self._engine_types: raise EngineTypeMissing() if self._engines is None: self.load() local_folder = normalized_path(local_folder) if local_folder == self.get_configuration_folder(): # Prevent from binding in the configuration folder raise FolderAlreadyUsed() uid = uuid.uuid1().hex # TODO Check that engine is not inside another or same position engine_def = self._dao.add_engine(engine_type, local_folder, uid, name) try: self._engines[uid] = self._engine_types[engine_type](self, engine_def, binder=binder, remote_watcher_delay=self.remote_watcher_delay) self._engine_definitions.append(engine_def) except Exception as e: log.exception(e) if uid in self._engines: del self._engines[uid] self._dao.delete_engine(uid) # TODO Remove the db ? raise e # As new engine was just bound, refresh application update status self.refresh_update_status() if starts: self._engines[uid].start() self.newEngine.emit(self._engines[uid]) return self._engines[uid] #server_url, username, password # check the connection to the server by issuing an authentication # request def unbind_engine(self, uid): if self._engines is None: self.load() self._engines[uid].suspend() self._engines[uid].unbind() self._dao.delete_engine(uid) # Refresh the engines definition del self._engines[uid] self.dropEngine.emit(uid) self._engine_definitions = self._dao.get_engines() def unbind_all(self): if self._engines is None: self.load() for engine in self._engine_definitions: self.unbind_engine(engine.uid) def dispose_db(self): if self._dao is not None: self._dao.dispose() def dispose_all(self): for engine in self.get_engines().values(): engine.dispose_db() self.dispose_db() def get_engines(self): return self._engines def get_engines_type(self): return self._engine_types def get_version(self): return __version__ def update_version(self, device_config): if self.version != device_config.client_version: log.info("Detected version upgrade: current version = %s," " new version = %s => upgrading current version," " yet DB upgrade might be needed.", device_config.client_version, self.version) device_config.client_version = self.version self.get_session().commit() return True return False def is_started(self): return self._started def is_updated(self): return self.updated def is_syncing(self): syncing_engines = [] for uid, engine in self._engines.items(): if engine.is_syncing(): syncing_engines.append(uid) if syncing_engines: log.debug("Some engines are currently synchronizing: %s", syncing_engines) return True else: log.debug("No engine currently synchronizing") return False def get_root_id(self, file_path): from nxdrive.client import LocalClient ref = LocalClient.get_path_remote_id(file_path, 'ndriveroot') if ref is None: parent = os.path.dirname(file_path) # We can't find in any parent if parent == file_path or parent is None: return None return self.get_root_id(parent) return ref def get_cf_bundle_identifier(self): return "org.nuxeo.drive" def get_metadata_infos(self, file_path): from nxdrive.client import LocalClient remote_ref = LocalClient.get_path_remote_id(file_path) if remote_ref is None: raise ValueError('Could not find file %s as Nuxeo Drive managed' % file_path) root_id = self.get_root_id(file_path) # TODO Add a class to handle root info root_values = root_id.split("|") try: engine = self.get_engines()[root_values[3]] except: raise ValueError('Unknown engine %s for %s' % (root_values[3], file_path)) metadata_url = engine.get_metadata_url(remote_ref) return (metadata_url, engine.get_remote_token(), engine, remote_ref) def set_script_object(self, obj): # Used to enhance scripting with UI self._script_object = obj def _create_script_engine(self): from nxdrive.scripting import DriveScript self._script_engine = QScriptEngine() if self._script_object is None: self._script_object = DriveScript(self) self._script_engine.globalObject().setProperty("drive", self._script_engine.newQObject(self._script_object)) def execute_script(self, script, engine_uid=None): if self._script_engine is None: self._create_script_engine() if self._script_engine is None: return self._script_object.set_engine_uid(engine_uid) log.debug("Will execute '%s'", script) result = self._script_engine.evaluate(script) if self._script_engine.hasUncaughtException(): log.debug("Execution exception: %r", result.toString())