Ejemplo n.º 1
0
    def _print_pair_diff(self, pair):
        any_entry = pair.any_entry

        has_meta = any_entry.get_sync_info("m") is not None

        # write("pair", pair)
        # print("pair.local", pair.local)
        # print("pair.remote", pair.remote)

        write(
            (
                VT_ERASE_LINE
                + ansi_code("Fore.LIGHTRED_EX")
                + "CONFLICT: {!r} was modified on both targets since last sync ({})."
                + ansi_code("Style.RESET_ALL")
            ).format(any_entry.get_rel_path(), _ts(any_entry.get_sync_info("u")))
        )
        if has_meta:
            write(
                "    Original modification time: {}, size: {:,d} bytes.".format(
                    _ts(any_entry.get_sync_info("m")), any_entry.get_sync_info("s")
                )
            )
        else:
            write("    (No meta data available.)")

        write("    Local:  {}".format(pair.local.as_string() if pair.local else "n.a."))
        write(
            "    Remote: {}".format(
                pair.remote.as_string(pair.local) if pair.remote else "n.a."
            )
        )
Ejemplo n.º 2
0
    def _log_action(self, action, status, symbol, entry, min_level=3):
        if self.verbose < min_level:
            return

        if len(symbol) > 1 and symbol[0] in (">", "<"):
            symbol = (
                " " + symbol
            )  # make sure direction characters are aligned at 2nd column

        color = ""
        final = ""
        if not self.options.get("no_color"):
            # CM = self.COLOR_MAP
            # color = CM.get((action, status),
            #                CM.get(("*", status),
            #                       CM.get((action, "*"),
            #                              "")))
            if action in ("copy", "restore"):
                if "<" in symbol:
                    if status == "new":
                        color = ansi_code("Fore.GREEN") + ansi_code(
                            "Style.BRIGHT")
                    else:
                        color = ansi_code("Fore.GREEN")
                else:
                    if status == "new":
                        color = ansi_code("Fore.CYAN") + ansi_code(
                            "Style.BRIGHT")
                    else:
                        color = ansi_code("Fore.CYAN")
            elif action == "delete":
                color = ansi_code("Fore.RED")
            elif status == "conflict":
                color = ansi_code("Fore.LIGHTRED_EX")
            elif action == "skip" or status == "equal" or status == "visit":
                color = ansi_code("Fore.LIGHTBLACK_EX")

            final = ansi_code("Style.RESET_ALL")

        if colorama:
            # Clear line"ESC [ mode K" mode 0:to-right, 2:all
            final += colorama.ansi.clear_line(0)
        else:
            final += " " * 10
        prefix = ""
        if self.dry_run:
            prefix = DRY_RUN_PREFIX

        if action and status:
            tag = ("{} {}".format(action, status)).upper()
        else:
            assert status
            tag = ("{}".format(status)).upper()

        name = entry.get_rel_path()
        if entry.is_dir():
            name = "[{}]".format(name)

        write("{}{}{:<16} {:^3} {}{}".format(prefix, color, tag, symbol, name,
                                             final))
Ejemplo n.º 3
0
    def _unlock(self, closing=False):
        """Remove lock file to the target root folder.

        """
        # write("_unlock", closing)
        try:
            if self.cur_dir != self.root_dir:
                if closing:
                    write(
                        "Changing to ftp root folder to remove lock file: {}".
                        format(self.root_dir))
                    self.cwd(self.root_dir)
                else:
                    write_error(
                        "Could not remove lock file, because CWD != ftp root: {}"
                        .format(self.cur_dir))
                    return

            if self.lock_data is False:
                if self.get_option("verbose", 3) >= 4:
                    write("Skip remove lock file (was not written).")
            else:
                # direct delete, without updating metadata or checking for target access:
                self.ftp.delete(DirMetadata.LOCK_FILE_NAME)
                # self.remove_file(DirMetadata.LOCK_FILE_NAME)

            self.lock_data = None
        except Exception as e:
            write_error("Could not remove lock file: {}".format(e))
            raise
Ejemplo n.º 4
0
    def _rmdir_impl(self, dir_name, keep_root_folder=False, predicate=None):
        # FTP does not support deletion of non-empty directories.
        self.check_write(dir_name)
        names = []
        nlst_res = self.ftp.nlst(dir_name)
        # write("rmdir(%s): %s" % (dir_name, nlst_res))
        for name in nlst_res:
            if "/" in name:
                name = os.path.basename(name)
            if name in (".", ".."):
                continue
            if predicate and not predicate(name):
                continue
            names.append(name)

        if len(names) > 0:
            self.ftp.cwd(dir_name)
            try:
                for name in names:
                    try:
                        # try to delete this as a file
                        self.ftp.delete(name)
                    except ftplib.all_errors as _e:
                        write(
                            "    ftp.delete({}) failed: {}, trying rmdir()...".
                            format(name, _e))
                        # assume <name> is a folder
                        self.rmdir(name)
            finally:
                if dir_name != ".":
                    self.ftp.cwd("..")
#        write("ftp.rmd(%s)..." % (dir_name, ))
        if not keep_root_folder:
            self.ftp.rmd(dir_name)
        return
Ejemplo n.º 5
0
    def test_logging(self):
        import logging
        import logging.handlers
        import os

        # Create and use a custom logger
        custom_logger = logging.getLogger("pyftpsync_test")
        log_path = os.path.join(PYFTPSYNC_TEST_FOLDER, "pyftpsync.log")
        handler = logging.handlers.WatchedFileHandler(log_path)
        # formatter = logging.Formatter(logging.BASIC_FORMAT)
        formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
        handler.setFormatter(formatter)
        custom_logger.addHandler(handler)
        set_pyftpsync_logger(custom_logger)

        custom_logger.setLevel(logging.DEBUG)
        print("print 1")
        write("write info 1")
        write_error("write error 1")

        custom_logger.setLevel(logging.WARNING)
        write("write info 2")
        write_error("write error 2")

        handler.flush()
        log_data = read_test_file("pyftpsync.log")
        assert "print 1" not in log_data
        assert "write info 1" in log_data
        assert "write error 1" in log_data
        assert "write info 2" not in log_data, "Loglevel honored"
        assert "write error 2" in log_data
        # Cleanup properly (log file would be locked otherwise)
        custom_logger.removeHandler(handler)
        handler.close()
Ejemplo n.º 6
0
 def _probe_lock_file(self, reported_mtime):
     """Called by get_dir"""
     delta = reported_mtime - self.lock_data["lock_time"]
     # delta2 = reported_mtime - self.lock_write_time
     self.server_time_ofs = delta
     if self.get_option("verbose", 3) >= 4:
         write("Server time offset: {:.2f} seconds.".format(delta))
Ejemplo n.º 7
0
 def _probe_lock_file(self, reported_mtime):
     """Called by get_dir"""
     delta = reported_mtime - self.lock_data["lock_time"]
     # delta2 = reported_mtime - self.lock_write_time
     self.server_time_ofs = delta
     if self.get_option("verbose", 3) >= 4:
         write("Server time offset: {:.2f} seconds.".format(delta))
Ejemplo n.º 8
0
 def close(self):
     if not self.connected:
         return
     if self.get_option("verbose", 3) >= 5:
         write("Closing target {}.".format(self))
     self.connected = False
     self.readonly = False  # issue #20
     self._rlock.release()
Ejemplo n.º 9
0
    def __init__(
        self,
        path,
        host,
        port=0,
        username=None,
        password=None,
        tls=False,
        timeout=None,
        extra_opts=None,
    ):
        """Create FTP target with host, initial path, optional credentials and options.

        Args:
            path (str): root path on FTP server, relative to *host*
            host (str): hostname of FTP server
            port (int): FTP port (defaults to 21)
            username (str):
            password (str):
            tls (bool): encrypt the connection using TLS (Python 2.7/3.2+)
            timeout (int): the timeout to set against the ftp socket (seconds)
            extra_opts (dict):
        """
        self.encoding = _get_encoding_opt(None, extra_opts, "utf-8")
        # path = self.to_unicode(path)
        path = path or "/"
        assert compat.is_native(path)
        super(FtpTarget, self).__init__(path, extra_opts)
        if tls:
            try:
                self.ftp = ftplib.FTP_TLS()
            except AttributeError:
                write("Python 2.7/3.2+ required for FTPS (TLS).")
                raise
        else:
            self.ftp = ftplib.FTP()
        self.ftp.set_debuglevel(self.get_option("ftp_debug", 0))
        self.host = host
        self.port = port or 0
        self.username = username
        self.password = password
        self.tls = tls
        self.timeout = timeout
        #: dict: written to ftp target root folder before synchronization starts.
        #: set to False, if write failed. Default: None
        self.lock_data = None
        self.lock_write_time = None
        self.feat_response = None
        self.syst_response = None
        self.is_unix = None
        #: True if server reports FEAT UTF8
        self.support_utf8 = None
        #: Time difference between <local upload time> and the mtime that the server reports afterwards.
        #: The value is added to the 'u' time stored in meta data.
        #: (This is only a rough estimation, derived from the lock-file.)
        self.server_time_ofs = None
        self.ftp_socket_connected = False
        self.support_set_time = False
Ejemplo n.º 10
0
    def __init__(
        self,
        path,
        host,
        port=0,
        username=None,
        password=None,
        tls=False,
        timeout=None,
        extra_opts=None,
    ):
        """Create FTP target with host, initial path, optional credentials and options.

        Args:
            path (str): root path on FTP server, relative to *host*
            host (str): hostname of FTP server
            port (int): FTP port (defaults to 21)
            username (str):
            password (str):
            tls (bool): encrypt the connection using TLS (Python 2.7/3.2+)
            timeout (int): the timeout to set against the ftp socket (seconds)
            extra_opts (dict):
        """
        self.encoding = _get_encoding_opt(None, extra_opts, "utf-8")
        # path = self.to_unicode(path)
        path = path or "/"
        assert is_native(path)
        super(FTPTarget, self).__init__(path, extra_opts)
        if tls:
            try:
                self.ftp = ftplib.FTP_TLS()
            except AttributeError:
                write("Python 2.7/3.2+ required for FTPS (TLS).")
                raise
        else:
            self.ftp = ftplib.FTP()
        self.ftp.set_debuglevel(self.get_option("ftp_debug", 0))
        self.host = host
        self.port = port or 0
        self.username = username
        self.password = password
        self.tls = tls
        self.timeout = timeout
        #: dict: written to ftp target root folder before synchronization starts.
        #: set to False, if write failed. Default: None
        self.lock_data = None
        self.lock_write_time = None
        self.feat_response = None
        self.syst_response = None
        self.is_unix = None
        #: True if server reports FEAT UTF8
        self.support_utf8 = None
        #: Time difference between <local upload time> and the mtime that the server reports afterwards.
        #: The value is added to the 'u' time stored in meta data.
        #: (This is only a rough estimation, derived from the lock-file.)
        self.server_time_ofs = None
        self.ftp_socket_connected = False
        self.support_set_time = False
