Exemple #1
0
class _Target(object):
    def __init__(self, root_dir, extra_opts):
        if root_dir != "/":
            root_dir = root_dir.rstrip("/")
        self.root_dir = root_dir
        self.extra_opts = extra_opts or {}
        self.readonly = False
        self.dry_run = False
        self.host = None
        self.synchronizer = None  # Set by BaseSynchronizer.__init__()
        self.peer = None
        self.cur_dir = None
        self.connected = False
        self.save_mode = True
        self.case_sensitive = None  # TODO: don't know yet
        self.time_ofs = None  # TODO: see _probe_lock_file()
        self.support_set_time = None  # Derived class knows
        self.cur_dir_meta = DirMetadata(self)
        self.meta_stack = []

    def __del__(self):
        # TODO: http://pydev.blogspot.de/2015/01/creating-safe-cyclic-reference.html
        self.close()

    def get_base_name(self):
        return "%s" % self.root_dir

    def is_local(self):
        return self.synchronizer.local is self

    def is_unbund(self):
        return self.synchronizer is None

    def get_option(self, key, default=None):
        """Return option from synchronizer (possibly overridden by target extra_opts)."""
        if self.synchronizer:
            return self.extra_opts.get(
                key, self.synchronizer.options.get(key, default))
        return self.extra_opts.get(key, default)

    def open(self):
        self.connected = True

    def close(self):
        self.connected = False

    def check_write(self, name):
        """Raise exception if writing cur_dir/name is not allowed."""
        if self.readonly and name not in (DirMetadata.META_FILE_NAME,
                                          DirMetadata.LOCK_FILE_NAME):
            raise RuntimeError("target is read-only: %s + %s / " %
                               (self, name))

    def get_id(self):
        return self.root_dir

    def get_sync_info(self, name, key=None):
        """Get mtime/size when this target's current dir was last synchronized with remote."""
        peer_target = self.peer
        if self.is_local():
            info = self.cur_dir_meta.dir["peer_sync"].get(peer_target.get_id())
        else:
            info = peer_target.cur_dir_meta.dir["peer_sync"].get(self.get_id())
        if name is not None:
            info = info.get(name) if info else None
        if info and key:
            info = info.get(key)
        return info

    def cwd(self, dir_name):
        raise NotImplementedError

    def push_meta(self):
        self.meta_stack.append(self.cur_dir_meta)
        self.cur_dir_meta = None

    def pop_meta(self):
        self.cur_dir_meta = self.meta_stack.pop()

    def flush_meta(self):
        """Write additional meta information for current directory."""
        if self.cur_dir_meta:
            self.cur_dir_meta.flush()

    def pwd(self, dir_name):
        raise NotImplementedError

    def mkdir(self, dir_name):
        raise NotImplementedError

    def rmdir(self, dir_name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def get_dir(self):
        """Return a list of _Resource entries."""
        raise NotImplementedError

    def walk(self, pred=None):
        """Iterate over all target entries recursively."""
        for entry in self.get_dir():
            if pred and pred(entry) is False:
                continue

            yield entry

            if isinstance(entry, DirectoryEntry):
                self.cwd(entry.name)
                for e in self.walk(pred):
                    yield e
                self.cwd("..")
        return

    def open_readable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def read_text(self, name):
        """Read text string from cur_dir/name using open_readable()."""
        with self.open_readable(name) as fp:
            res = fp.read()  # StringIO or file object
            #             try:
            #                 res = fp.getvalue()  # StringIO returned by FtpTarget
            #             except AttributeError:
            #                 res = fp.read()  # file object returned by FsTarget
            res = res.decode("utf8")
            return res

    def write_file(self, name, fp_src, blocksize=8192, callback=None):
        """Write binary data from file-like to cur_dir/name."""
        raise NotImplementedError

    def write_text(self, name, s):
        """Write string data to cur_dir/name using write_file()."""
        buf = io.BytesIO(to_binary(s))
        self.write_file(name, buf)

    def remove_file(self, name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def set_mtime(self, name, mtime, size):
        raise NotImplementedError

    def set_sync_info(self, name, mtime, size):
        """Store mtime/size when this resource was last synchronized with remote."""
        if not self.is_local():
            return self.peer.set_sync_info(name, mtime, size)
        return self.cur_dir_meta.set_sync_info(name, mtime, size)

    def remove_sync_info(self, name):
        if not self.is_local():
            return self.peer.remove_sync_info(name)
        if self.cur_dir_meta:
            return self.cur_dir_meta.remove(name)
        # print("%s.remove_sync_info(%s): nothing to do" % (self, name))
        return
Exemple #2
0
class _Target(object):
    """Base class for :class:`FsTarget`, :class:`FtpTarget`, etc."""

    DEFAULT_BLOCKSIZE = 16 * 1024  # shutil.copyobj() uses 16k blocks by default

    def __init__(self, root_dir, extra_opts):
        # All internal paths should use unicode.
        # (We cannot convert here, since we don't know the target encoding.)
        assert compat.is_native(root_dir)
        if root_dir != "/":
            root_dir = root_dir.rstrip("/")
        # This target is not thread safe
        self._rlock = threading.RLock()
        #: The target's top-level folder
        self.root_dir = root_dir
        self.extra_opts = extra_opts or {}
        self.readonly = False
        self.dry_run = False
        self.host = None
        self.synchronizer = None  # Set by BaseSynchronizer.__init__()
        self.peer = None
        self.cur_dir = None
        self.connected = False
        self.save_mode = True
        self.case_sensitive = None  # TODO: don't know yet
        #: 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
        #: Maximum allowed difference between a reported mtime and the last known update time,
        #: before we classify the entry as 'modified externally'
        self.mtime_compare_eps = FileEntry.EPS_TIME
        self.cur_dir_meta = DirMetadata(self)
        self.meta_stack = []
        # Optionally define an encoding for this target, but don't override
        # derived class's setting
        if not hasattr(self, "encoding"):
            #: Assumed encoding for this target. Used to decode binary paths.
            self.encoding = _get_encoding_opt(None, extra_opts, None)
        return

    def __del__(self):
        # TODO: http://pydev.blogspot.de/2015/01/creating-safe-cyclic-reference.html
        self.close()

    # def __enter__(self):
    #     self.open()
    #     return self

    # def __exit__(self, exc_type, exc_value, traceback):
    #     self.close()

    def get_base_name(self):
        return "{}".format(self.root_dir)

    def is_local(self):
        return self.synchronizer.local is self

    def is_unbound(self):
        return self.synchronizer is None

    # def to_bytes(self, s):
    #     """Convert `s` to bytes, using this target's encoding (does nothing if `s` is already bytes)."""
    #     return compat.to_bytes(s, self.encoding)

    # def to_unicode(self, s):
    #     """Convert `s` to unicode, using this target's encoding (does nothing if `s` is already unic)."""
    #     return compat.to_unicode(s, self.encoding)

    # def to_native(self, s):
    #     """Convert `s` to native, using this target's encoding (does nothing if `s` is already native)."""
    #     return compat.to_native(s, self.encoding)

    # def re_encode_to_native(self, s):
    #     """Return `s` in `str` format, assuming target.encoding.

    #     On Python 2 return a binary `str`:
    #         Encode unicode to UTF-8 binary str
    #         Re-encode binary str from self.encoding to UTF-8
    #     On Python 3 return unicode `str`:
    #         Leave unicode unmodified
    #         Decode binary str using self.encoding
    #     """
    #     if compat.PY2:
    #         if isinstance(s, unicode):  # noqa
    #             s = s.encode("utf-8")
    #         elif self.encoding != "utf-8":
    #             s = s.decode(self.encoding)
    #             s = s.encode("utf-8")
    #     elif not isinstance(s, str):
    #         s = s.decode(self.encoding)
    #     return s

    def get_options_dict(self):
        """Return options from synchronizer (possibly overridden by own extra_opts)."""
        d = self.synchronizer.options if self.synchronizer else {}
        d.update(self.extra_opts)
        return d

    def get_option(self, key, default=None):
        """Return option from synchronizer (possibly overridden by target extra_opts)."""
        if self.synchronizer:
            return self.extra_opts.get(
                key, self.synchronizer.options.get(key, default))
        return self.extra_opts.get(key, default)

    def open(self):
        if self.connected:
            raise RuntimeError("Target already open: {}.  ".format(self))
        # Not thread safe (issue #20)
        if not self._rlock.acquire(False):
            raise RuntimeError("Could not acquire _Target lock on open")
        self.connected = True

    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()

    def check_write(self, name):
        """Raise exception if writing cur_dir/name is not allowed."""
        assert compat.is_native(name)
        if self.readonly and name not in (
                DirMetadata.META_FILE_NAME,
                DirMetadata.LOCK_FILE_NAME,
        ):
            raise RuntimeError("Target is read-only: {} + {} / ".format(
                self, name))

    def get_id(self):
        return self.root_dir

    def get_sync_info(self, name, key=None):
        """Get mtime/size when this target's current dir was last synchronized with remote."""
        peer_target = self.peer
        if self.is_local():
            info = self.cur_dir_meta.dir["peer_sync"].get(peer_target.get_id())
        else:
            info = peer_target.cur_dir_meta.dir["peer_sync"].get(self.get_id())
        if name is not None:
            info = info.get(name) if info else None
        if info and key:
            info = info.get(key)
        return info

    def cwd(self, dir_name):
        raise NotImplementedError

    def push_meta(self):
        self.meta_stack.append(self.cur_dir_meta)
        self.cur_dir_meta = None

    def pop_meta(self):
        self.cur_dir_meta = self.meta_stack.pop()

    def flush_meta(self):
        """Write additional meta information for current directory."""
        if self.cur_dir_meta:
            self.cur_dir_meta.flush()

    def pwd(self, dir_name):
        raise NotImplementedError

    def mkdir(self, dir_name):
        raise NotImplementedError

    def rmdir(self, dir_name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def get_dir(self):
        """Return a list of _Resource entries."""
        raise NotImplementedError

    def walk(self, pred=None, recursive=True):
        """Iterate over all target entries recursively.

        Args:
            pred (function, optional):
                Callback(:class:`ftpsync.resources._Resource`) should return `False` to
                ignore entry. Default: `None`.
            recursive (bool, optional):
                Pass `False` to generate top level entries only. Default: `True`.
        Yields:
            :class:`ftpsync.resources._Resource`
        """
        for entry in self.get_dir():
            if pred and pred(entry) is False:
                continue

            yield entry

            if recursive:
                if isinstance(entry, DirectoryEntry):
                    self.cwd(entry.name)
                    for e in self.walk(pred):
                        yield e
                    self.cwd("..")
        return

    def open_readable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def open_writable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def read_text(self, name):
        """Read text string from cur_dir/name using open_readable()."""
        with self.open_readable(name) as fp:
            res = fp.read()  # StringIO or file object
            # try:
            #     res = fp.getvalue()  # StringIO returned by FtpTarget
            # except AttributeError:
            #     res = fp.read()  # file object returned by FsTarget
            res = res.decode("utf-8")
            return res

    def copy_to_file(self, name, fp_dest, callback=None):
        """Write cur_dir/name to file-like `fp_dest`.

        Args:
            name (str): file name, located in self.curdir
            fp_dest (file-like): must support write() method
            callback (function, optional):
                Called like `func(buf)` for every written chunk
        """
        raise NotImplementedError

    def write_file(self,
                   name,
                   fp_src,
                   blocksize=DEFAULT_BLOCKSIZE,
                   callback=None):
        """Write binary data from file-like to cur_dir/name."""
        raise NotImplementedError

    def write_text(self, name, s):
        """Write string data to cur_dir/name using write_file()."""
        buf = io.BytesIO(compat.to_bytes(s))
        self.write_file(name, buf)

    def remove_file(self, name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def set_mtime(self, name, mtime, size):
        raise NotImplementedError

    def set_sync_info(self, name, mtime, size):
        """Store mtime/size when this resource was last synchronized with remote."""
        if not self.is_local():
            return self.peer.set_sync_info(name, mtime, size)
        return self.cur_dir_meta.set_sync_info(name, mtime, size)

    def remove_sync_info(self, name):
        if not self.is_local():
            return self.peer.remove_sync_info(name)
        if self.cur_dir_meta:
            return self.cur_dir_meta.remove(name)
        # write("%s.remove_sync_info(%s): nothing to do" % (self, name))
        return
Exemple #3
0
class _Target:
    """Base class for :class:`FsTarget`, :class:`FTPTarget`, etc."""

    DEFAULT_BLOCKSIZE = 16 * 1024  # shutil.copyobj() uses 16k blocks by default

    def __init__(self, root_dir, extra_opts):
        # All internal paths should use unicode.
        # (We cannot convert here, since we don't know the target encoding.)
        assert is_native(root_dir)
        if root_dir != "/":
            root_dir = root_dir.rstrip("/")
        # This target is not thread safe
        self._rlock = threading.RLock()
        #: The target's top-level folder
        self.root_dir = root_dir
        self.extra_opts = extra_opts or {}
        self.readonly = False
        self.dry_run = False
        self.host = None
        self.synchronizer = None  # Set by BaseSynchronizer.__init__()
        self.peer = None
        self.cur_dir = None
        self.connected = False
        self.save_mode = True
        self.case_sensitive = None  # TODO: don't know yet
        #: 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
        #: Maximum allowed difference between a reported mtime and the last known update time,
        #: before we classify the entry as 'modified externally'
        self.mtime_compare_eps = FileEntry.EPS_TIME
        self.cur_dir_meta = DirMetadata(self)
        self.meta_stack = []
        # Optionally define an encoding for this target, but don't override
        # derived class's setting
        if not hasattr(self, "encoding"):
            #: Assumed encoding for this target. Used to decode binary paths.
            self.encoding = _get_encoding_opt(None, extra_opts, None)
        return

    def __del__(self):
        # TODO: http://pydev.blogspot.de/2015/01/creating-safe-cyclic-reference.html
        if self.connected:
            self.close()

    # def __enter__(self):
    #     self.open()
    #     return self

    # def __exit__(self, exc_type, exc_value, traceback):
    #     self.close()

    def get_base_name(self):
        return "{}".format(self.root_dir)

    def is_local(self):
        return self.synchronizer.local is self

    def is_unbound(self):
        return self.synchronizer is None

    def get_options_dict(self):
        """Return options from synchronizer (possibly overridden by own extra_opts)."""
        d = self.synchronizer.options if self.synchronizer else {}
        d.update(self.extra_opts)
        return d

    def get_option(self, key, default=None):
        """Return option from synchronizer (possibly overridden by target extra_opts)."""
        if self.synchronizer:
            return self.extra_opts.get(
                key, self.synchronizer.options.get(key, default))
        return self.extra_opts.get(key, default)

    def open(self):
        if self.connected:
            raise RuntimeError("Target already open: {}.  ".format(self))
        # Not thread safe (issue #20)
        if not self._rlock.acquire(False):
            raise RuntimeError("Could not acquire _Target lock on open")
        self.connected = True

    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()

    def check_write(self, name):
        """Raise exception if writing cur_dir/name is not allowed."""
        assert is_native(name)
        if self.readonly and name not in (
                DirMetadata.META_FILE_NAME,
                DirMetadata.LOCK_FILE_NAME,
        ):
            raise RuntimeError("Target is read-only: {} + {} / ".format(
                self, name))

    def get_id(self):
        return self.root_dir

    def get_sync_info(self, name, key=None):
        """Get mtime/size when this target's current dir was last synchronized with remote."""
        peer_target = self.peer
        if self.is_local():
            info = self.cur_dir_meta.dir["peer_sync"].get(peer_target.get_id())
        else:
            info = peer_target.cur_dir_meta.dir["peer_sync"].get(self.get_id())
        if name is not None:
            info = info.get(name) if info else None
        if info and key:
            info = info.get(key)
        return info

    def cwd(self, dir_name):
        raise NotImplementedError

    @contextlib.contextmanager
    def enter_subdir(self, name):
        """Temporarily changes the working directory to `name`.

        Examples:
            with target.enter_subdir(folder):
                ...
        """
        self.cwd(name)
        yield
        self.cwd("..")

    def push_meta(self):
        self.meta_stack.append(self.cur_dir_meta)
        self.cur_dir_meta = None

    def pop_meta(self):
        self.cur_dir_meta = self.meta_stack.pop()

    def flush_meta(self):
        """Write additional meta information for current directory."""
        if self.cur_dir_meta:
            self.cur_dir_meta.flush()

    def pwd(self, dir_name):
        raise NotImplementedError

    def mkdir(self, dir_name):
        raise NotImplementedError

    def rmdir(self, dir_name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def get_dir(self):
        """Return a list of _Resource entries."""
        raise NotImplementedError

    def walk(self, pred=None, recursive=True):
        """Iterate over all target entries recursively.

        Args:
            pred (function, optional):
                Callback(:class:`ftpsync.resources._Resource`) should return `False` to
                ignore entry. Default: `None`.
            recursive (bool, optional):
                Pass `False` to generate top level entries only. Default: `True`.
        Yields:
            :class:`ftpsync.resources._Resource`
        """
        for entry in self.get_dir():
            if pred and pred(entry) is False:
                continue

            yield entry

            if recursive:
                if isinstance(entry, DirectoryEntry):
                    self.cwd(entry.name)
                    for e in self.walk(pred):
                        yield e
                    self.cwd("..")
        return

    def walk_tree(self, sort=True, files=False, pred=None, _prefixes=None):
        """Iterate over target hierarchy, depth-first, adding a connector prefix.

        This iterator walks the tree nodes, but slightly delays the output, in
        order to add information if a node is the *last* sibling.
        This information is then used to create pretty tree connector prefixes.

        Args:
            sort (bool):
            files (bool):
            pred (function, optional):
                Callback(:class:`ftpsync.resources._Resource`) should return `False` to
                ignore entry. Default: `None`.
        Yields:
            3-tuple (
                :class:`ftpsync.resources._Resource`,
                is_last_sibling,
                prefix,
            )

        A
         +- a
         |   +- 1
         |   |   `- 1.1
         |   `- 2
         |       `- 2.1
         `- b
             +- 1
             |   `-  1.1
              ` 2
        """
        # List of parent's `is_last` flags:
        if _prefixes is None:
            _prefixes = []

        def _yield_entry(entry, is_last):
            path = "".join(["    " if last else " |  " for last in _prefixes])
            path += " `- " if is_last else " +- "
            yield path, entry
            if entry.is_dir():
                with self.enter_subdir(entry.name):
                    _prefixes.append(is_last)
                    yield from self.walk_tree(sort, files, pred, _prefixes)
                    _prefixes.pop()
            return

        dir_list = self.get_dir()
        if not files:
            dir_list = [entry for entry in dir_list if entry.is_dir()]

        if sort:
            # Sort by name, files first
            dir_list.sort(
                key=lambda entry: (entry.is_dir(), entry.name.lower()))

        prev_entry = None
        for next_entry in dir_list:
            if pred and pred(next_entry) is False:
                continue
            # Skip first entry
            if prev_entry is None:
                prev_entry = next_entry
                continue
            # Yield entry (this is never the last sibling)
            yield from _yield_entry(prev_entry, False)
            prev_entry = next_entry

        # Finally yield the last sibling
        if prev_entry:
            yield from _yield_entry(prev_entry, True)
        return

    def open_readable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def open_writable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def read_text(self, name):
        """Read text string from cur_dir/name using open_readable()."""
        with self.open_readable(name) as fp:
            res = fp.read()  # StringIO or file object
            # try:
            #     res = fp.getvalue()  # StringIO returned by FTPTarget
            # except AttributeError:
            #     res = fp.read()  # file object returned by FsTarget
            res = res.decode("utf-8")
            return res

    def copy_to_file(self, name, fp_dest, callback=None):
        """Write cur_dir/name to file-like `fp_dest`.

        Args:
            name (str): file name, located in self.curdir
            fp_dest (file-like): must support write() method
            callback (function, optional):
                Called like `func(buf)` for every written chunk
        """
        raise NotImplementedError

    def write_file(self,
                   name,
                   fp_src,
                   blocksize=DEFAULT_BLOCKSIZE,
                   callback=None):
        """Write binary data from file-like to cur_dir/name."""
        raise NotImplementedError

    def write_text(self, name, s):
        """Write string data to cur_dir/name using write_file()."""
        buf = io.BytesIO(to_bytes(s))
        self.write_file(name, buf)

    def remove_file(self, name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def set_mtime(self, name, mtime, size):
        raise NotImplementedError

    def set_sync_info(self, name, mtime, size):
        """Store mtime/size when this resource was last synchronized with remote."""
        if not self.is_local():
            return self.peer.set_sync_info(name, mtime, size)
        return self.cur_dir_meta.set_sync_info(name, mtime, size)

    def remove_sync_info(self, name):
        if not self.is_local():
            return self.peer.remove_sync_info(name)
        if self.cur_dir_meta:
            return self.cur_dir_meta.remove(name)
        # write("%s.remove_sync_info(%s): nothing to do" % (self, name))
        return
Exemple #4
0
class _Target(object):
    """Base class for :class:`FsTarget`, :class:`FtpTarget`, etc."""
    DEFAULT_BLOCKSIZE = 16 * 1024  # shutil.copyobj() uses 16k blocks by default

    def __init__(self, root_dir, extra_opts):
        if root_dir != "/":
            root_dir = root_dir.rstrip("/")
        # This target is not thread safe
        self._rlock = threading.RLock()
        #:
        self.root_dir = root_dir
        self.extra_opts = extra_opts or {}
        self.readonly = False
        self.dry_run = False
        self.host = None
        self.synchronizer = None  # Set by BaseSynchronizer.__init__()
        self.peer = None
        self.cur_dir = None
        self.connected = False
        self.save_mode = True
        self.case_sensitive = None  # TODO: don't know yet
        self.time_ofs = None  # TODO: see _probe_lock_file()
        self.support_set_time = None  # Derived class knows
        self.cur_dir_meta = DirMetadata(self)
        self.meta_stack = []

    def __del__(self):
        # TODO: http://pydev.blogspot.de/2015/01/creating-safe-cyclic-reference.html
        self.close()

#     def __enter__(self):
#         self.open()
#         return self
#
#     def __exit__(self, exc_type, exc_value, traceback):
#         self.close()

    def get_base_name(self):
        return "{}".format(self.root_dir)

    def is_local(self):
        return self.synchronizer.local is self

    def is_unbound(self):
        return self.synchronizer is None

    def get_options_dict(self):
        """Return options from synchronizer (possibly overridden by own extra_opts)."""
        d = self.synchronizer.options if self.synchronizer else {}
        d.update(self.extra_opts)
        return d

    def get_option(self, key, default=None):
        """Return option from synchronizer (possibly overridden by target extra_opts)."""
        if self.synchronizer:
            return self.extra_opts.get(
                key, self.synchronizer.options.get(key, default))
        return self.extra_opts.get(key, default)

    def open(self):
        if self.connected:
            raise RuntimeError("Target already open: {}.  ".format(self))
        # Not thread safe (issue #20)
        if not self._rlock.acquire(False):
            raise RuntimeError("Could not acquire _Target lock on open")
        self.connected = True

    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()

    def check_write(self, name):
        """Raise exception if writing cur_dir/name is not allowed."""
        if self.readonly and name not in (DirMetadata.META_FILE_NAME,
                                          DirMetadata.LOCK_FILE_NAME):
            raise RuntimeError("Target is read-only: {} + {} / ".format(
                self, name))

    def get_id(self):
        return self.root_dir

    def get_sync_info(self, name, key=None):
        """Get mtime/size when this target's current dir was last synchronized with remote."""
        peer_target = self.peer
        if self.is_local():
            info = self.cur_dir_meta.dir["peer_sync"].get(peer_target.get_id())
        else:
            info = peer_target.cur_dir_meta.dir["peer_sync"].get(self.get_id())
        if name is not None:
            info = info.get(name) if info else None
        if info and key:
            info = info.get(key)
        return info

    def cwd(self, dir_name):
        raise NotImplementedError

    def push_meta(self):
        self.meta_stack.append(self.cur_dir_meta)
        self.cur_dir_meta = None

    def pop_meta(self):
        self.cur_dir_meta = self.meta_stack.pop()

    def flush_meta(self):
        """Write additional meta information for current directory."""
        if self.cur_dir_meta:
            self.cur_dir_meta.flush()

    def pwd(self, dir_name):
        raise NotImplementedError

    def mkdir(self, dir_name):
        raise NotImplementedError

    def rmdir(self, dir_name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def get_dir(self):
        """Return a list of _Resource entries."""
        raise NotImplementedError

    def walk(self, pred=None, recursive=True):
        """Iterate over all target entries recursively.

        Args:
            pred (function, optional):
                Callback(:class:`ftpsync.resources._Resource`) should return `False` to
                ignore entry. Default: `None`.
            recursive (bool, optional):
                Pass `False` to generate top level entries only. Default: `True`.
        Yields:
            :class:`ftpsync.resources._Resource`
        """
        for entry in self.get_dir():
            if pred and pred(entry) is False:
                continue

            yield entry

            if recursive:
                if isinstance(entry, DirectoryEntry):
                    self.cwd(entry.name)
                    for e in self.walk(pred):
                        yield e
                    self.cwd("..")
        return

    def open_readable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def open_writable(self, name):
        """Return file-like object opened in binary mode for cur_dir/name."""
        raise NotImplementedError

    def read_text(self, name):
        """Read text string from cur_dir/name using open_readable()."""
        with self.open_readable(name) as fp:
            res = fp.read()  # StringIO or file object
            #             try:
            #                 res = fp.getvalue()  # StringIO returned by FtpTarget
            #             except AttributeError:
            #                 res = fp.read()  # file object returned by FsTarget
            res = res.decode("utf8")
            return res

    def copy_to_file(self, name, fp_dest, callback=None):
        """Write cur_dir/name to file-like `fp_dest`.

        Args:
            name (str): file name, located in self.curdir
            fp_dest (file-like): must support write() method
            callback (function, optional):
                Called like `func(buf)` for every written chunk
        """

    def write_file(self,
                   name,
                   fp_src,
                   blocksize=DEFAULT_BLOCKSIZE,
                   callback=None):
        """Write binary data from file-like to cur_dir/name."""
        raise NotImplementedError

    def write_text(self, name, s):
        """Write string data to cur_dir/name using write_file()."""
        buf = io.BytesIO(compat.to_bytes(s))
        self.write_file(name, buf)

    def remove_file(self, name):
        """Remove cur_dir/name."""
        raise NotImplementedError

    def set_mtime(self, name, mtime, size):
        raise NotImplementedError

    def set_sync_info(self, name, mtime, size):
        """Store mtime/size when this resource was last synchronized with remote."""
        if not self.is_local():
            return self.peer.set_sync_info(name, mtime, size)
        return self.cur_dir_meta.set_sync_info(name, mtime, size)

    def remove_sync_info(self, name):
        if not self.is_local():
            return self.peer.remove_sync_info(name)
        if self.cur_dir_meta:
            return self.cur_dir_meta.remove(name)
        # write("%s.remove_sync_info(%s): nothing to do" % (self, name))
        return