diff cutils/treesum.py @ 210:1be3af138183

Refactor option handling for configuring symlink handling: now all variations are supported
author Franz Glasner <fzglas.hg@dom66.de>
date Thu, 23 Jan 2025 09:16:51 +0100
parents 85e7edea8ac7
children 5bb0b25f8e99
line wrap: on
line diff
--- a/cutils/treesum.py	Wed Jan 22 19:09:58 2025 +0100
+++ b/cutils/treesum.py	Thu Jan 23 09:16:51 2025 +0100
@@ -16,6 +16,7 @@
 import argparse
 import base64
 import binascii
+import collections
 import datetime
 import logging
 import os
@@ -63,12 +64,19 @@
             help="Put given comment COMMENT into the output as \"COMMENT\". "
                  "Can be given more than once.")
         gp.add_argument(
-            "--follow-directory-symlinks", "-l", action="store_true",
-            dest="follow_directory_symlinks",
-            help="Follow symbolic links to directories when walking a "
-                 "directory tree. Note that this is different from using "
-                 "\"--logical\" or \"--physical\" for arguments given "
-                 "directly on the command line")
+            "--follow-directory-symlinks", "-l", action=SymlinkAction,
+            const="follow-directory-symlinks",
+            default=FollowSymlinkConfig(False, False, True),
+            dest="follow_symlinks",
+            help="""Follow symbolic links to directories when walking a
+directory tree. Augments --physical.""")
+        gp.add_argument(
+            "--follow-file-symlinks", action=SymlinkAction,
+            const="follow-file-symlinks",
+            default=FollowSymlinkConfig(False, False, True),
+            dest="follow_symlinks",
+            help="""Follow symbolic links to files when walking a
+directory tree. Augments --physical.""")
         gp.add_argument(
             "--full-mode", action="store_true", dest="metadata_full_mode",
             help="Consider all mode bits as returned from stat(2) when "
@@ -83,11 +91,13 @@
 `normal' prints just whether Python 2 or Python 3 is used, and `none'
 suppresses the output completely. The default is `normal'.""")
         gp.add_argument(
-            "--logical", "-L", dest="logical", action="store_true",
-            default=None,
-            help="Follow symbolic links given on command line arguments."
-                 " Note that this is a different setting as to follow symbolic"
-                 " links to directories when traversing a directory tree.")
+            "--logical", "-L", action=SymlinkAction, dest="follow_symlinks",
+            const=FollowSymlinkConfig(True, True, True),
+            help="""Follow symbolic links everywhere: on command line
+arguments and -- while walking -- directory and file symbolic links.
+Overwrites any other symlink related options
+(--physical, no-follow-directory-symlinks, no-follow-file-symlinks, et al.).
+""")
         gp.add_argument(
             "--minimal", nargs="?", const="", default=None, metavar="TAG",
             help="Produce minimal output only. If a TAG is given and not "
@@ -108,6 +118,18 @@
                  "generating digests for directories. Digests for files are "
                  "not affected.")
         gp.add_argument(
+            "--no-follow-directory-symlinks", action=SymlinkAction,
+            const="no-follow-directory-symlinks",
+            dest="follow_symlinks",
+            help="""Do not follow symbolic links to directories when walking a
+directory tree. Augments --logical.""")
+        gp.add_argument(
+            "--no-follow-file-symlinks", action=SymlinkAction,
+            const="no-follow-file-symlinks",
+            dest="follow_symlinks",
+            help="""Dont follow symbolic links to files when walking a
+directory tree. Augments --logical.""")
+        gp.add_argument(
             "--no-mmap", action="store_false", dest="mmap", default=None,
             help="Dont use mmap.")
         gp.add_argument(
@@ -115,10 +137,13 @@
             help="Put the checksum into given file. "
                  "If not given or if it is given as `-' then stdout is used.")
         gp.add_argument(
-            "--physical", "-P", dest="logical", action="store_false",
-            default=None,
-            help="Do not follow symbolic links given on comment line "
-                 "arguments. This is the default.")
+            "--physical", "-P", action=SymlinkAction, dest="follow_symlinks",
+            const=FollowSymlinkConfig(False, False, False),
+            help="""Do not follow any symbolic links whether they are given
+on the command line or when walking the directory tree.
+Overwrites any other symlink related options
+(--logical, follow-directory-symlinks, follow-file-symlinks, et al.).
+This is the default.""")
         gp.add_argument(
             "--print-size", action="store_true",
             help="""Print the size of a file or the accumulated sizes of
@@ -249,12 +274,81 @@
     return treesum(opts)
 
 
+FollowSymlinkConfig = collections.namedtuple("FollowSymlinkConfig",
+                                             ["command_line",
+                                              "directory",
+                                              "file"])
+
+
+class SymlinkAction(argparse.Action):
+
+    """`type' is fixed here.
+    `dest' is a tuple with three items:
+
+    1. follow symlinks on the command line
+    2. follow directory symlinks while walking
+    3. follow file symlinks while walking (not yet implemented)
+
+    """
+
+    def __init__(self, *args, **kwargs):
+        if "nargs" in kwargs:
+            raise ValueError("`nargs' not allowed")
+        if "type" in kwargs:
+            raise ValueError("`type' not allowed")
+        c = kwargs.get("const", None)
+        if c is None:
+            raise ValueError("a const value is needed")
+        if (not isinstance(c, FollowSymlinkConfig)
+                and c not in ("follow-directory-symlinks",
+                              "no-follow-directory-symlinks",
+                              "follow-file-symlinks",
+                              "no-follow-file-symlinks")):
+            raise ValueError(
+                "invalid value for the `const' configuration value")
+        default = kwargs.get("default", None)
+        if (default is not None
+                and not isinstance(default, FollowSymlinkConfig)):
+            raise TypeError("invalid type for `default'")
+        kwargs["nargs"] = 0
+        super(SymlinkAction, self).__init__(*args, **kwargs)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        curval = getattr(namespace, self.dest, None)
+        if curval is None:
+            curval = FollowSymlinkConfig(False, False, True)
+        if isinstance(self.const, FollowSymlinkConfig):
+            curval = self.const
+        else:
+            if self.const == "follow-directory-symlinks":
+                curval = FollowSymlinkConfig(
+                    curval.command_line, True, curval.file)
+            elif self.const == "no-follow-directory-symlinks":
+                curval = FollowSymlinkConfig(
+                    curval.command_line, False, curval.file)
+            elif self.const == "follow-file-symlinks":
+                curval = FollowSymlinkConfig(
+                    curval.command_line, curval.directory, True)
+            elif self.const == "no-follow-file-symlinks":
+                curval = FollowSymlinkConfig(
+                    curval.command_line, curval.directory, False)
+            else:
+                assert False, "Implementation error: not yet implemented"
+
+        # Not following symlinks to files is not yet supported: reset to True
+        if not curval.file:
+            curval = FollowSymlinkConfig(
+                curval.command_line, curval.directory, True)
+            logging.warning("Coercing options to `follow-file-symlinks'")
+        setattr(namespace, self.dest, curval)
+
+
 def gen_generate_opts(directories=[],
                       algorithm=util.default_algotag(),
                       append_output=False,
                       base64=False,
                       comment=[],
-                      follow_directory_symlinks=False,
+                      follow_symlinks=FollowSymlinkConfig(False, False, False),
                       full_mode=False,
                       generator="normal",
                       logical=None,
@@ -266,13 +360,21 @@
                       print_size=False,
                       size_only=False,
                       utf8=False):
+    # Not following symlinks to files is not yet supported: reset to True
+    if not isinstance(follow_symlinks, FollowSymlinkConfig):
+        raise TypeError("`follow_symlinks' must be a FollowSymlinkConfig")
+    if not follow_symlinks.file:
+        follow_symlinks = follow_symlinks._make([follow_symlinks.command_line,
+                                                 follow_symlinks.directory,
+                                                 True])
+        logging.warning("Coercing to follow-symlinks-file")
     opts = argparse.Namespace(
         directories=directories,
         algorithm=util.argv2algo(algorithm),
         append_output=append_output,
         base64=base64,
         comment=comment,
-        follow_directory_symlinks=follow_directory_symlinks,
+        follow_symlinks=follow_symlinks,
         generator=generator,
         logical=logical,
         minimal=minimal,
@@ -326,10 +428,9 @@
 
     with out_cm as outfp:
         for d in opts.directories:
-
             V1DirectoryTreesumGenerator(
                 opts.algorithm, opts.mmap, opts.base64,
-                opts.logical, opts.follow_directory_symlinks,
+                opts.follow_symlinks,
                 opts.generator,
                 opts.metadata_mode,
                 opts.metadata_full_mode,
@@ -344,7 +445,7 @@
 class V1DirectoryTreesumGenerator(object):
 
     def __init__(self, algorithm, use_mmap, use_base64,
-                 handle_root_logical, follow_directory_symlinks,
+                 follow_symlinks,
                  with_generator,
                  with_metadata_mode, with_metadata_full_mode,
                  with_metadata_mtime, size_only, print_size, utf8_mode,
@@ -353,8 +454,7 @@
         self._algorithm = algorithm
         self._use_mmap = use_mmap
         self._use_base64 = use_base64
-        self._handle_root_logical = handle_root_logical
-        self._follow_directory_symlinks = follow_directory_symlinks
+        self._follow_symlinks = follow_symlinks
         self._with_generator = with_generator
         self._with_metadata_mode = with_metadata_mode
         self._with_metadata_full_mode = with_metadata_full_mode
@@ -404,10 +504,12 @@
             flags.append("with-metadata-mode")
         if self._with_metadata_mtime:
             flags.append("with-metadata-mtime")
-        if self._handle_root_logical:
-            flags.append("logical")
-        if self._follow_directory_symlinks:
-            flags.append("follow-directory-symlinks")
+        if self._follow_symlinks.command_line:
+            flags.append("follow-symlinks-commandline")
+        if self._follow_symlinks.directory:
+            flags.append("follow-symlinks-directory")
+        if self._follow_symlinks.file:
+            flags.append("follow-symlinks-file")
         if self._size_only:
             flags.append("size-only")
         if self._utf8_mode:
@@ -444,7 +546,7 @@
                 "ROOT", None, walk.WalkDirEntry.alt_u8(root), False))
         self._outfp.flush()
 
-        if not self._handle_root_logical and os.path.islink(root):
+        if not self._follow_symlinks.command_line and os.path.islink(root):
             linktgt = walk.WalkDirEntry.from_readlink(os.readlink(root))
             linkdgst = self._algorithm[0]()
             linkdgst.update(
@@ -480,6 +582,9 @@
                 "CRC32", self._outfp.hexcrcdigest(), None, False))
 
     def _generate(self, root, top):
+        # This is currently always True
+        assert self._follow_symlinks.file
+
         logging.debug("Handling %s/%r", root, top)
         path = os.path.join(root, *top) if top else root
         with walk.ScanDir(path) as dirscan:
@@ -493,7 +598,7 @@
         dir_tainted = False
         for fso in fsobjects:
             if fso.is_dir:
-                if fso.is_symlink and not self._follow_directory_symlinks:
+                if fso.is_symlink and not self._follow_symlinks.directory:
                     linktgt = walk.WalkDirEntry.from_readlink(
                         os.readlink(fso.path))
                     # linktgt = util.fsencode(os.readlink(fso.path)))