Ejemplo n.º 11
0
    def _log_action(self, action, status, symbol, entry, min_level=3):
        if self.verbose < min_level:
            return

        if len(symbol) > 1 and symbol[0] in (">", "<"):
            symbol = (
                " " + symbol
            )  # make sure direction characters are aligned at 2nd column

        color = ""
        final = ""
        if not self.options.get("no_color"):
            # CM = self.COLOR_MAP
            # color = CM.get((action, status),
            #                CM.get(("*", status),
            #                       CM.get((action, "*"),
            #                              "")))
            if action in ("copy", "restore"):
                if "<" in symbol:
                    if status == "new":
                        color = ansi_code("Fore.GREEN") + ansi_code("Style.BRIGHT")
                    else:
                        color = ansi_code("Fore.GREEN")
                else:
                    if status == "new":
                        color = ansi_code("Fore.CYAN") + ansi_code("Style.BRIGHT")
                    else:
                        color = ansi_code("Fore.CYAN")
            elif action == "delete":
                color = ansi_code("Fore.RED")
            elif status == "conflict":
                color = ansi_code("Fore.LIGHTRED_EX")
            elif action == "skip" or status == "equal" or status == "visit":
                color = ansi_code("Fore.LIGHTBLACK_EX")

            final = ansi_code("Style.RESET_ALL")

        if colorama:
            # Clear line"ESC [ mode K" mode 0:to-right, 2:all
            final += colorama.ansi.clear_line(0)
        else:
            final += " " * 10
        prefix = ""
        if self.dry_run:
            prefix = DRY_RUN_PREFIX

        if action and status:
            tag = ("{} {}".format(action, status)).upper()
        else:
            assert status
            tag = ("{}".format(status)).upper()

        name = entry.get_rel_path()
        if entry.is_dir():
            name = "[{}]".format(name)

        write("{}{}{:<16} {:^3} {}{}".format(prefix, color, tag, symbol, name, final))
Ejemplo n.º 12
0
    def flush(self):
        """Write self to .pyftpsync-meta.json."""
        # We DO write meta files even on read-only targets, but not in dry-run mode
        # if self.target.readonly:
        #     write("DirMetadata.flush(%s): read-only; nothing to do" % self.target)
        #     return
        assert self.path == self.target.cur_dir
        if self.target.dry_run:
            # write("DirMetadata.flush(%s): dry-run; nothing to do" % self.target)
            pass

        elif self.was_read and len(self.list) == 0 and len(
                self.peer_sync) == 0:
            write("Remove empty meta data file: {}".format(self.target))
            self.target.remove_file(self.filename)

        elif not self.modified_list and not self.modified_sync:
            # write("DirMetadata.flush(%s): unmodified; nothing to do" % self.target)
            pass

        else:
            self.dir[
                "_disclaimer"] = "Generated by https://github.com/mar10/pyftpsync"
            self.dir["_time_str"] = pretty_stamp(time.time())
            self.dir["_file_version"] = self.VERSION
            self.dir["_version"] = __version__
            self.dir["_time"] = time.mktime(time.gmtime())

            # We always save utf-8 encoded.
            # `ensure_ascii` would escape all bytes >127 as `\x12` or `\u1234`,
            # which makes it hard to read, so we set it to false.
            # `sort_keys` converts binary keys to unicode using utf-8, so we
            # must make sure that we don't pass cp1225 or other encoded data.
            data = self.dir
            opts = {"indent": 4, "sort_keys": True, "ensure_ascii": False}

            # if compat.PY2:
            #     # The `encoding` arg defaults to utf-8 on Py2 and was removed in Py3
            #     # opts["encoding"] = "utf-8"
            #     # Python 2 has problems with mixed keys (str/unicode)
            #     data = decode_dict_keys(data, "utf-8")

            if not self.PRETTY:
                opts["indent"] = None
                opts["separators"] = (",", ":")

            s = json.dumps(data, **opts)

            self.target.write_text(self.filename, s)
            if self.target.synchronizer:
                self.target.synchronizer._inc_stat("meta_bytes_written",
                                                   len(s))

        self.modified_list = False
        self.modified_sync = False
Ejemplo n.º 13
0
 def on_error(self, e, pair):
     """Called for pairs that don't match `match` and `exclude` filters."""
     RED = ansi_code("Fore.LIGHTRED_EX")
     R = ansi_code("Style.RESET_ALL")
     # any_entry = pair.any_entry
     write((RED + "ERROR: {}\n    {}" + R).format(e, pair))
     # Return True to ignore this error (instead of raising and terminating the app)
     if "[Errno 92] Illegal byte sequence" in "{}".format(e) and compat.PY2:
         write(RED + "This _may_ be solved by using Python 3." + R)
         # return True
     return False
Ejemplo n.º 14
0
 def on_error(self, e, pair):
     """Called for pairs that don't match `match` and `exclude` filters."""
     RED = ansi_code("Fore.LIGHTRED_EX")
     R = ansi_code("Style.RESET_ALL")
     # any_entry = pair.any_entry
     write((RED + "ERROR: {}\n    {}" + R).format(e, pair))
     # Return True to ignore this error (instead of raising and terminating the app)
     if "[Errno 92] Illegal byte sequence" in "{}".format(e) and compat.PY2:
         write(RED + "This _may_ be solved by using Python 3." + R)
         # return True
     return False
Ejemplo n.º 15
0
    def flush(self):
        """Write self to .pyftpsync-meta.json."""
        # We DO write meta files even on read-only targets, but not in dry-run mode
        # if self.target.readonly:
        #     write("DirMetadata.flush(%s): read-only; nothing to do" % self.target)
        #     return
        assert self.path == self.target.cur_dir
        if self.target.dry_run:
            # write("DirMetadata.flush(%s): dry-run; nothing to do" % self.target)
            pass

        elif self.was_read and len(self.list) == 0 and len(self.peer_sync) == 0:
            write("Remove empty meta data file: {}".format(self.target))
            self.target.remove_file(self.filename)

        elif not self.modified_list and not self.modified_sync:
            # write("DirMetadata.flush(%s): unmodified; nothing to do" % self.target)
            pass

        else:
            self.dir["_disclaimer"] = "Generated by https://github.com/mar10/pyftpsync"
            self.dir["_time_str"] = pretty_stamp(time.time())
            self.dir["_file_version"] = self.VERSION
            self.dir["_version"] = __version__
            self.dir["_time"] = time.mktime(time.gmtime())

            # We always save utf-8 encoded.
            # `ensure_ascii` would escape all bytes >127 as `\x12` or `\u1234`,
            # which makes it hard to read, so we set it to false.
            # `sort_keys` converts binary keys to unicode using utf-8, so we
            # must make sure that we don't pass cp1225 or other encoded data.
            data = self.dir
            opts = {"indent": 4, "sort_keys": True, "ensure_ascii": False}

            if compat.PY2:
                # The `encoding` arg defaults to utf-8 on Py2 and was removed in Py3
                # opts["encoding"] = "utf-8"
                # Python 2 has problems with mixed keys (str/unicode)
                data = decode_dict_keys(data, "utf-8")

            if not self.PRETTY:
                opts["indent"] = None
                opts["separators"] = (",", ":")

            s = json.dumps(data, **opts)

            self.target.write_text(self.filename, s)
            if self.target.synchronizer:
                self.target.synchronizer._inc_stat("meta_bytes_written", len(s))

        self.modified_list = False
        self.modified_sync = False
Ejemplo n.º 16
0
    def run(self):
        start = time.time()

        info_strings = self.get_info_strings()
        if self.verbose >= 3:
            write(
                "{} {}\n{:>20} {}".format(
                    info_strings[0].capitalize(),
                    self.local.get_base_name(),
                    info_strings[1],
                    self.remote.get_base_name(),
                )
            )
            write(
                "Encoding local: {}, remote: {}".format(
                    self.local.encoding, self.remote.encoding
                )
            )

        try:
            self.local.synchronizer = self.remote.synchronizer = self
            self.local.peer = self.remote
            self.remote.peer = self.local

            if self.dry_run:
                self.local.readonly = True
                self.local.dry_run = True
                self.remote.readonly = True
                self.remote.dry_run = True

            if not self.local.connected:
                self.local.open()
            if not self.remote.connected:
                self.remote.open()

            res = self._sync_dir()
        finally:
            self.local.synchronizer = self.remote.synchronizer = None
            self.local.peer = self.remote.peer = None
            self.close()

        stats = self._stats
        stats["elap_secs"] = time.time() - start
        stats["elap_str"] = "%0.2f sec" % stats["elap_secs"]

        def _add(rate, size, time):
            if stats.get(time) and stats.get(size):
                stats[rate] = "%0.2f kB/sec" % (0.001 * stats[size] / stats[time])

        _add("upload_rate_str", "upload_bytes_written", "upload_write_time")
        _add("download_rate_str", "download_bytes_written", "download_write_time")
        return res
Ejemplo n.º 17
0
    def run(self):
        start = time.time()

        info_strings = self.get_info_strings()
        if self.verbose >= 3:
            write(
                "{} {}\n{:>20} {}".format(
                    info_strings[0].capitalize(),
                    self.local.get_base_name(),
                    info_strings[1],
                    self.remote.get_base_name(),
                )
            )
            write(
                "Encoding local: {}, remote: {}".format(
                    self.local.encoding, self.remote.encoding
                )
            )

        try:
            self.local.synchronizer = self.remote.synchronizer = self
            self.local.peer = self.remote
            self.remote.peer = self.local

            if self.dry_run:
                self.local.readonly = True
                self.local.dry_run = True
                self.remote.readonly = True
                self.remote.dry_run = True

            if not self.local.connected:
                self.local.open()
            if not self.remote.connected:
                self.remote.open()

            res = self._sync_dir()
        finally:
            self.local.synchronizer = self.remote.synchronizer = None
            self.local.peer = self.remote.peer = None
            self.close()

        stats = self._stats
        stats["elap_secs"] = time.time() - start
        stats["elap_str"] = "{:0.2f} sec".format(stats["elap_secs"])

        def _add(rate, size, time):
            if stats.get(time) and stats.get(size):
                stats[rate] = "{:0.2f} kB/sec".format(0.001 * stats[size] / stats[time])

        _add("upload_rate_str", "upload_bytes_written", "upload_write_time")
        _add("download_rate_str", "download_bytes_written", "download_write_time")
        return res
