6 changed files with 289 additions and 35 deletions
@ -0,0 +1,157 @@ |
|||||
|
# bsv - Backup, Synchronization, Versioning |
||||
|
# Copyright (C) 2023 Simon Boyé |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as published by |
||||
|
# the Free Software Foundation, either version 3 of the License, or |
||||
|
# (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
|
from __future__ import annotations |
||||
|
|
||||
|
from enum import Enum |
||||
|
from os import stat_result |
||||
|
from pathlib import Path |
||||
|
import stat |
||||
|
|
||||
|
from bsv.object import Digest |
||||
|
from bsv.repository import Repository, Tree, TreeItem |
||||
|
from bsv.util import is_bsv_repository, object_type_from_mode |
||||
|
|
||||
|
|
||||
|
class Action(Enum): |
||||
|
ADD = "add" |
||||
|
UPDATE = "update" |
||||
|
IGNORE = "ignore" |
||||
|
ERROR = "error" |
||||
|
|
||||
|
class IgnoreCause(Enum): |
||||
|
IGNORE_RULE = "ignore_rule" |
||||
|
UNCHANGED = "unchanged" |
||||
|
UNSUPPORTED_TYPE = "unsupported_type" |
||||
|
|
||||
|
|
||||
|
class TreeWalker: |
||||
|
_repo: Repository |
||||
|
_dry_run: bool = False |
||||
|
|
||||
|
def __init__(self, repo: Repository, dry_run: bool=False): |
||||
|
self._repo = repo |
||||
|
self._dry_run = dry_run |
||||
|
|
||||
|
def add_tree(self, path: Path) -> Digest: |
||||
|
pstat = path.stat(follow_symlinks=False) |
||||
|
if self.ignore(path, pstat): |
||||
|
self.report(Action.IGNORE, path, pstat, IgnoreCause.IGNORE_RULE) |
||||
|
return Digest() |
||||
|
return self._add_tree(path, pstat) |
||||
|
|
||||
|
def _add_tree(self, path: Path, pstat: stat_result) -> Digest: |
||||
|
tree = Tree(self._repo, []) |
||||
|
for item in sorted(path.iterdir()): |
||||
|
digest = Digest() |
||||
|
try: |
||||
|
istat = item.lstat() |
||||
|
if self.ignore(item, istat): |
||||
|
self.report(Action.IGNORE, item, istat, IgnoreCause.IGNORE_RULE) |
||||
|
continue |
||||
|
object_type = object_type_from_mode(istat.st_mode) |
||||
|
if object_type == b"slnk": |
||||
|
digest = self._add_symlink(item, istat) |
||||
|
elif object_type == b"tree": |
||||
|
digest = self._add_tree(item, istat) |
||||
|
elif object_type == b"blob": |
||||
|
digest = self._add_blob(item, istat) |
||||
|
else: |
||||
|
self.report(Action.IGNORE, item, istat, IgnoreCause.UNSUPPORTED_TYPE) |
||||
|
continue |
||||
|
except Exception as err: |
||||
|
self.report(Action.ERROR, item, None, err) |
||||
|
continue |
||||
|
|
||||
|
if digest: |
||||
|
self.report(Action.ADD, path, pstat) |
||||
|
tree.items.append(TreeItem( |
||||
|
digest = digest, |
||||
|
object_type = object_type, |
||||
|
size = istat.st_size, |
||||
|
permissions = stat.S_IMODE(istat.st_mode), |
||||
|
modification_timestamp = istat.st_mtime_ns, |
||||
|
name = item.name, |
||||
|
)) |
||||
|
|
||||
|
return self._repo.add_tree(tree, dry_run=self._dry_run) |
||||
|
|
||||
|
|
||||
|
def _add_symlink(self, path: Path, pstat: stat_result) -> Digest: |
||||
|
# TODO: Store symlink relative to current dir ? |
||||
|
# * What about symlink that points outside of the backup dirs |
||||
|
# * Should symlinks that points inside the backup dirs but in another |
||||
|
# mount-point adjusted ? |
||||
|
# * Should absolute symlink be restored as absolute ? |
||||
|
self.report(Action.ADD, path, pstat) |
||||
|
return self._repo._cas.write( |
||||
|
b"slnk", |
||||
|
path.readlink().as_posix().encode("utf-8"), |
||||
|
dry_run = self._dry_run, |
||||
|
) |
||||
|
|
||||
|
def _add_blob(self, path: Path, pstat: stat_result) -> Digest: |
||||
|
self.report(Action.ADD, path, pstat) |
||||
|
with path.open("rb") as stream: |
||||
|
return self._repo.add_blob(stream, dry_run=self._dry_run) |
||||
|
|
||||
|
|
||||
|
def ignore(self, path: Path, pstat: stat_result) -> bool: |
||||
|
return is_bsv_repository(path) |
||||
|
|
||||
|
def report(self, action: Action, path: Path, pstat: stat_result | None, info: IgnoreCause | Exception | None=None): |
||||
|
match action, info: |
||||
|
case (Action.ADD, None): |
||||
|
print(f"Add: {path}") |
||||
|
case (Action.IGNORE, IgnoreCause.IGNORE_RULE): |
||||
|
print(f"Ignore (rule): {path}") |
||||
|
case (Action.IGNORE, IgnoreCause.UNCHANGED): |
||||
|
print(f"Ignore (unchanged): {path}") |
||||
|
case (Action.IGNORE, IgnoreCause.UNSUPPORTED_TYPE) if pstat is not None: |
||||
|
assert pstat is not None |
||||
|
print(f"Ignore (unsupported type {path_type_name(pstat)}): {path}") |
||||
|
case (Action.ERROR, _) if isinstance(info, Exception): |
||||
|
print(f"Error {info}: {path}") |
||||
|
case _: |
||||
|
raise ValueError("TreeWalker.report(): unsupported parameter combination") |
||||
|
|
||||
|
|
||||
|
def path_type_name(pstat: stat_result) -> str: |
||||
|
parts = [] |
||||
|
|
||||
|
if stat.S_ISBLK(pstat.st_mode): |
||||
|
parts.append("block_device") |
||||
|
if stat.S_ISCHR(pstat.st_mode): |
||||
|
parts.append("char_device") |
||||
|
if stat.S_ISDIR(pstat.st_mode): |
||||
|
parts.append("dir") |
||||
|
if stat.S_ISDOOR(pstat.st_mode): |
||||
|
parts.append("door") |
||||
|
if stat.S_ISFIFO(pstat.st_mode): |
||||
|
parts.append("fifo") |
||||
|
if stat.S_ISLNK(pstat.st_mode): |
||||
|
parts.append("symlink") |
||||
|
if stat.S_ISPORT(pstat.st_mode): |
||||
|
parts.append("port") |
||||
|
if stat.S_ISREG(pstat.st_mode): |
||||
|
parts.append("file") |
||||
|
if stat.S_ISSOCK(pstat.st_mode): |
||||
|
parts.append("socket") |
||||
|
if stat.S_ISWHT(pstat.st_mode): |
||||
|
parts.append("whiteout") |
||||
|
|
||||
|
if not parts: |
||||
|
return "unknown" |
||||
|
return ", ".join(parts) |
||||
Loading…
Reference in new issue