mirror of
https://github.com/samschott/maestral.git
synced 2024-11-15 08:25:49 +03:00
support bytes and str paths from fsevents
This commit is contained in:
parent
0027ed0c57
commit
96523ed668
@ -48,7 +48,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_dest_path(event: FileSystemEvent) -> str:
|
def get_dest_path(event: FileSystemEvent) -> str | bytes:
|
||||||
"""
|
"""
|
||||||
Returns the dest_path of a file system event if present (moved events only)
|
Returns the dest_path of a file system event if present (moved events only)
|
||||||
otherwise returns the src_path (which is also the "destination").
|
otherwise returns the src_path (which is also the "destination").
|
||||||
@ -425,7 +425,7 @@ class SyncEvent(Model):
|
|||||||
change_time = stat.st_ctime if stat else None
|
change_time = stat.st_ctime if stat else None
|
||||||
size = stat.st_size if stat else 0
|
size = stat.st_size if stat else 0
|
||||||
try:
|
try:
|
||||||
symlink_target = os.readlink(event.src_path)
|
symlink_target = os.readlink(os.fsdecode(event.src_path))
|
||||||
except OSError:
|
except OSError:
|
||||||
symlink_target = None
|
symlink_target = None
|
||||||
|
|
||||||
|
@ -1066,7 +1066,7 @@ class SyncEngine:
|
|||||||
|
|
||||||
# ==== Content hashing =============================================================
|
# ==== Content hashing =============================================================
|
||||||
|
|
||||||
def get_local_hash(self, local_path: str) -> str | None:
|
def get_local_hash(self, local_path: str | bytes) -> str | None:
|
||||||
"""
|
"""
|
||||||
Computes content hash of a local file.
|
Computes content hash of a local file.
|
||||||
|
|
||||||
@ -1074,6 +1074,8 @@ class SyncEngine:
|
|||||||
:returns: Content hash to compare with Dropbox's content hash, or 'folder' if
|
:returns: Content hash to compare with Dropbox's content hash, or 'folder' if
|
||||||
the path points to a directory. ``None`` if there is nothing at the path.
|
the path points to a directory. ``None`` if there is nothing at the path.
|
||||||
"""
|
"""
|
||||||
|
local_path = os.fsdecode(local_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stat = os.lstat(local_path)
|
stat = os.lstat(local_path)
|
||||||
except (FileNotFoundError, NotADirectoryError):
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
@ -1360,7 +1362,7 @@ class SyncEngine:
|
|||||||
|
|
||||||
return dbx_path_cased
|
return dbx_path_cased
|
||||||
|
|
||||||
def to_dbx_path(self, local_path: str) -> str:
|
def to_dbx_path(self, local_path: str | bytes) -> str:
|
||||||
"""
|
"""
|
||||||
Converts a local path to a path relative to the Dropbox folder. Casing of the
|
Converts a local path to a path relative to the Dropbox folder. Casing of the
|
||||||
given ``local_path`` will be preserved.
|
given ``local_path`` will be preserved.
|
||||||
@ -1369,15 +1371,15 @@ class SyncEngine:
|
|||||||
:returns: Relative path with respect to Dropbox folder.
|
:returns: Relative path with respect to Dropbox folder.
|
||||||
:raises ValueError: When the path lies outside the local Dropbox folder.
|
:raises ValueError: When the path lies outside the local Dropbox folder.
|
||||||
"""
|
"""
|
||||||
if not is_equal_or_child(
|
path = os.fsdecode(local_path)
|
||||||
local_path, self.dropbox_path, self.is_fs_case_sensitive
|
|
||||||
):
|
if not is_equal_or_child(path, self.dropbox_path, self.is_fs_case_sensitive):
|
||||||
raise ValueError(f'"{local_path}" is not in "{self.dropbox_path}"')
|
raise ValueError(f'"{path}" is not in "{self.dropbox_path}"')
|
||||||
return "/" + removeprefix(
|
return "/" + removeprefix(
|
||||||
local_path, self.dropbox_path, self.is_fs_case_sensitive
|
path, self.dropbox_path, self.is_fs_case_sensitive
|
||||||
).lstrip("/")
|
).lstrip("/")
|
||||||
|
|
||||||
def to_dbx_path_lower(self, local_path: str) -> str:
|
def to_dbx_path_lower(self, local_path: str | bytes) -> str:
|
||||||
"""
|
"""
|
||||||
Converts a local path to a path relative to the Dropbox folder. The path will be
|
Converts a local path to a path relative to the Dropbox folder. The path will be
|
||||||
normalized as on Dropbox servers (lower case and some additional
|
normalized as on Dropbox servers (lower case and some additional
|
||||||
@ -1414,7 +1416,7 @@ class SyncEngine:
|
|||||||
dbx_path_cased = self.correct_case(dbx_path)
|
dbx_path_cased = self.correct_case(dbx_path)
|
||||||
return self.to_local_path_from_cased(dbx_path_cased)
|
return self.to_local_path_from_cased(dbx_path_cased)
|
||||||
|
|
||||||
def is_excluded(self, path: str) -> bool:
|
def is_excluded(self, path: str | bytes) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a file is excluded from sync. Certain file names are always excluded
|
Checks if a file is excluded from sync. Certain file names are always excluded
|
||||||
from syncing, following the Dropbox support article:
|
from syncing, following the Dropbox support article:
|
||||||
@ -1429,6 +1431,7 @@ class SyncEngine:
|
|||||||
just a file name. Does not need to be normalized.
|
just a file name. Does not need to be normalized.
|
||||||
:returns: Whether the path is excluded from syncing.
|
:returns: Whether the path is excluded from syncing.
|
||||||
"""
|
"""
|
||||||
|
path = os.fsdecode(path)
|
||||||
dirname, basename = osp.split(path)
|
dirname, basename = osp.split(path)
|
||||||
|
|
||||||
# Is in excluded files?
|
# Is in excluded files?
|
||||||
@ -1996,7 +1999,9 @@ class SyncEngine:
|
|||||||
# from sync.
|
# from sync.
|
||||||
|
|
||||||
# mapping of path -> event history
|
# mapping of path -> event history
|
||||||
events_for_path: defaultdict[str, list[FileSystemEvent]] = defaultdict(list)
|
events_for_path: defaultdict[str | bytes, list[FileSystemEvent]] = defaultdict(
|
||||||
|
list
|
||||||
|
)
|
||||||
|
|
||||||
# mapping of source deletion event -> destination creation event
|
# mapping of source deletion event -> destination creation event
|
||||||
moved_from_to: dict[FileSystemEvent, FileSystemEvent] = {}
|
moved_from_to: dict[FileSystemEvent, FileSystemEvent] = {}
|
||||||
@ -2119,8 +2124,8 @@ class SyncEngine:
|
|||||||
|
|
||||||
# 0) Collect all moved and deleted events in sets.
|
# 0) Collect all moved and deleted events in sets.
|
||||||
|
|
||||||
dir_moved_paths: set[tuple[str, str]] = set()
|
dir_moved_paths: set[tuple[str | bytes, str | bytes]] = set()
|
||||||
dir_deleted_paths: set[str] = set()
|
dir_deleted_paths: set[str | bytes] = set()
|
||||||
|
|
||||||
for events in events_for_path.values():
|
for events in events_for_path.values():
|
||||||
event = events[0]
|
event = events[0]
|
||||||
@ -2132,7 +2137,7 @@ class SyncEngine:
|
|||||||
# 1) Combine moved events of folders and their children into one event.
|
# 1) Combine moved events of folders and their children into one event.
|
||||||
|
|
||||||
if len(dir_moved_paths) > 0:
|
if len(dir_moved_paths) > 0:
|
||||||
child_moved_dst_paths: set[str] = set()
|
child_moved_dst_paths: set[str | bytes] = set()
|
||||||
|
|
||||||
# For each event, check if it is a child of a moved event discard it if yes.
|
# For each event, check if it is a child of a moved event discard it if yes.
|
||||||
for events in events_for_path.values():
|
for events in events_for_path.values():
|
||||||
@ -2151,7 +2156,7 @@ class SyncEngine:
|
|||||||
# 2) Combine deleted events of folders and their children to one event.
|
# 2) Combine deleted events of folders and their children to one event.
|
||||||
|
|
||||||
if len(dir_deleted_paths) > 0:
|
if len(dir_deleted_paths) > 0:
|
||||||
child_deleted_paths: set[str] = set()
|
child_deleted_paths: set[str | bytes] = set()
|
||||||
|
|
||||||
for events in events_for_path.values():
|
for events in events_for_path.values():
|
||||||
event = events[0]
|
event = events[0]
|
||||||
@ -3737,7 +3742,7 @@ class SyncEngine:
|
|||||||
|
|
||||||
self._logger.debug('Renamed "%s" to "%s"', local_path_old, event.local_path)
|
self._logger.debug('Renamed "%s" to "%s"', local_path_old, event.local_path)
|
||||||
|
|
||||||
def rescan(self, local_path: str) -> None:
|
def rescan(self, local_path: str | bytes) -> None:
|
||||||
"""
|
"""
|
||||||
Forces a rescan of a local path: schedules created events for every folder,
|
Forces a rescan of a local path: schedules created events for every folder,
|
||||||
modified events for every file and deleted events for every deleted item
|
modified events for every file and deleted events for every deleted item
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
This module contains functions for common path operations.
|
This module contains functions for common path operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
import itertools
|
import itertools
|
||||||
@ -36,7 +38,9 @@ _AnyPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
|
|||||||
# ==== path relationships ==============================================================
|
# ==== path relationships ==============================================================
|
||||||
|
|
||||||
|
|
||||||
def is_child(path: str, parent: str, case_sensitive: bool = True) -> bool:
|
def is_child(
|
||||||
|
path: str | bytes, parent: str | bytes, case_sensitive: bool = True
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if ``path`` semantically is inside ``parent``. Neither path needs to
|
Checks if ``path`` semantically is inside ``parent``. Neither path needs to
|
||||||
refer to an actual item on the drive. This function is case-sensitive.
|
refer to an actual item on the drive. This function is case-sensitive.
|
||||||
@ -49,6 +53,9 @@ def is_child(path: str, parent: str, case_sensitive: bool = True) -> bool:
|
|||||||
if not case_sensitive:
|
if not case_sensitive:
|
||||||
path = normalize(path)
|
path = normalize(path)
|
||||||
parent = normalize(parent)
|
parent = normalize(parent)
|
||||||
|
else:
|
||||||
|
path = os.fsdecode(path)
|
||||||
|
parent = os.fsdecode(parent)
|
||||||
|
|
||||||
parent = parent.rstrip(osp.sep) + osp.sep
|
parent = parent.rstrip(osp.sep) + osp.sep
|
||||||
path = path.rstrip(osp.sep)
|
path = path.rstrip(osp.sep)
|
||||||
@ -56,7 +63,9 @@ def is_child(path: str, parent: str, case_sensitive: bool = True) -> bool:
|
|||||||
return path.startswith(parent)
|
return path.startswith(parent)
|
||||||
|
|
||||||
|
|
||||||
def is_equal_or_child(path: str, parent: str, case_sensitive: bool = True) -> bool:
|
def is_equal_or_child(
|
||||||
|
path: str | bytes, parent: str | bytes, case_sensitive: bool = True
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if ``path`` semantically is inside ``parent`` or equals ``parent``. Neither
|
Checks if ``path`` semantically is inside ``parent`` or equals ``parent``. Neither
|
||||||
path needs to refer to an actual item on the drive. This function is case-sensitive.
|
path needs to refer to an actual item on the drive. This function is case-sensitive.
|
||||||
@ -67,11 +76,7 @@ def is_equal_or_child(path: str, parent: str, case_sensitive: bool = True) -> bo
|
|||||||
:returns: ``True`` if ``path`` semantically lies inside ``parent`` or
|
:returns: ``True`` if ``path`` semantically lies inside ``parent`` or
|
||||||
``path == parent``.
|
``path == parent``.
|
||||||
"""
|
"""
|
||||||
if not case_sensitive:
|
return is_child(path, parent, case_sensitive) or path == parent
|
||||||
path = normalize(path)
|
|
||||||
parent = normalize(parent)
|
|
||||||
|
|
||||||
return is_child(path, parent) or path == parent
|
|
||||||
|
|
||||||
|
|
||||||
# ==== case sensitivity and normalization ==============================================
|
# ==== case sensitivity and normalization ==============================================
|
||||||
@ -98,7 +103,7 @@ def normalize_unicode(string: str) -> str:
|
|||||||
return unicodedata.normalize("NFC", string)
|
return unicodedata.normalize("NFC", string)
|
||||||
|
|
||||||
|
|
||||||
def normalize(string: str) -> str:
|
def normalize(path: str | bytes) -> str:
|
||||||
"""
|
"""
|
||||||
Replicates the path normalization performed by Dropbox servers. This typically only
|
Replicates the path normalization performed by Dropbox servers. This typically only
|
||||||
involves converting the path to lower case, with a few (undocumented) exceptions:
|
involves converting the path to lower case, with a few (undocumented) exceptions:
|
||||||
@ -121,7 +126,7 @@ def normalize(string: str) -> str:
|
|||||||
:param string: Original path.
|
:param string: Original path.
|
||||||
:returns: Normalized path.
|
:returns: Normalized path.
|
||||||
"""
|
"""
|
||||||
return normalize_case(normalize_unicode(string))
|
return normalize_case(normalize_unicode(os.fsdecode(path)))
|
||||||
|
|
||||||
|
|
||||||
def is_fs_case_sensitive(path: str) -> bool:
|
def is_fs_case_sensitive(path: str) -> bool:
|
||||||
@ -417,7 +422,7 @@ def move(
|
|||||||
|
|
||||||
|
|
||||||
def walk(
|
def walk(
|
||||||
root: str,
|
root: str | bytes,
|
||||||
listdir: Callable[[str], Iterable["os.DirEntry[str]"]] = os.scandir,
|
listdir: Callable[[str], Iterable["os.DirEntry[str]"]] = os.scandir,
|
||||||
) -> Iterator[Tuple[str, os.stat_result]]:
|
) -> Iterator[Tuple[str, os.stat_result]]:
|
||||||
"""
|
"""
|
||||||
@ -427,7 +432,7 @@ def walk(
|
|||||||
:param listdir: Function to call to get the folder content.
|
:param listdir: Function to call to get the folder content.
|
||||||
:returns: Iterator over (path, stat) results.
|
:returns: Iterator over (path, stat) results.
|
||||||
"""
|
"""
|
||||||
for entry in listdir(root):
|
for entry in listdir(os.fsdecode(root)):
|
||||||
try:
|
try:
|
||||||
path = entry.path
|
path = entry.path
|
||||||
stat = entry.stat(follow_symlinks=False)
|
stat = entry.stat(follow_symlinks=False)
|
||||||
|
Loading…
Reference in New Issue
Block a user