Ejemplo n.º 18
0
    def _compare_file(self, local, remote):
        """Byte compare two files (early out on first difference)."""
        assert isinstance(local, FileEntry) and isinstance(remote, FileEntry)

        if not local or not remote:
            write("    Files cannot be compared ({} != {}).".format(local, remote))
            return False
        elif local.size != remote.size:
            write(
                "    Files are different (size {:,d} != {:,d}).".format(
                    local.size, remote.size
                )
            )
            return False

        with local.target.open_readable(
            local.name
        ) as fp_src, remote.target.open_readable(remote.name) as fp_dest:
            res, ofs = byte_compare(fp_src, fp_dest)

        if not res:
            write("    Files are different at offset {:,d}.".format(ofs))
        else:
            write("    Files are equal.")
        return res
Ejemplo n.º 19
0
    def _compare_file(self, local, remote):
        """Byte compare two files (early out on first difference)."""
        assert isinstance(local, FileEntry) and isinstance(remote, FileEntry)

        if not local or not remote:
            write("    Files cannot be compared ({} != {}).".format(local, remote))
            return False
        elif local.size != remote.size:
            write(
                "    Files are different (size {:,d} != {:,d}).".format(
                    local.size, remote.size
                )
            )
            return False

        with local.target.open_readable(
            local.name
        ) as fp_src, remote.target.open_readable(remote.name) as fp_dest:
            res, ofs = byte_compare(fp_src, fp_dest)

        if not res:
            write("    Files are different at offset {:,d}.".format(ofs))
        else:
            write("    Files are equal.")
        return res
Ejemplo n.º 20
0
    def read(self):
        """Initialize self from .pyftpsync-meta.json file."""
        assert self.path == self.target.cur_dir
        try:
            self.modified_list = False
            self.modified_sync = False
            is_valid_file = False

            s = self.target.read_text(self.filename)
            # print("s", s)
            if self.target.synchronizer:
                self.target.synchronizer._inc_stat("meta_bytes_read", len(s))
            self.was_read = True  # True if a file exists (even invalid)
            self.dir = json.loads(s)
            # import pprint
            # print("dir")
            # print(pprint.pformat(self.dir))
            self.dir = make_native_dict_keys(self.dir)
            # print(pprint.pformat(self.dir))
            self.list = self.dir["mtimes"]
            self.peer_sync = self.dir["peer_sync"]
            is_valid_file = True
            # write"DirMetadata: read(%s)" % (self.filename, ), self.dir)
        # except IncompatibleMetadataVersion:
        #     raise  # We want version errors to terminate the app
        except Exception as e:
            write_error("Could not read meta info {}: {!r}".format(self, e))

        # If the version is incompatible, we stop, unless:
        # if --migrate is set, we simply ignore this file (and probably replace it
        # with a current version)
        if is_valid_file and self.dir.get("_file_version", 0) != self.VERSION:
            if not self.target or not self.target.get_option("migrate"):
                raise IncompatibleMetadataVersion(
                    "Invalid meta data version: {} (expected {}).\n"
                    "Consider passing --migrate to discard old data.".format(
                        self.dir.get("_file_version"), self.VERSION
                    )
                )
            #
            write(
                "Migrating meta data version from {} to {} (discarding old): {}".format(
                    self.dir.get("_file_version"), self.VERSION, self.filename
                )
            )
            self.list = {}
            self.peer_sync = {}

        return
Ejemplo n.º 21
0
 def override_operation(self, operation, reason):
     """Re-Classify entry pair."""
     # prev_class = (self.local_classification, self.remote_classification)
     prev_op = self.operation
     assert operation != prev_op
     assert operation in PAIR_OPERATIONS
     if "classify" in DEBUG_FLAGS:
         write(
             "override_operation {} -> {} (reason: '{}')".format(
                 self, operation, reason
             ),
             debug=True,
         )
     self.operation = operation
     self.re_class_reason = reason
Ejemplo n.º 22
0
 def override_operation(self, operation, reason):
     """Re-Classify entry pair."""
     prev_class = (self.local_classification, self.remote_classification)
     prev_op = self.operation
     assert operation != prev_op
     assert operation in PAIR_OPERATIONS
     if self.any_entry.target.synchronizer.verbose > 3:
         write(
             "override_operation({}, {}) -> {} ({})".format(
                 prev_class, prev_op, operation, reason
             ),
             debug=True,
         )
     self.operation = operation
     self.re_class_reason = reason
Ejemplo n.º 23
0
 def on_error(self, exc, pair):
     """Called for pairs that don't match `match` and `exclude` filters."""
     # any_entry = pair.any_entry
     msg = "{red}ERROR: {exc}\n    {pair}{reset}".format(
         exc=exc,
         pair=pair,
         red=ansi_code("Fore.LIGHTRED_EX"),
         reset=ansi_code("Style.RESET_ALL"),
     )
     write(msg)
     # Return True to ignore this error (instead of raising and terminating the app)
     # if "[Errno 92] Illegal byte sequence" in "{}".format(e) and compat.PY2:
     #     write(RED + "This _may_ be solved by using Python 3." + R)
     #     # return True
     return False
Ejemplo n.º 24
0
    def read(self):
        """Initialize self from .pyftpsync-meta.json file."""
        assert self.path == self.target.cur_dir
        try:
            self.modified_list = False
            self.modified_sync = False
            is_valid_file = False

            s = self.target.read_text(self.filename)
            # print("s", s)
            if self.target.synchronizer:
                self.target.synchronizer._inc_stat("meta_bytes_read", len(s))
            self.was_read = True  # True if a file exists (even invalid)
            self.dir = json.loads(s)
            # import pprint
            # print("dir")
            # print(pprint.pformat(self.dir))
            self.dir = make_native_dict_keys(self.dir)
            # print(pprint.pformat(self.dir))
            self.list = self.dir["mtimes"]
            self.peer_sync = self.dir["peer_sync"]
            is_valid_file = True
            # write"DirMetadata: read(%s)" % (self.filename, ), self.dir)
        # except IncompatibleMetadataVersion:
        #     raise  # We want version errors to terminate the app
        except Exception as e:
            write_error("Could not read meta info {}: {!r}".format(self, e))

        # If the version is incompatible, we stop, unless:
        # if --migrate is set, we simply ignore this file (and probably replace it
        # with a current version)
        if is_valid_file and self.dir.get("_file_version", 0) != self.VERSION:
            if not self.target or not self.target.get_option("migrate"):
                raise IncompatibleMetadataVersion(
                    "Invalid meta data version: {} (expected {}).\n"
                    "Consider passing --migrate to discard old data.".format(
                        self.dir.get("_file_version"), self.VERSION))
            #
            write(
                "Migrating meta data version from {} to {} (i.e. discarding it): {}"
                .format(self.dir.get("_file_version"), self.VERSION,
                        self.filename))
            self.list = {}
            self.peer_sync = {}
            # This will remove .pyftpsync-meta.json:
            self.flush()

        return
Ejemplo n.º 25
0
    def __init__(self,
                 path,
                 host,
                 port=0,
                 username=None,
                 password=None,
                 tls=False,
                 timeout=None,
                 extra_opts=None):
        """Create FTP target with host, initial path, optional credentials and options.

        Args:
            path (str): root path on FTP server, relative to *host*
            host (str): hostname of FTP server
            port (int): FTP port (defaults to 21)
            username (str):
            password (str):
            tls (bool): encrypt the connection using TLS (Python 2.7/3.2+)
            timeout (int): the timeout to set against the ftp socket (seconds)
            extra_opts (dict):
        """
        path = path or "/"
        super(FtpTarget, self).__init__(path, extra_opts)
        if tls:
            try:
                self.ftp = ftplib.FTP_TLS()
            except AttributeError:
                write("Python 2.7/3.2+ required for FTPS (TLS).")
                raise
        else:
            self.ftp = ftplib.FTP()
        self.ftp.set_debuglevel(self.get_option("ftp_debug", 0))
        self.host = host
        self.port = port or 0
        self.username = username
        self.password = password
        self.tls = tls
        self.timeout = timeout
        #: dict: written to ftp target root folder before synchronization starts.
        #: set to False, if write failed. Default: None
        self.lock_data = None
        self.is_unix = None
        self.time_zone_ofs = None
        self.clock_ofs = None
        self.ftp_socket_connected = False
        self.support_set_time = False
Ejemplo n.º 26
0
    def _unlock(self, closing=False):
        """Remove lock file to the target root folder.

        """
        # write("_unlock", closing)
        try:
            if self.cur_dir != self.root_dir:
                if closing:
                    write(
                        "Changing to ftp root folder to remove lock file: {}".format(
                            self.root_dir
                        )
                    )
                    self.cwd(self.root_dir)
                else:
                    write_error(
                        "Could not remove lock file, because CWD != ftp root: {}".format(
                            self.cur_dir
                        )
                    )
                    return

            if self.lock_data is False:
                if self.get_option("verbose", 3) >= 4:
                    write("Skip remove lock file (was not written).")
            else:
                # direct delete, without updating metadata or checking for target access:
                try:
                    self.ftp.delete(DirMetadata.LOCK_FILE_NAME)
                    # self.remove_file(DirMetadata.LOCK_FILE_NAME)
                except Exception as e:
                    # I have seen '226 Closing data connection' responses here,
                    # probably when a previous command threw another error.
                    # However here, 2xx response should be Ok(?):
                    # A 226 reply code is sent by the server before closing the
                    # data connection after successfully processing the previous client command
                    if e.args[0][:3] == "226":
                        write_error("Ignoring 226 response for ftp.delete() lockfile")
                    else:
                        raise

            self.lock_data = None
        except Exception as e:
            write_error("Could not remove lock file: {}".format(e))
            raise
Ejemplo n.º 27
0
    def _ftp_pwd(self):
        """Variant of `self.ftp.pwd()` that supports encoding-fallback.

        Returns:
            Current working directory as native string.
        """
        try:
            return self.ftp.pwd()
        except UnicodeEncodeError:
            if compat.PY2 or self.ftp.encoding != "utf-8":
                raise  # should not happen, since Py2 does not try to encode
            # TODO: this is NOT THREAD-SAFE!
            prev_encoding = self.ftp.encoding
            try:
                write("ftp.pwd() failed with utf-8: trying Cp1252...", warning=True)
                return self.ftp.pwd()
            finally:
                self.ftp.encoding = prev_encoding
