diff --git a/a/b b/a/b new file mode 100644 index 0000000..e69de29 diff --git a/a/c b/a/c new file mode 100644 index 0000000..e69de29 diff --git a/a/cc b/a/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/a/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/a/d/b b/a/d/b new file mode 100644 index 0000000..e69de29 diff --git a/a/d/bb b/a/d/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/a/d/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/a/d/c b/a/d/c new file mode 100644 index 0000000..e69de29 diff --git a/a/d/cc b/a/d/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/a/d/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/a/d/d/b b/a/d/d/b new file mode 100644 index 0000000..e69de29 diff --git a/a/d/d/bb b/a/d/d/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/a/d/d/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/a/d/d/c b/a/d/d/c new file mode 100644 index 0000000..e69de29 diff --git a/a/d/d/cc b/a/d/d/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/a/d/d/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/a/d/d/d/b b/a/d/d/d/b new file mode 100644 index 0000000..e69de29 diff --git a/a/d/d/d/bb b/a/d/d/d/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/a/d/d/d/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/a/d/d/d/c b/a/d/d/d/c new file mode 100644 index 0000000..e69de29 diff --git a/a/d/d/d/cc b/a/d/d/d/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/a/d/d/d/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/a/e b/a/e new file mode 100644 index 0000000..04f7545 --- /dev/null +++ b/a/e @@ -0,0 +1 @@ +eyye diff --git a/a/f b/a/f new file mode 100644 index 0000000..5e47e9d --- /dev/null +++ b/a/f @@ -0,0 +1 @@ +ayya diff --git a/a/g/e b/a/g/e new file mode 100644 index 0000000..04f7545 --- /dev/null +++ b/a/g/e @@ -0,0 +1 @@ +eyye diff --git a/a/g/f b/a/g/f new file mode 100644 index 0000000..5e47e9d --- /dev/null +++ b/a/g/f @@ -0,0 +1 @@ +ayya diff --git a/b/b b/b/b new file mode 100644 index 0000000..e69de29 diff --git a/b/bb b/b/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/b/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/b/c b/b/c new file mode 100644 index 0000000..e69de29 diff --git a/b/cc b/b/cc new file mode 100644 index 0000000..e69de29 diff --git a/b/d/b b/b/d/b new file mode 100644 index 0000000..837e3e2 --- /dev/null +++ b/b/d/b @@ -0,0 +1 @@ +eheheh diff --git a/b/d/bb b/b/d/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/b/d/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/b/d/c b/b/d/c new file mode 100644 index 0000000..e69de29 diff --git a/b/d/cc b/b/d/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/b/d/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/b/d/d/b b/b/d/d/b new file mode 100644 index 0000000..e69de29 diff --git a/b/d/d/bb b/b/d/d/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/b/d/d/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/b/d/d/c b/b/d/d/c new file mode 100644 index 0000000..e69de29 diff --git a/b/d/d/cc b/b/d/d/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/b/d/d/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/b/d/d/d/b b/b/d/d/d/b new file mode 100644 index 0000000..e69de29 diff --git a/b/d/d/d/bb b/b/d/d/d/bb new file mode 120000 index 0000000..db89c97 --- /dev/null +++ b/b/d/d/d/bb @@ -0,0 +1 @@ +a/b \ No newline at end of file diff --git a/b/d/d/d/c b/b/d/d/d/c new file mode 100644 index 0000000..e69de29 diff --git a/b/d/d/d/cc b/b/d/d/d/cc new file mode 120000 index 0000000..5e71efa --- /dev/null +++ b/b/d/d/d/cc @@ -0,0 +1 @@ +/home/zy/local/dirf/a/c \ No newline at end of file diff --git a/b/e b/b/e new file mode 100644 index 0000000..04f7545 --- /dev/null +++ b/b/e @@ -0,0 +1 @@ +eyye diff --git a/b/f b/b/f new file mode 100644 index 0000000..4b55eed --- /dev/null +++ b/b/f @@ -0,0 +1 @@ +oyyo diff --git a/b/g/e b/b/g/e new file mode 100644 index 0000000..04f7545 --- /dev/null +++ b/b/g/e @@ -0,0 +1 @@ +eyye diff --git a/b/g/f b/b/g/f new file mode 100644 index 0000000..5e47e9d --- /dev/null +++ b/b/g/f @@ -0,0 +1 @@ +ayya diff --git a/dirf/Config.py b/dirf/Config.py new file mode 100644 index 0000000..831cbed --- /dev/null +++ b/dirf/Config.py @@ -0,0 +1,188 @@ +__all__ = [ "Config" ] + +from typing import Final, Literal, TextIO, cast +import os.path +from portalocker import Lock +from portalocker.exceptions import AlreadyLocked + +from .mytypes import * + + +class Config: + + + _file:Final[TextIO] + _source:Final[str] + _dest:Final[str] + _paths_to_ignore:Final[list[PathAndType]] + _paths_to_sync:Final[list[PathAndType]] + _paths_added_to_ignore:list[PathAndType] + _paths_added_to_sync:list[PathAndType] + def __init__(self, filename:str, *, + source:str|None, dest:str|None, + ) -> None: + try: + self._file = cast(TextIO, + Lock(filename, "r").acquire(fail_when_locked=True) + ) + except AlreadyLocked: + raise Exception(f"the config file '{filename}' is already used.") + lines = self._file.readlines() + + self._paths_to_ignore = [] + self._paths_to_sync = [] + + config_source:tuple[int,str]|None = None + config_dest:tuple[int,str]|None = None + + current_category:list[PathAndType]|None = None + + for i in range(len(lines)): + line = lines[i].rstrip() + head = line.split()[0] + body = line[len(head):].lstrip() + + if line == "": + continue + + elif line.startswith("#"): + continue + + elif head == "SOURCE": + if config_source is not None: + raise Exception(f"line {i+1}: the config already " + + f"defined SOURCE at line {config_source[0]+1}.") + if os.path.isabs(body): + raise Exception(f"line {i+1}: " + + "this ain't an absolute path.") + config_source = i, body + + elif head == "DEST": + if config_dest is not None: + raise Exception(f"line {i+1}: the config already " + + f"defined DEST at line {config_dest[0]+1}.") + body = line[4:].lstrip() + if os.path.isabs(body): + raise Exception(f"line {i+1}: " + + "this ain't an absolute path.") + config_dest = i, body + + elif line == "[IGNORE]": + current_category = self._paths_to_ignore + + elif line == "[SYNC]": + current_category = self._paths_to_sync + + elif head in ["d", "f", "l"]: + head = cast(Literal["d","f","l"], head) + if current_category is None: + raise Exception(f"line {i+1}: " + + "the path is defined before its category name.") + if os.path.isabs(body): + raise Exception(f"line {i+1}: " + + "this must be an absolute path.") + if any([ + body in [v[1] for v in l] + for l in [self._paths_to_sync, self._paths_to_ignore] + ]): + raise Exception(f"line {i+1}: " + + "this path was already used.") + current_category.append( (head, body) ) + + else: + raise Exception(f"line {i+1}: what does that mean") + + if dest is None and config_dest is None: + raise Exception("'dest' is unknown.") + if (dest is not None and config_dest is not None + and dest != config_dest[1] + ): + raise Exception("the 'dest' given to Config() is different " + + "than in the config file.") + self._dest = config_dest[1] if config_dest is not None else ( + cast(str, dest) + ) + + if source is None and config_source is None: + raise Exception("'source' is unknown.") + if (source is not None and config_source is not None + and source != config_source[1] + ): + raise Exception("the 'source' given to Config() is different " + + "than in the config file.") + self._source = config_source[1] if config_source is not None else ( + cast(str, source) + ) + + self._paths_added_to_ignore = [] + self._paths_added_to_sync = [] + + + def toIgnore(self, + path:str, pathtype:PathType + ) -> bool|Literal["different_types"]: + if path in [ + path[1] for path in self._paths_to_ignore + if pathtype != path[0] + ]: + return "different_types" + return path in [ path[1] for path in self._paths_to_ignore ] + + + def toSync(self, + path:str, pathtype:PathType + ) -> bool|Literal["different_types"]: + if path in [ + path[1] for path in self._paths_to_sync + if pathtype != path[0] + ]: + return "different_types" + return path in [ path[1] for path in self._paths_to_sync ] + + + def flush(self) -> None: + if not self._file.writable(): + return + raise NotImplemented + + + def addToIgnore(self, + path:str, pathtype:PathType + ) -> bool|Literal["different_types"]: + if not self._file.writable(): + raise Exception("the file is read-only.") + if not self.toIgnore(path, pathtype) is False: + if self.toIgnore(path, pathtype) == "different_types": + return "different_types" + return False + self._paths_to_ignore.append( (pathtype, path) ) + self._paths_added_to_ignore.append( (pathtype, path) ) + return True + + + def addToSync(self, + path:str, pathtype:PathType + ) -> bool|Literal["different_types"]: + if not self._file.writable(): + raise Exception("the file is read-only.") + if not self.toSync(path, pathtype) is False: + if self.toSync(path, pathtype) == "different_types": + return "different_types" + return False + self._paths_to_sync.append( (pathtype, path) ) + self._paths_added_to_sync.append( (pathtype, path) ) + return True + + def __enter__(self) -> "Config": + return self + + def __exit__(self, *_) -> None: + self.flush() + + def __del__(self) -> None: + self.flush() + + def getSource(self) -> str: + return self._source + def getDest(self) -> str: + return self._dest diff --git a/dirf/__init__.py b/dirf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dirf/__main__.py b/dirf/__main__.py new file mode 100644 index 0000000..cc7d323 --- /dev/null +++ b/dirf/__main__.py @@ -0,0 +1,9 @@ +import argparse + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("") + + +if __name__ == "__main__": + main() diff --git a/dirf/diff.py b/dirf/diff.py new file mode 100644 index 0000000..195294b --- /dev/null +++ b/dirf/diff.py @@ -0,0 +1,162 @@ +__add__ = [ "diff" ] + +from collections.abc import Set +from os import listdir, readlink +from os.path import abspath +from typing import Callable, Final +from collections.abc import Iterable + +from .mytypes import * +from .Config import * +from .utils import * +from .whatsthispath import * +from .utils import * + + +class DiffResult: + def __init__(self, path:str) -> None: + self.path:Final[str] = path + self.differs:set[PathAndType] = set() + self.same:set[PathAndType] = set() + self.differentTypes:set[tuple[str,PathType,PathType]] = set() + self.presentOnlyInFirst:set[PathAndType] = set() + self.presentOnlyInSecond:set[PathAndType] = set() + self.weird:set[str] = set() + self.subresults:dict[str,DiffResult] = {} + + def simplify(self, *, deep:bool) -> None: + for l in [ + self.differs, self.same, self.presentOnlyInFirst, + self.presentOnlyInSecond + ]: + mapSet(l, ( lambda v: ( v[0], simplifyPath(v[1]) ) )) + mapSet(self.differentTypes, ( + lambda v: ( simplifyPath(v[0]), *v[1:] ) + )) + mapSet(self.weird, simplifyPath) + if deep: + for v in self.subresults.values(): + v.simplify(deep=True) + + def print(self, source:str|None = None, target:str|None = None) -> None: + reprSource = repr(source) if source is not None else "the source directory" + reprTarget = repr(target) if target is not None else "the target directory" + + def reprPathAndType(v:PathAndType) -> str: + return f"{lineForPathAndType(v)}" + def printWith[T](label:str, what:set[T], f:Callable[[T],str]): + if len(what) == 0: + return + print(label + ":") + for v in what: + print(f"-\t{f(v)}") + title(f"for path {repr(self.path)}") + + printWith("same", self.same, reprPathAndType) + + printWith("different", self.differs, reprPathAndType) + + printWith("different types", self.differentTypes, + lambda v: f"{reprPathType(v[1])} / {reprPathType(v[2])}: {v[0]}" + ) + + printWith(f"exist only in {reprSource}", + self.presentOnlyInFirst, reprPathAndType + ) + + printWith(f"exist only in {reprTarget}", + self.presentOnlyInSecond, reprPathAndType + ) + + printWith("dunno what that is lol", self.weird, + lambda v: v + ) + + +def diff(pathA:str, pathB:str, *, ignored:Set[PathAndType]) -> DiffResult: + r = _diffDirs(abspath(pathA), abspath(pathB), ignored, "/", trust=False) + return r + + +def _diffDirs( + absPathA:str, absPathB:str, absIgnoredPaths:Set[PathAndType], + relCurrentPath:str, *, trust:bool +) -> DiffResult: + + absCurrentPathA = absPathA + "/" + relCurrentPath + absCurrentPathB = absPathB + "/" + relCurrentPath + absCurrentPathA_what = whatsthispath(absCurrentPathA) + absCurrentPathB_what = whatsthispath(absCurrentPathB) + + if not trust: + assert absPathA[0] == "/", "'pathA' should be an absolute path." + assert absPathB[0] == "/", "'pathB' should be an absolute path." + assert relCurrentPath[0] == "/", ( + "'relCurrentPath' should start with '/'." + ) + assert all([ v[1][0] == "/" for v in absIgnoredPaths ]), ( + "'ignoredPaths' must contain only absolute paths." + ) + assert absCurrentPathA_what == 'd', "'pathA' should be a directory." + assert absCurrentPathB_what == 'd', "'pathB' should be a directory." + + r = DiffResult(relCurrentPath) + + for name in set(listdir(absCurrentPathA)) | set(listdir(absCurrentPathB)): + relNewPath = relCurrentPath + "/" + name + absNewPathA = absCurrentPathA + "/" + name + absNewPathB = absCurrentPathB + "/" + name + absNewPathA_what = whatsthispath(absNewPathA) + absNewPathB_what = whatsthispath(absNewPathB) + if absNewPathA_what is None: + assert absNewPathB_what is not None + r.presentOnlyInSecond.add((absNewPathB_what, relNewPath)) + elif absNewPathB_what is None: + r.presentOnlyInFirst.add((absNewPathA_what, relNewPath)) + elif absNewPathA_what != absNewPathB_what: + r.differentTypes.add( + (relNewPath, absNewPathA_what, absNewPathB_what) + ) + else: + _diffOne(r, + absPathA, absPathB, absIgnoredPaths, relNewPath, name, + absNewPathA_what + ) + + r.simplify(deep=False) + return r + + +def _diffOne(r:DiffResult, absPathA:str, absPathB:str, absIgnoredPaths:Set[PathAndType], + relCurrentPath:str, filename:str, relCurrentPath_what:PathType +) -> None: + match relCurrentPath_what: + case "d": + rr = _diffDirs(absPathA, absPathB, absIgnoredPaths, relCurrentPath, trust=False) # TODO(debug) add trust=True + r.subresults[filename] = rr + if (len(rr.differentTypes) > 0 + or len(rr.presentOnlyInFirst) > 0 + or len(rr.presentOnlyInSecond) > 0 + or len(rr.differs) > 0 + ): + r.differs.add( (relCurrentPath_what, filename) ) + elif len(rr.weird) > 0: + r.weird.add( filename ) + else: + r.same.add( ( relCurrentPath_what, filename ) ) + + case "f"|"l": + getData:Callable[[str],str|bytes] = ( + (lambda path: open(path, "rb").read()) + if relCurrentPath_what == "f" else + (lambda path: readlink(path)) + ) + ( + r.same if ( + getData(absPathA + "/" + relCurrentPath) + == getData(absPathB + "/" + relCurrentPath) + ) else r.differs + ).add( (relCurrentPath_what, filename) ) + + case "w": + r.weird.add( filename ) diff --git a/dirf/mytypes.py b/dirf/mytypes.py new file mode 100644 index 0000000..75e4a12 --- /dev/null +++ b/dirf/mytypes.py @@ -0,0 +1,28 @@ +__all__ = [ + "PathType", + "PathAndType", + "reprPathType", + "lineForPathAndType", +] + +from typing import Literal + + +type PathType = Literal["d","f","l","w"] + +def reprPathType(t:PathType) -> str: + match t: + case "d": + return "directory" + case "f": + return "file" + case "l": + return "symbolic link" + case "w": + return "unknown type" + + +type PathAndType = tuple["PathType",str] + +def lineForPathAndType(v:PathAndType) -> str: + return f"{reprPathType(v[0])}: {v[1]}" diff --git a/dirf/utils.py b/dirf/utils.py new file mode 100644 index 0000000..fed4481 --- /dev/null +++ b/dirf/utils.py @@ -0,0 +1,25 @@ +__all__ = [ + "title", + "simplifyPath", + "mapSet", +] + +from typing import Callable + + +def title(txt:str) -> None: + return print(f"\033[1m{txt}\033[0m") + + +def simplifyPath(path:str) -> str: + start = "/" if path[0] == "/" else "" + return start + "/".join( + [ el for el in path.split("/") if el != "" ] + ) + + +def mapSet[T](s:set[T], f:Callable[[T],T]) -> None: + s2:set[T] = set() + while len(s) > 0: + s2.add(f(s.pop())) + s.update(s2) diff --git a/dirf/whatsthispath.py b/dirf/whatsthispath.py new file mode 100644 index 0000000..4d8e197 --- /dev/null +++ b/dirf/whatsthispath.py @@ -0,0 +1,17 @@ +__all__ = [ "whatsthispath" ] + +import os.path + +from .mytypes import * + + +def whatsthispath(path:str) -> PathType|None: + if os.path.islink(path): + return "l" + if os.path.isdir(path): + return "d" + if os.path.isfile(path): + return "f" + if os.path.exists(path): + return "w" + return None