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