Ejemplo n.º 28
0
    def classify(self, peer_dir_meta):
        """Classify this entry as 'new', 'unmodified', or 'modified'."""
        assert self.classification is None
        peer_entry_meta = None
        if peer_dir_meta:
            # Metadata is generally available, so we can detect 'new' or 'modified'
            peer_entry_meta = peer_dir_meta.get(self.name, False)

            if self.is_dir():
                # Directories are considered 'unmodified' (would require deep traversal
                # to check otherwise)
                if peer_entry_meta:
                    self.classification = "unmodified"
                else:
                    self.classification = "new"
            elif peer_entry_meta:
                # File entries can be classified as modified/unmodified
                self.ps_size = peer_entry_meta.get("s")
                self.ps_mtime = peer_entry_meta.get("m")
                self.ps_utime = peer_entry_meta.get("u")
                if (
                    self.size == self.ps_size
                    and FileEntry._eps_compare(self.mtime, self.ps_mtime) == 0
                ):
                    self.classification = "unmodified"
                else:
                    self.classification = "modified"
            else:
                # A new file entry
                self.classification = "new"
        else:
            # No metadata available:
            if self.is_dir():
                # Directories are considered 'unmodified' (would require deep traversal
                # to check otherwise)
                self.classification = "unmodified"
            else:
                # That's all we know, but EntryPair.classify() may adjust this
                self.classification = "existing"

        if PRINT_CLASSIFICATIONS:
            write("classify {}".format(self))
        assert self.classification in ENTRY_CLASSIFICATIONS
        return self.classification
Ejemplo n.º 29
0
    def _ftp_pwd(self):
        """Variant of `self.ftp.pwd()` that supports encoding-fallback.

        Returns:
            Current working directory as native string.
        """
        try:
            return self.ftp.pwd()
        except UnicodeEncodeError:
            if self.ftp.encoding != "utf-8":
                raise  # should not happen, since Py2 does not try to encode
            # TODO: this is NOT THREAD-SAFE!
            prev_encoding = self.ftp.encoding
            try:
                write("ftp.pwd() failed with utf-8: trying Cp1252...",
                      warning=True)
                return self.ftp.pwd()
            finally:
                self.ftp.encoding = prev_encoding
Ejemplo n.º 30
0
    def _unlock(self, closing=False):
        """Remove lock file to the target root folder.

        """
        # write("_unlock", closing)
        try:
            if self.cur_dir != self.root_dir:
                if closing:
                    write(
                        "Changing to ftp root folder to remove lock file: {}".
                        format(self.root_dir))
                    self.cwd(self.root_dir)
                else:
                    write_error(
                        "Could not remove lock file, because CWD != ftp root: {}"
                        .format(self.cur_dir))
                    return

            if self.lock_data is False:
                if self.get_option("verbose", 3) >= 4:
                    write("Skip remove lock file (was not written).")
            else:
                # direct delete, without updating metadata or checking for target access:
                try:
                    self.ftp.delete(DirMetadata.LOCK_FILE_NAME)
                    # self.remove_file(DirMetadata.LOCK_FILE_NAME)
                except Exception as e:
                    # I have seen '226 Closing data connection' responses here,
                    # probably when a previous command threw another error.
                    # However here, 2xx response should be Ok(?):
                    # A 226 reply code is sent by the server before closing the
                    # data connection after successfully processing the previous client command
                    if e.args[0][:3] == "226":
                        write_error(
                            "Ignoring 226 response for ftp.delete() lockfile")
                    else:
                        raise

            self.lock_data = None
        except Exception as e:
            write_error("Could not remove lock file: {}".format(e))
            raise
Ejemplo n.º 31
0
    def _rmdir_impl(self, dir_name, keep_root_folder=False, predicate=None):
        # FTP does not support deletion of non-empty directories.
        assert compat.is_native(dir_name)
        self.check_write(dir_name)
        names = []
        nlst_res = self._ftp_nlst(dir_name)
        # nlst_res = self.ftp.nlst(dir_name)
        # write("rmdir(%s): %s" % (dir_name, nlst_res))
        for name in nlst_res:
            # name = self.re_encode_to_native(name)
            if "/" in name:
                name = os.path.basename(name)
            if name in (".", ".."):
                continue
            if predicate and not predicate(name):
                continue
            names.append(name)

        if len(names) > 0:
            self.ftp.cwd(dir_name)
            try:
                for name in names:
                    try:
                        # try to delete this as a file
                        self.ftp.delete(name)
                    except ftplib.all_errors as _e:
                        write(
                            "    ftp.delete({}) failed: {}, trying rmdir()...".format(
                                name, _e
                            )
                        )
                        # assume <name> is a folder
                        self.rmdir(name)
            finally:
                if dir_name != ".":
                    self.ftp.cwd("..")
        #        write("ftp.rmd(%s)..." % (dir_name, ))
        if not keep_root_folder:
            self.ftp.rmd(dir_name)
        return
Ejemplo n.º 32
0
    def classify(self, peer_dir_meta):
        """Classify entry pair."""
        assert self.operation is None
        # Note: We pass False if the entry is not listed in the metadata.
        #       We pass None if we don't have metadata all.
        peer_entry_meta = peer_dir_meta.get(self.name, False) if peer_dir_meta else None

        if self.local:
            self.local.classify(peer_dir_meta)
            self.local_classification = self.local.classification
        elif peer_entry_meta:
            self.local_classification = "deleted"
        else:
            self.local_classification = "missing"

        if self.remote:
            self.remote.classify(peer_dir_meta)
            self.remote_classification = self.remote.classification
        elif peer_entry_meta:
            self.remote_classification = "deleted"
        else:
            self.remote_classification = "missing"

        c_pair = (self.local_classification, self.remote_classification)

        self.operation = operation_map.get(c_pair)
        if not self.operation:
            raise RuntimeError(
                "Undefined operation for pair classification {}".format(c_pair)
            )
        if "classify" in DEBUG_FLAGS:
            write(
                "Classified pair {}, meta={}".format(self, peer_entry_meta),
                debug=True,
            )
        # if not entry.meta:
        # assert self.classification in PAIR_CLASSIFICATIONS
        assert self.operation in PAIR_OPERATIONS
        return self.operation
Ejemplo n.º 33
0
    def classify(self, peer_dir_meta):
        """Classify entry pair."""
        assert self.operation is None
        # write("CLASSIFIY", self, peer_dir_meta)
        # Note: We pass False if the entry is not listed in the metadata.
        #       We pass None if we don't have metadata all.
        peer_entry_meta = peer_dir_meta.get(self.name, False) if peer_dir_meta else None
        # write("=>", self, peer_entry_meta)
        if self.local:
            self.local.classify(peer_dir_meta)
            self.local_classification = self.local.classification
        elif peer_entry_meta:
            self.local_classification = "deleted"
        else:
            self.local_classification = "missing"

        if self.remote:
            self.remote.classify(peer_dir_meta)
            self.remote_classification = self.remote.classification
        elif peer_entry_meta:
            self.remote_classification = "deleted"
        else:
            self.remote_classification = "missing"

        c_pair = (self.local_classification, self.remote_classification)

        self.operation = operation_map.get(c_pair)
        if not self.operation:
            raise RuntimeError(
                "Undefined operation for pair classification {}".format(c_pair)
            )

        if PRINT_CLASSIFICATIONS:
            write("classify {}".format(self))
        # if not entry.meta:
        # assert self.classification in PAIR_CLASSIFICATIONS
        assert self.operation in PAIR_OPERATIONS
        return self.operation
Ejemplo n.º 34
0
    def test_logging(self):
        import logging
        import logging.handlers
        import os

        # Create and use a custom logger
        custom_logger = logging.getLogger("pyftpsync_test")
        log_path = os.path.join(PYFTPSYNC_TEST_FOLDER, "pyftpsync.log")
        handler = logging.handlers.WatchedFileHandler(log_path)
        # formatter = logging.Formatter(logging.BASIC_FORMAT)
        formatter = logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        )
        handler.setFormatter(formatter)
        custom_logger.addHandler(handler)
        set_pyftpsync_logger(custom_logger)

        custom_logger.setLevel(logging.DEBUG)
        print("print 1")
        write("write info 1")
        write_error("write error 1")

        custom_logger.setLevel(logging.WARNING)
        write("write info 2")
        write_error("write error 2")

        handler.flush()
        log_data = read_test_file("pyftpsync.log")
        assert "print 1" not in log_data
        assert "write info 1" in log_data
        assert "write error 1" in log_data
        assert "write info 2" not in log_data, "Loglevel honored"
        assert "write error 2" in log_data
        # Cleanup properly (log file would be locked otherwise)
        custom_logger.removeHandler(handler)
        handler.close()
Ejemplo n.º 35
0
    def _print_pair_diff(self, pair):
        RED = ansi_code("Fore.LIGHTRED_EX")
        # M = ansi_code("Style.BRIGHT") + ansi_code("Style.UNDERLINE")
        R = ansi_code("Style.RESET_ALL")

        any_entry = pair.any_entry

        has_meta = any_entry.get_sync_info("m") is not None

        # write("pair", pair)
        # print("pair.local", pair.local)
        # print("pair.remote", pair.remote)

        write(
            (
                VT_ERASE_LINE
                + RED
                + "CONFLICT: {!r} was modified on both targets since last sync ({})."
                + R
            ).format(any_entry.get_rel_path(), _ts(any_entry.get_sync_info("u")))
        )
        if has_meta:
            write(
                "    Original modification time: {}, size: {:,d} bytes.".format(
                    _ts(any_entry.get_sync_info("m")), any_entry.get_sync_info("s")
                )
            )
        else:
            write("    (No meta data available.)")

        write("    Local:  {}".format(pair.local.as_string() if pair.local else "n.a."))
        write(
            "    Remote: {}".format(
                pair.remote.as_string(pair.local) if pair.remote else "n.a."
            )
        )
Ejemplo n.º 36
0
        def _addline(status, line):
            # _ftp_retrlines_native() made sure that we always get `str` type  lines
            assert status in (0, 1, 2)
            assert compat.is_native(line)

            data, _, name = line.partition("; ")

            # print(status, name, u_name)
            if status == 1:
                write(
                    "WARNING: File name seems not to be {}; re-encoded from CP-1252:".format(
                        encoding
                    ),
                    name,
                )
            elif status == 2:
                write_error("File name is neither UTF-8 nor CP-1252 encoded:", name)

            res_type = size = mtime = unique = None
            fields = data.split(";")
            # https://tools.ietf.org/html/rfc3659#page-23
            # "Size" / "Modify" / "Create" / "Type" / "Unique" / "Perm" / "Lang"
            #   / "Media-Type" / "CharSet" / os-depend-fact / local-fact
            for field in fields:
                field_name, _, field_value = field.partition("=")
                field_name = field_name.lower()
                if field_name == "type":
                    res_type = field_value
                elif field_name in ("sizd", "size"):
                    size = int(field_value)
                elif field_name == "modify":
                    # Use calendar.timegm() instead of time.mktime(), because
                    # the date was returned as UTC
                    if "." in field_value:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S.%f")
                        )
                    else:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S")
                        )
                elif field_name == "unique":
                    unique = field_value

            entry = None
            if res_type == "dir":
                entry = DirectoryEntry(self, self.cur_dir, name, size, mtime, unique)
            elif res_type == "file":
                if name == DirMetadata.META_FILE_NAME:
                    # the meta-data file is silently ignored
                    local_var["has_meta"] = True
                elif (
                    name == DirMetadata.LOCK_FILE_NAME and self.cur_dir == self.root_dir
                ):
                    # this is the root lock file. compare reported mtime with
                    # local upload time
                    self._probe_lock_file(mtime)
                else:
                    entry = FileEntry(self, self.cur_dir, name, size, mtime, unique)
            elif res_type in ("cdir", "pdir"):
                pass
            else:
                write_error("Could not parse '{}'".format(line))
                raise NotImplementedError(
                    "MLSD returned unsupported type: {!r}".format(res_type)
                )

            if entry:
                entry_map[name] = entry
                entry_list.append(entry)
