Example #1
0
 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))
Example #2
0
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__()
        self._autolock_service = None
        self.client_version = __version__
        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.engine import Engine
        self._engine_types["NXDRIVE"] = Engine
        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("drive_edit_auto_lock")
        if res is None:
            self._dao.update_config("update_url", "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 DriveEdit
        self._create_autolock_service()
        self._create_drive_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):
        # 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:
            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
        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_drive_edit(self, url):
        from nxdrive.drive_edit import DriveEdit
        self._drive_edit = DriveEdit(self, os.path.join(normalized_path(self.nxdrive_home), "edit"), url)
        self.started.connect(self._drive_edit._thread.start)
        return self._drive_edit

    def get_drive_edit(self):
        return self._drive_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.client_version:
            self.clientUpdated.emit(last_version, self.client_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_drive_edit_auto_lock(self):
        return self._dao.get_config("drive_edit_auto_lock", "1") == "1"

    def set_drive_edit_auto_lock(self, value):
        self._dao.update_config("drive_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 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 self.client_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())
Example #3
0
 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))
Example #4
0
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())