Ejemplo n.º 37
0
    def open(self):
        assert not self.ftp_socket_connected

        super(FtpTarget, self).open()

        options = self.get_options_dict()
        no_prompt = self.get_option("no_prompt", True)
        store_password = self.get_option("store_password", False)
        verbose = self.get_option("verbose", 3)

        self.ftp.set_debuglevel(self.get_option("ftp_debug", 0))

        # Optionally use FTP active mode (default: PASV) (issue #21)
        force_active = self.get_option("ftp_active", False)
        self.ftp.set_pasv(not force_active)

        if self.timeout:
            self.ftp.connect(self.host, self.port, self.timeout)
        else:
            # Py2.7 uses -999 as default for `timeout`, Py3 uses None
            self.ftp.connect(self.host, self.port)

        self.ftp_socket_connected = True

        if self.username is None or self.password is None:
            creds = get_credentials_for_url(
                self.host, options, force_user=self.username
            )
            if creds:
                self.username, self.password = creds

        while True:
            try:
                # Login (as 'anonymous' if self.username is undefined):
                self.ftp.login(self.username, self.password)
                if verbose >= 4:
                    write(
                        "Login as '{}'.".format(
                            self.username if self.username else "anonymous"
                        )
                    )
                break
            except ftplib.error_perm as e:
                # If credentials were passed, but authentication fails, prompt
                # for new password
                if not e.args[0].startswith("530"):
                    raise  # error other then '530 Login incorrect'
                write_error(
                    "Could not login to {}@{}: {}".format(self.username, self.host, e)
                )
                if no_prompt or not self.username:
                    raise
                creds = prompt_for_password(self.host, self.username)
                self.username, self.password = creds
                # Continue while-loop

        if self.tls:
            # Upgrade data connection to TLS.
            self.ftp.prot_p()

        try:
            self.syst_response = self.ftp.sendcmd("SYST")
            if verbose >= 5:
                write("SYST: '{}'.".format(self.syst_response.replace("\n", " ")))
            # self.is_unix = "unix" in resp.lower() # not necessarily true, better check with r/w tests
            # TODO: case sensitivity?
        except Exception as e:
            write("SYST command failed: '{}'".format(e))

        try:
            self.feat_response = self.ftp.sendcmd("FEAT")
            self.support_utf8 = "UTF8" in self.feat_response
            if verbose >= 5:
                write("FEAT: '{}'.".format(self.feat_response.replace("\n", " ")))
        except Exception as e:
            write("FEAT command failed: '{}'".format(e))

        if self.encoding == "utf-8":
            if not self.support_utf8 and verbose >= 4:
                write(
                    "Server does not list utf-8 as supported feature (using it anyway).",
                    warning=True,
                )

            try:
                # Announce our wish to use UTF-8 to the server as proposed here:
                # See https://tools.ietf.org/html/draft-ietf-ftpext-utf-8-option-00
                # Note: this RFC is inactive, expired, and failed on Strato
                self.ftp.sendcmd("OPTS UTF-8")
                if verbose >= 4:
                    write("Sent 'OPTS UTF-8'.")
            except Exception as e:
                if verbose >= 4:
                    write("Could not send 'OPTS UTF-8': '{}'".format(e), warning=True)

            try:
                # Announce our wish to use UTF-8 to the server as proposed here:
                # See https://tools.ietf.org/html/rfc2389
                # https://www.cerberusftp.com/phpBB3/viewtopic.php?t=2608
                # Note: this was accepted on Strato
                self.ftp.sendcmd("OPTS UTF8 ON")
                if verbose >= 4:
                    write("Sent 'OPTS UTF8 ON'.")
            except Exception as e:
                write("Could not send 'OPTS UTF8 ON': '{}'".format(e), warning=True)

        if hasattr(self.ftp, "encoding"):
            # Python 3 encodes using latin-1 by default(!)
            # (In Python 2 ftp.encoding does not exist, but ascii is used)
            if self.encoding != codecs.lookup(self.ftp.encoding).name:
                write(
                    "Setting FTP encoding to {} (was {}).".format(
                        self.encoding, self.ftp.encoding
                    )
                )
                self.ftp.encoding = self.encoding

        try:
            self.ftp.cwd(self.root_dir)
        except ftplib.error_perm as e:
            if not e.args[0].startswith("550"):
                raise  # error other then 550 No such directory'
            write_error(
                "Could not change directory to {} ({}): missing permissions?".format(
                    self.root_dir, e
                )
            )

        pwd = self.pwd()
        # pwd = self.to_unicode(pwd)
        if pwd != self.root_dir:
            raise RuntimeError(
                "Unable to navigate to working directory {!r} (now at {!r})".format(
                    self.root_dir, pwd
                )
            )

        self.cur_dir = pwd

        # self.ftp_initialized = True
        # Successfully authenticated: store password
        if store_password:
            save_password(self.host, self.username, self.password)

        self._lock()

        return
Ejemplo n.º 38
0
        def _addline(status, line):
            # _ftp_retrlines_native() made sure that we always get `str` type  lines
            assert status in (0, 1, 2)
            assert is_native(line)

            data, _, name = line.partition("; ")

            # print(status, name, u_name)
            if status == 1:
                write(
                    "WARNING: File name seems not to be {}; re-encoded from CP-1252:"
                    .format(encoding),
                    name,
                )
            elif status == 2:
                write_error("File name is neither UTF-8 nor CP-1252 encoded:",
                            name)

            res_type = size = mtime = unique = None
            fields = data.split(";")
            # https://tools.ietf.org/html/rfc3659#page-23
            # "Size" / "Modify" / "Create" / "Type" / "Unique" / "Perm" / "Lang"
            #   / "Media-Type" / "CharSet" / os-depend-fact / local-fact
            for field in fields:
                field_name, _, field_value = field.partition("=")
                field_name = field_name.lower()
                if field_name == "type":
                    res_type = field_value
                elif field_name in ("sizd", "size"):
                    size = int(field_value)
                elif field_name == "modify":
                    # Use calendar.timegm() instead of time.mktime(), because
                    # the date was returned as UTC
                    if "." in field_value:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S.%f"))
                    else:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S"))
                elif field_name == "unique":
                    unique = field_value

            entry = None
            if res_type == "dir":
                entry = DirectoryEntry(self, self.cur_dir, name, size, mtime,
                                       unique)
            elif res_type == "file":
                if name == DirMetadata.META_FILE_NAME:
                    # the meta-data file is silently ignored
                    local_var["has_meta"] = True
                elif (name == DirMetadata.LOCK_FILE_NAME
                      and self.cur_dir == self.root_dir):
                    # this is the root lock file. compare reported mtime with
                    # local upload time
                    self._probe_lock_file(mtime)
                else:
                    entry = FileEntry(self, self.cur_dir, name, size, mtime,
                                      unique)
            elif res_type in ("cdir", "pdir"):
                pass
            else:
                write_error("Could not parse '{}'".format(line))
                raise NotImplementedError(
                    "MLSD returned unsupported type: {!r}".format(res_type))

            if entry:
                entry_map[name] = entry
                entry_list.append(entry)
Ejemplo n.º 39
0
    def get_dir(self):
        entry_list = []
        entry_map = {}
        local_var = {
            "has_meta": False
        }  # pass local variables outside func scope

        encoding = self.encoding

        def _addline(status, line):
            # _ftp_retrlines_native() made sure that we always get `str` type  lines
            assert status in (0, 1, 2)
            assert is_native(line)

            data, _, name = line.partition("; ")

            # print(status, name, u_name)
            if status == 1:
                write(
                    "WARNING: File name seems not to be {}; re-encoded from CP-1252:"
                    .format(encoding),
                    name,
                )
            elif status == 2:
                write_error("File name is neither UTF-8 nor CP-1252 encoded:",
                            name)

            res_type = size = mtime = unique = None
            fields = data.split(";")
            # https://tools.ietf.org/html/rfc3659#page-23
            # "Size" / "Modify" / "Create" / "Type" / "Unique" / "Perm" / "Lang"
            #   / "Media-Type" / "CharSet" / os-depend-fact / local-fact
            for field in fields:
                field_name, _, field_value = field.partition("=")
                field_name = field_name.lower()
                if field_name == "type":
                    res_type = field_value
                elif field_name in ("sizd", "size"):
                    size = int(field_value)
                elif field_name == "modify":
                    # Use calendar.timegm() instead of time.mktime(), because
                    # the date was returned as UTC
                    if "." in field_value:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S.%f"))
                    else:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S"))
                elif field_name == "unique":
                    unique = field_value

            entry = None
            if res_type == "dir":
                entry = DirectoryEntry(self, self.cur_dir, name, size, mtime,
                                       unique)
            elif res_type == "file":
                if name == DirMetadata.META_FILE_NAME:
                    # the meta-data file is silently ignored
                    local_var["has_meta"] = True
                elif (name == DirMetadata.LOCK_FILE_NAME
                      and self.cur_dir == self.root_dir):
                    # this is the root lock file. compare reported mtime with
                    # local upload time
                    self._probe_lock_file(mtime)
                else:
                    entry = FileEntry(self, self.cur_dir, name, size, mtime,
                                      unique)
            elif res_type in ("cdir", "pdir"):
                pass
            else:
                write_error("Could not parse '{}'".format(line))
                raise NotImplementedError(
                    "MLSD returned unsupported type: {!r}".format(res_type))

            if entry:
                entry_map[name] = entry
                entry_list.append(entry)

        try:
            # We use a custom wrapper here, so we can implement a codding fall back:
            self._ftp_retrlines_native("MLSD", _addline, encoding)
            # self.ftp.retrlines("MLSD", _addline)
        except ftplib.error_perm as e:
            # write_error("The FTP server responded with {}".format(e))
            # raises error_perm "500 Unknown command" if command is not supported
            if "500" in str(e.args):
                raise RuntimeError(
                    "The FTP server does not support the 'MLSD' command.")
            raise

        # load stored meta data if present
        self.cur_dir_meta = DirMetadata(self)

        if local_var["has_meta"]:
            try:
                self.cur_dir_meta.read()
            except IncompatibleMetadataVersion:
                raise  # this should end the script (user should pass --migrate)
            except Exception as e:
                write_error("Could not read meta info {}: {}".format(
                    self.cur_dir_meta, e))

            meta_files = self.cur_dir_meta.list

            # Adjust file mtime from meta-data if present
            missing = []
            for n in meta_files:
                meta = meta_files[n]
                if n in entry_map:
                    # We have a meta-data entry for this resource
                    upload_time = meta.get("u", 0)

                    # Discard stored meta-data if
                    #   1. the reported files size is different than the
                    #      size we stored in the meta-data
                    #      or
                    #   2. the the mtime reported by the FTP server is later
                    #      than the stored upload time (which indicates
                    #      that the file was modified directly on the server)
                    if entry_map[n].size != meta.get("s"):
                        if self.get_option("verbose", 3) >= 5:
                            write(
                                "Removing meta entry {} (size changed from {} to {})."
                                .format(n, entry_map[n].size, meta.get("s")))
                        missing.append(n)
                    elif (entry_map[n].mtime -
                          upload_time) > self.mtime_compare_eps:
                        if self.get_option("verbose", 3) >= 5:
                            write("Removing meta entry {} (modified {} > {}).".
                                  format(
                                      n,
                                      time.ctime(entry_map[n].mtime),
                                      time.ctime(upload_time),
                                  ))
                        missing.append(n)
                    else:
                        # Use meta-data mtime instead of the one reported by FTP server
                        entry_map[n].meta = meta
                        entry_map[n].mtime = meta["m"]
                else:
                    # File is stored in meta-data, but no longer exists on FTP server
                    # write("META: Removing missing meta entry %s" % n)
                    missing.append(n)
            # Remove missing or invalid files from cur_dir_meta
            for n in missing:
                self.cur_dir_meta.remove(n)

        return entry_list
Ejemplo n.º 40
0
    def _get_dir_impl(self):
        entry_list = []
        entry_map = {}
        has_meta = False

        attr_list = self.sftp.listdir_attr()

        for de in attr_list:
            is_dir = stat.S_ISDIR(de.st_mode)
            name = de.filename
            entry = None
            if is_dir:
                if name not in (".", ".."):
                    entry = DirectoryEntry(
                        self, self.cur_dir, name, de.st_size, de.st_mtime, unique=None
                    )
            elif name == DirMetadata.META_FILE_NAME:
                # the meta-data file is silently ignored
                has_meta = True
            elif name == DirMetadata.LOCK_FILE_NAME and self.cur_dir == self.root_dir:
                # this is the root lock file. Compare reported mtime with
                # local upload time
                self._probe_lock_file(de.st_mtime)
            else:
                entry = FileEntry(
                    self, self.cur_dir, name, de.st_size, de.st_mtime, unique=None
                )

            if entry:
                entry_map[name] = entry
                entry_list.append(entry)

        # load stored meta data if present
        self.cur_dir_meta = DirMetadata(self)

        if has_meta:
            try:
                self.cur_dir_meta.read()
            except IncompatibleMetadataVersion:
                raise  # this should end the script (user should pass --migrate)
            except Exception as e:
                write_error(
                    "Could not read meta info {}: {}".format(self.cur_dir_meta, e)
                )

            meta_files = self.cur_dir_meta.list

            # Adjust file mtime from meta-data if present
            missing = []
            for n in meta_files:
                meta = meta_files[n]
                if n in entry_map:
                    # We have a meta-data entry for this resource
                    upload_time = meta.get("u", 0)

                    # Discard stored meta-data if
                    #   1. the reported files size is different than the
                    #      size we stored in the meta-data
                    #      or
                    #   2. the the mtime reported by the SFTP server is later
                    #      than the stored upload time (which indicates
                    #      that the file was modified directly on the server)
                    if entry_map[n].size != meta.get("s"):
                        if self.get_option("verbose", 3) >= 5:
                            write(
                                "Removing meta entry {} (size changed from {} to {}).".format(
                                    n, entry_map[n].size, meta.get("s")
                                )
                            )
                        missing.append(n)
                    elif (entry_map[n].mtime - upload_time) > self.mtime_compare_eps:
                        if self.get_option("verbose", 3) >= 5:
                            write(
                                "Removing meta entry {} (modified {} > {}).".format(
                                    n,
                                    time.ctime(entry_map[n].mtime),
                                    time.ctime(upload_time),
                                )
                            )
                        missing.append(n)
                    else:
                        # Use meta-data mtime instead of the one reported by SFTP server
                        entry_map[n].meta = meta
                        entry_map[n].mtime = meta["m"]
                else:
                    # File is stored in meta-data, but no longer exists on SFTP server
                    # write("META: Removing missing meta entry %s" % n)
                    missing.append(n)
            # Remove missing or invalid files from cur_dir_meta
            for n in missing:
                self.cur_dir_meta.remove(n)
        # print("entry_list", entry_list)
        return entry_list
Ejemplo n.º 41
0
def handle_run_command(parser, args):
    """Implement `run` sub-command."""
    MAX_LEVELS = 15

    # --- Look for `pyftpsync.yaml` in current folder and parents ---

    cur_level = 0
    cur_folder = os.getcwd()
    config_path = None
    while cur_level < MAX_LEVELS:
        path = os.path.join(cur_folder, CONFIG_FILE_NAME)
        # print("Searching for {}...".format(path))
        if os.path.isfile(path):
            config_path = path
            break
        parent = os.path.dirname(cur_folder)
        if parent == cur_folder:
            break
        cur_folder = parent
        cur_level += 1

    if not config_path:
        parser.error(
            "Could not locate `.pyftpsync.yaml` in {} or {} parent folders.".
            format(os.getcwd(), cur_level))

    # --- Parse `pyftpsync.yaml` and set `args` attributes ---

    try:
        with open(config_path, "rb") as f:
            config = yaml.safe_load(f)
    except Exception as e:
        parser.error("Error parsing {}: {}".format(config_path, e))
        # write_error("Error parsing {}: {}".format(config_path, e))
        # raise

    # print(config)
    if "tasks" not in config:
        parser.error("Missing option `tasks` in {}".format(config_path))

    common_config = config.get("common_config", {})

    default_task = config.get("default_task", "default")
    task_name = args.task or default_task
    if task_name not in config["tasks"]:
        parser.error("Missing option `tasks.{}` in {}".format(
            task_name, config_path))
    task = config["tasks"][task_name]

    write("Running task '{}' from {}".format(task_name, config_path))

    common_config.update(task)
    task = common_config
    # write("task", task)

    # --- Check task syntax ---

    task_args = set(task.keys())

    missing_args = MANDATORY_TASK_ARGS.difference(task_args)
    if missing_args:
        parser.error("Missing mandatory options: tasks.{}.{}".format(
            task_name, ", ".join(missing_args)))

    allowed_args = KNOWN_TASK_ARGS.union(MANDATORY_TASK_ARGS)
    invalid_args = task_args.difference(allowed_args)
    if invalid_args:
        parser.error("Invalid options: tasks.{}.{}".format(
            task_name, ", ".join(invalid_args)))

    # write("args", args)

    for name in allowed_args:
        val = task.get(name, None)  # default)

        if val is None:
            continue  # option not specified in yaml

        # Override yaml entry by command line
        cmd_val = getattr(args, name, None)

        # write("check --{}: {} => {}".format(name, val, cmd_val))

        if cmd_val != val:
            override = False
            if name in OVERRIDABLE_BOOL_ARGS and cmd_val:
                override = True
            elif name in {"here", "root"} and (args.here or args.root):
                override = True
            elif name == "verbose" and cmd_val != 3:
                override = True

            if override:
                write("Yaml entry overriden by --{}: {} => {}".format(
                    name, val, cmd_val))
                continue

        setattr(args, name, val)

    # --- Figure out local target path ---

    cur_folder = os.getcwd()
    root_folder = os.path.dirname(config_path)
    path_ofs = os.path.relpath(os.getcwd(), root_folder)

    if cur_level == 0 or args.root:
        path_ofs = ""
        args.local = root_folder
    elif args.here:
        write("Using sub-branch {sub} of {root}".format(root=root_folder,
                                                        sub=path_ofs))
        args.local = cur_folder
        args.remote = os.path.join(args.remote, path_ofs)
    else:
        parser.error(
            "`.pyftpsync.yaml` configuration was found in a parent directory. "
            "Please pass an additional argument to clarify:\n"
            "  --root: synchronize whole project ({root})\n"
            "  --here: synchronize sub branch ({root}/{sub})".format(
                root=root_folder, sub=path_ofs))
Ejemplo n.º 42
0
def handle_run_command(parser, args):
    """Implement `run` sub-command."""
    MAX_LEVELS = 15

    # --- Look for `pyftpsync.yaml` in current folder and parents ---

    cur_level = 0
    cur_folder = os.getcwd()
    config_path = None
    while cur_level < MAX_LEVELS:
        path = os.path.join(cur_folder, CONFIG_FILE_NAME)
        # print("Searching for {}...".format(path))
        if os.path.isfile(path):
            config_path = path
            break
        parent = os.path.dirname(cur_folder)
        if parent == cur_folder:
            break
        cur_folder = parent
        cur_level += 1

    if not config_path:
        parser.error(
            "Could not locate `.pyftpsync.yaml` in {} or {} parent folders.".format(
                os.getcwd(), cur_level
            )
        )

    # --- Parse `pyftpsync.yaml` and set `args` attributes ---

    try:
        with open(config_path, "rb") as f:
            config = yaml.safe_load(f)
    except Exception as e:
        parser.error("Error parsing {}: {}".format(config_path, e))
        # write_error("Error parsing {}: {}".format(config_path, e))
        # raise

    # print(config)
    if "tasks" not in config:
        parser.error("Missing option `tasks` in {}".format(config_path))

    common_config = config.get("common_config", {})

    default_task = config.get("default_task", "default")
    task_name = args.task or default_task
    if task_name not in config["tasks"]:
        parser.error("Missing option `tasks.{}` in {}".format(task_name, config_path))
    task = config["tasks"][task_name]

    write("Running task '{}' from {}".format(task_name, config_path))

    common_config.update(task)
    task = common_config
    # write("task", task)

    # --- Check task syntax ---

    task_args = set(task.keys())

    missing_args = MANDATORY_TASK_ARGS.difference(task_args)
    if missing_args:
        parser.error(
            "Missing mandatory options: tasks.{}.{}".format(
                task_name, ", ".join(missing_args)
            )
        )

    allowed_args = KNOWN_TASK_ARGS.union(MANDATORY_TASK_ARGS)
    invalid_args = task_args.difference(allowed_args)
    if invalid_args:
        parser.error(
            "Invalid options: tasks.{}.{}".format(task_name, ", ".join(invalid_args))
        )

    # write("args", args)

    for name in allowed_args:
        val = task.get(name, None)  # default)

        if val is None:
            continue  # option not specified in yaml

        # Override yaml entry by command line
        cmd_val = getattr(args, name, None)

        # write("check --{}: {} => {}".format(name, val, cmd_val))

        if cmd_val != val:
            override = False
            if name in OVERRIDABLE_BOOL_ARGS and cmd_val:
                override = True
            elif name in {"here", "root"} and (args.here or args.root):
                override = True
            elif name == "verbose" and cmd_val != 3:
                override = True

            if override:
                write(
                    "Yaml entry overriden by --{}: {} => {}".format(name, val, cmd_val)
                )
                continue

        setattr(args, name, val)

    # --- Figure out local target path ---

    cur_folder = os.getcwd()
    root_folder = os.path.dirname(config_path)
    path_ofs = os.path.relpath(os.getcwd(), root_folder)

    if cur_level == 0 or args.root:
        path_ofs = ""
        args.local = root_folder
    elif args.here:
        write("Using sub-branch {sub} of {root}".format(root=root_folder, sub=path_ofs))
        args.local = cur_folder
        args.remote = os.path.join(args.remote, path_ofs)
    else:
        parser.error(
            "`.pyftpsync.yaml` configuration was found in a parent directory. "
            "Please pass an additional argument to clarify:\n"
            "  --root: synchronize whole project ({root})\n"
            "  --here: synchronize sub branch ({root}/{sub})".format(
                root=root_folder, sub=path_ofs
            )
        )
Ejemplo n.º 43
0
    def open(self):
        assert not self.ftp_socket_connected

        super(FTPTarget, self).open()

        options = self.get_options_dict()
        no_prompt = self.get_option("no_prompt", True)
        store_password = self.get_option("store_password", False)
        verbose = self.get_option("verbose", 3)

        self.ftp.set_debuglevel(self.get_option("ftp_debug", 0))

        # Optionally use FTP active mode (default: PASV) (issue #21)
        force_active = self.get_option("ftp_active", False)
        self.ftp.set_pasv(not force_active)

        self.ftp.connect(self.host, self.port, self.timeout)
        # if self.timeout:
        #     self.ftp.connect(self.host, self.port, self.timeout)
        # else:
        #     # Py2.7 uses -999 as default for `timeout`, Py3 uses None
        #     self.ftp.connect(self.host, self.port)

        self.ftp_socket_connected = True

        if self.username is None or self.password is None:
            creds = get_credentials_for_url(self.host,
                                            options,
                                            force_user=self.username)
            if creds:
                self.username, self.password = creds

        while True:
            try:
                # Login (as 'anonymous' if self.username is undefined):
                self.ftp.login(self.username, self.password)
                if verbose >= 4:
                    write("Login as '{}'.".format(
                        self.username if self.username else "anonymous"))
                break
            except ftplib.error_perm as e:
                # If credentials were passed, but authentication fails, prompt
                # for new password
                if not e.args[0].startswith("530"):
                    raise  # error other then '530 Login incorrect'
                write_error("Could not login to {}@{}: {}".format(
                    self.username, self.host, e))
                if no_prompt or not self.username:
                    raise
                creds = prompt_for_password(self.host, self.username)
                self.username, self.password = creds
                # Continue while-loop

        if self.tls:
            # Upgrade data connection to TLS.
            self.ftp.prot_p()

        try:
            self.syst_response = self.ftp.sendcmd("SYST")
            if verbose >= 5:
                write("SYST: '{}'.".format(
                    self.syst_response.replace("\n", " ")))
            # self.is_unix = "unix" in resp.lower() # not necessarily true, better check with r/w tests
            # TODO: case sensitivity?
        except Exception as e:
            write("SYST command failed: '{}'".format(e))

        try:
            self.feat_response = self.ftp.sendcmd("FEAT")
            self.support_utf8 = "UTF8" in self.feat_response
            if verbose >= 5:
                write("FEAT: '{}'.".format(
                    self.feat_response.replace("\n", " ")))
        except Exception as e:
            write("FEAT command failed: '{}'".format(e))

        if self.encoding == "utf-8":
            if not self.support_utf8 and verbose >= 4:
                write(
                    "Server does not list utf-8 as supported feature (using it anyway).",
                    warning=True,
                )

            try:
                # Announce our wish to use UTF-8 to the server as proposed here:
                # See https://tools.ietf.org/html/draft-ietf-ftpext-utf-8-option-00
                # Note: this RFC is inactive, expired, and failed on Strato
                self.ftp.sendcmd("OPTS UTF-8")
                if verbose >= 4:
                    write("Sent 'OPTS UTF-8'.")
            except Exception as e:
                if verbose >= 4:
                    write("Could not send 'OPTS UTF-8': '{}'".format(e),
                          warning=True)

            try:
                # Announce our wish to use UTF-8 to the server as proposed here:
                # See https://tools.ietf.org/html/rfc2389
                # https://www.cerberusftp.com/phpBB3/viewtopic.php?t=2608
                # Note: this was accepted on Strato
                self.ftp.sendcmd("OPTS UTF8 ON")
                if verbose >= 4:
                    write("Sent 'OPTS UTF8 ON'.")
            except Exception as e:
                write("Could not send 'OPTS UTF8 ON': '{}'".format(e),
                      warning=True)

        if hasattr(self.ftp, "encoding"):
            # Python 3 encodes using latin-1 by default(!)
            # (In Python 2 ftp.encoding does not exist, but ascii is used)
            if self.encoding != codecs.lookup(self.ftp.encoding).name:
                write("Setting FTP encoding to {} (was {}).".format(
                    self.encoding, self.ftp.encoding))
                self.ftp.encoding = self.encoding

        try:
            self.ftp.cwd(self.root_dir)
        except ftplib.error_perm as e:
            if not e.args[0].startswith("550"):
                raise  # error other then 550 No such directory'
            write_error(
                "Could not change directory to {} ({}): missing permissions?".
                format(self.root_dir, e))

        pwd = self.pwd()
        # pwd = self.to_unicode(pwd)
        if pwd != self.root_dir:
            raise RuntimeError(
                "Unable to navigate to working directory {!r} (now at {!r})".
                format(self.root_dir, pwd))

        self.cur_dir = pwd

        # self.ftp_initialized = True
        # Successfully authenticated: store password
        if store_password:
            save_password(self.host, self.username, self.password)

        self._lock()

        return
Ejemplo n.º 44
0
    def get_dir(self):
        entry_list = []
        entry_map = {}
        local_res = {
            "has_meta": False
        }  # pass local variables outside func scope

        def _addline(line):
            data, _, name = line.partition("; ")
            res_type = size = mtime = unique = None
            fields = data.split(";")
            # http://tools.ietf.org/html/rfc3659#page-23
            # "Size" / "Modify" / "Create" / "Type" / "Unique" / "Perm" / "Lang"
            #   / "Media-Type" / "CharSet" / os-depend-fact / local-fact
            for field in fields:
                field_name, _, field_value = field.partition("=")
                field_name = field_name.lower()
                if field_name == "type":
                    res_type = field_value
                elif field_name in ("sizd", "size"):
                    size = int(field_value)
                elif field_name == "modify":
                    # Use calendar.timegm() instead of time.mktime(), because
                    # the date was returned as UTC
                    if "." in field_value:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S.%f"))
                    else:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S"))
                elif field_name == "unique":
                    unique = field_value

            entry = None
            if res_type == "dir":
                entry = DirectoryEntry(self, self.cur_dir, name, size, mtime,
                                       unique)
            elif res_type == "file":
                if name == DirMetadata.META_FILE_NAME:
                    # the meta-data file is silently ignored
                    local_res["has_meta"] = True
                elif name == DirMetadata.LOCK_FILE_NAME and self.cur_dir == self.root_dir:
                    # this is the root lock file. compare reported mtime with
                    # local upload time
                    self._probe_lock_file(mtime)
                else:
                    entry = FileEntry(self, self.cur_dir, name, size, mtime,
                                      unique)
            elif res_type in ("cdir", "pdir"):
                pass
            else:
                raise NotImplementedError(
                    "MLSD returned unsupported type: {!r}".format(res_type))

            if entry:
                entry_map[name] = entry
                entry_list.append(entry)

        try:
            self.ftp.retrlines("MLSD", _addline)
        except ftplib.error_perm as e:
            # write_error("The FTP server responded with {}".format(e))
            # raises error_perm "500 Unknown command" if command is not supported
            if "500" in str(e.args):
                raise RuntimeError(
                    "The FTP server does not support the 'MLSD' command.")
            raise

        # load stored meta data if present
        self.cur_dir_meta = DirMetadata(self)

        if local_res["has_meta"]:
            try:
                self.cur_dir_meta.read()
            except IncompatibleMetadataVersion:
                raise  # this should end the script (user should pass --migrate)
            except Exception as e:
                write_error("Could not read meta info {}: {}".format(
                    self.cur_dir_meta, e))

            meta_files = self.cur_dir_meta.list

            # Adjust file mtime from meta-data if present
            missing = []
            for n in meta_files:
                meta = meta_files[n]
                if n in entry_map:
                    # We have a meta-data entry for this resource
                    upload_time = meta.get("u", 0)
                    if (entry_map[n].size == meta.get("s")
                            and FileEntry._eps_compare(entry_map[n].mtime,
                                                       upload_time) <= 0):
                        # Use meta-data mtime instead of the one reported by FTP server
                        entry_map[n].meta = meta
                        entry_map[n].mtime = meta["m"]
                        # entry_map[n].dt_modified = datetime.fromtimestamp(meta["m"])
                    else:
                        # Discard stored meta-data if
                        #   1. the the mtime reported by the FTP server is later
                        #      than the stored upload time (which indicates
                        #      that the file was modified directly on the server)
                        #      or
                        #   2. the reported files size is different than the
                        #      size we stored in the meta-data
                        if self.get_option("verbose", 3) >= 5:
                            write(
                                ("META: Removing outdated meta entry {}\n" +
                                 "      modified after upload ({} > {}), or\n"
                                 "      cur. size ({}) != meta size ({})"
                                 ).format(n, time.ctime(entry_map[n].mtime),
                                          time.ctime(upload_time),
                                          entry_map[n].size, meta.get("s")))
                        missing.append(n)
                else:
                    # File is stored in meta-data, but no longer exists on FTP server
                    # write("META: Removing missing meta entry %s" % n)
                    missing.append(n)
            # Remove missing or invalid files from cur_dir_meta
            for n in missing:
                self.cur_dir_meta.remove(n)

        return entry_list
Ejemplo n.º 45
0
    def get_dir(self):
        entry_list = []
        entry_map = {}
        local_var = {"has_meta": False}  # pass local variables outside func scope

        encoding = self.encoding

        def _addline(status, line):
            # _ftp_retrlines_native() made sure that we always get `str` type  lines
            assert status in (0, 1, 2)
            assert compat.is_native(line)

            data, _, name = line.partition("; ")

            # print(status, name, u_name)
            if status == 1:
                write(
                    "WARNING: File name seems not to be {}; re-encoded from CP-1252:".format(
                        encoding
                    ),
                    name,
                )
            elif status == 2:
                write_error("File name is neither UTF-8 nor CP-1252 encoded:", name)

            res_type = size = mtime = unique = None
            fields = data.split(";")
            # https://tools.ietf.org/html/rfc3659#page-23
            # "Size" / "Modify" / "Create" / "Type" / "Unique" / "Perm" / "Lang"
            #   / "Media-Type" / "CharSet" / os-depend-fact / local-fact
            for field in fields:
                field_name, _, field_value = field.partition("=")
                field_name = field_name.lower()
                if field_name == "type":
                    res_type = field_value
                elif field_name in ("sizd", "size"):
                    size = int(field_value)
                elif field_name == "modify":
                    # Use calendar.timegm() instead of time.mktime(), because
                    # the date was returned as UTC
                    if "." in field_value:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S.%f")
                        )
                    else:
                        mtime = calendar.timegm(
                            time.strptime(field_value, "%Y%m%d%H%M%S")
                        )
                elif field_name == "unique":
                    unique = field_value

            entry = None
            if res_type == "dir":
                entry = DirectoryEntry(self, self.cur_dir, name, size, mtime, unique)
            elif res_type == "file":
                if name == DirMetadata.META_FILE_NAME:
                    # the meta-data file is silently ignored
                    local_var["has_meta"] = True
                elif (
                    name == DirMetadata.LOCK_FILE_NAME and self.cur_dir == self.root_dir
                ):
                    # this is the root lock file. compare reported mtime with
                    # local upload time
                    self._probe_lock_file(mtime)
                else:
                    entry = FileEntry(self, self.cur_dir, name, size, mtime, unique)
            elif res_type in ("cdir", "pdir"):
                pass
            else:
                write_error("Could not parse '{}'".format(line))
                raise NotImplementedError(
                    "MLSD returned unsupported type: {!r}".format(res_type)
                )

            if entry:
                entry_map[name] = entry
                entry_list.append(entry)

        try:
            # We use a custom wrapper here, so we can implement a codding fall back:
            self._ftp_retrlines_native("MLSD", _addline, encoding)
            # self.ftp.retrlines("MLSD", _addline)
        except ftplib.error_perm as e:
            # write_error("The FTP server responded with {}".format(e))
            # raises error_perm "500 Unknown command" if command is not supported
            if "500" in str(e.args):
                raise RuntimeError(
                    "The FTP server does not support the 'MLSD' command."
                )
            raise

        # load stored meta data if present
        self.cur_dir_meta = DirMetadata(self)

        if local_var["has_meta"]:
            try:
                self.cur_dir_meta.read()
            except IncompatibleMetadataVersion:
                raise  # this should end the script (user should pass --migrate)
            except Exception as e:
                write_error(
                    "Could not read meta info {}: {}".format(self.cur_dir_meta, e)
                )

            meta_files = self.cur_dir_meta.list

            # Adjust file mtime from meta-data if present
            missing = []
            for n in meta_files:
                meta = meta_files[n]
                if n in entry_map:
                    # We have a meta-data entry for this resource
                    upload_time = meta.get("u", 0)

                    # Discard stored meta-data if
                    #   1. the reported files size is different than the
                    #      size we stored in the meta-data
                    #      or
                    #   2. the the mtime reported by the FTP server is later
                    #      than the stored upload time (which indicates
                    #      that the file was modified directly on the server)
                    if entry_map[n].size != meta.get("s"):
                        if self.get_option("verbose", 3) >= 5:
                            write(
                                "Removing meta entry {} (size changed from {} to {}).".format(
                                    n, entry_map[n].size, meta.get("s")
                                )
                            )
                        missing.append(n)
                    elif (entry_map[n].mtime - upload_time) > self.mtime_compare_eps:
                        if self.get_option("verbose", 3) >= 5:
                            write(
                                "Removing meta entry {} (modified {} > {}).".format(
                                    n,
                                    time.ctime(entry_map[n].mtime),
                                    time.ctime(upload_time),
                                )
                            )
                        missing.append(n)
                    else:
                        # Use meta-data mtime instead of the one reported by FTP server
                        entry_map[n].meta = meta
                        entry_map[n].mtime = meta["m"]
                else:
                    # File is stored in meta-data, but no longer exists on FTP server
                    # write("META: Removing missing meta entry %s" % n)
                    missing.append(n)
            # Remove missing or invalid files from cur_dir_meta
            for n in missing:
                self.cur_dir_meta.remove(n)

        return entry_list
Ejemplo n.º 46
0
    def open(self):
        assert not self.ftp_socket_connected

        super(FtpTarget, self).open()

        options = self.get_options_dict()
        no_prompt = self.get_option("no_prompt", True)
        store_password = self.get_option("store_password", False)

        self.ftp.set_debuglevel(self.get_option("ftp_debug", 0))

        # Optionally use FTP active mode (default: PASV) (issue #21)
        force_active = self.get_option("ftp_active", False)
        self.ftp.set_pasv(not force_active)

        if self.timeout:
            self.ftp.connect(self.host, self.port, self.timeout)
        else:
            # Py2.7 uses -999 as default for `timeout`, Py3 uses None
            self.ftp.connect(self.host, self.port)

        self.ftp_socket_connected = True

        if self.username is None or self.password is None:
            creds = get_credentials_for_url(self.host,
                                            options,
                                            force_user=self.username)
            if creds:
                self.username, self.password = creds

        while True:
            try:
                # Login (as 'anonymous' if self.username is undefined):
                self.ftp.login(self.username, self.password)
                if self.get_option("verbose", 3) >= 4:
                    write("Login as '{}'.".format(
                        self.username if self.username else "anonymous"))
                break
            except ftplib.error_perm as e:
                # If credentials were passed, but authentication fails, prompt
                # for new password
                if not e.args[0].startswith("530"):
                    raise  # error other then '530 Login incorrect'
                write_error("Could not login to {}@{}: {}".format(
                    self.username, self.host, e))
                if no_prompt or not self.username:
                    raise
                creds = prompt_for_password(self.host, self.username)
                self.username, self.password = creds
                # Continue while-loop

        if self.tls:
            # Upgrade data connection to TLS.
            self.ftp.prot_p()

        try:
            self.ftp.cwd(self.root_dir)
        except ftplib.error_perm as e:
            # If credentials were passed, but authentication fails, prompt
            # for new password
            if not e.args[0].startswith("550"):
                raise  # error other then 550 No such directory'
            write_error(
                "Could not change directory to {} ({}): missing permissions?".
                format(self.root_dir, e))

        pwd = self.ftp.pwd()
        if pwd != self.root_dir:
            raise RuntimeError(
                "Unable to navigate to working directory {!r} (now at {!r})".
                format(self.root_dir, pwd))

        self.cur_dir = pwd

        # self.ftp_initialized = True
        # Successfully authenticated: store password
        if store_password:
            save_password(self.host, self.username, self.password)

        # TODO: case sensitivity?
        # resp = self.ftp.sendcmd("system")
        # self.is_unix = "unix" in resp.lower() # not necessarily true, better check with r/w tests

        self._lock()

        return
Ejemplo n.º 47
0
    def open(self):
        assert not self.ftp_socket_connected

        super(SFTPTarget, self).open()

        options = self.get_options_dict()
        no_prompt = self.get_option("no_prompt", True)
        store_password = self.get_option("store_password", False)
        verbose = self.get_option("verbose", 3)
        verify_host_keys = not self.get_option("no_verify_host_keys", False)
        if self.get_option("ftp_active", False):
            raise RuntimeError("SFTP does not have active/passive mode.")

        if verbose <= 3:
            logging.getLogger("paramiko.transport").setLevel(logging.WARNING)

        write("Connecting {}:*** to sftp://{}".format(self.username, self.host))

        cnopts = pysftp.CnOpts()
        cnopts.log = self.get_option("ftp_debug", False)
        if not verify_host_keys:
            cnopts.hostkeys = None

        if self.username is None or self.password is None:
            creds = get_credentials_for_url(
                self.host, options, force_user=self.username
            )
            if creds:
                self.username, self.password = creds

        assert self.sftp is None
        while True:
            try:
                self.sftp = pysftp.Connection(
                    self.host,
                    username=self.username,
                    password=self.password,
                    port=self.port,
                    cnopts=cnopts,
                )
                break
            except paramiko.ssh_exception.AuthenticationException as e:
                write_error(
                    "Could not login to {}@{}: {}".format(self.username, self.host, e)
                )
                if no_prompt or not self.username:
                    raise
                creds = prompt_for_password(self.host, self.username)
                self.username, self.password = creds
                # Continue while-loop
            except paramiko.ssh_exception.SSHException as e:
                write_error(
                    "{exc}: Try `ssh-keyscan HOST` to add it "
                    "(or pass `--no-verify-host-keys` if you don't care about security).".format(
                        exc=e
                    )
                )
                raise

        if verbose >= 4:
            write(
                "Login as '{}'.".format(self.username if self.username else "anonymous")
            )
        if self.sftp.logfile:
            write("Logging to {}".format(self.sftp.logfile))
        self.sftp.timeout = self.timeout
        self.ftp_socket_connected = True

        try:
            self.sftp.cwd(self.root_dir)
        except IOError as e:
            # if not e.args[0].startswith("550"):
            #     raise  # error other then 550 No such directory'
            write_error(
                "Could not change directory to {} ({}): missing permissions?".format(
                    self.root_dir, e
                )
            )

        pwd = self.pwd()
        # pwd = self.to_unicode(pwd)
        if pwd != self.root_dir:
            raise RuntimeError(
                "Unable to navigate to working directory {!r} (now at {!r})".format(
                    self.root_dir, pwd
                )
            )

        self.cur_dir = pwd

        # self.ftp_initialized = True
        # Successfully authenticated: store password
        if store_password:
            save_password(self.host, self.username, self.password)

        self._lock()

        return