#!/usr/bin/env python3 # File: s2slib.py # Locations: # /mnt/public/Support/Programs/shortcuts/ # https://gitlab.com/bgstack15/s2s # Author: bgstack15@gmail.com # SPDX-License-Identifier: GPL-3.0 # Startdate: 2021-02-11 20:00 # Title: Shortcut To Symlink library # Purpose: Convert .lnk files to POSIX symlinks within a single filesystem # History: # Usage: # see s2s.py # Reference: # https://stackoverflow.com/questions/25146960/python-convert-back-slashes-to-forward-slashes/25147093#25147093 # https://github.com/libyal/liblnk/blob/main/documentation/Windows%20Shortcut%20File%20(LNK)%20format.asciidoc#file_attribute_flags # Improve: # Handle absolute paths when relative paths are absent? # Add a force=True option to overwrite existing symlink that points to something else # Dependencies: # devuan-req: python3-liblnk import pylnk # python3-liblnk import os, re, posixpath from sys import stderr debuglevel = 8 # Functions def eprint(*args, **kwargs): print(*args, file=stderr, **kwargs) # to benefit from caching outside of the function, maybe lnkregex = re.compile(".*\.lnk$") def replace_one_symlink( lnkfile , delete = False , debuglevel = debuglevel , dryrun = False ): thislnk = pylnk.file() basename = os.path.basename(lnkfile) #workdir = os.path.dirname(lnkfile) # not used # Exit if this is not a .lnk file if not re.match(lnkregex,basename): eprint(f"Cannot operate on {lnkfile}") return -1 thislnk.open(lnkfile) # Get relative path # we are really expecting a relative path for every single one, because we are dealing with Windows shortcuts to files all on the same network share. # Although I suppose a symlink could work if you could somehow map the expected SMB path to a POSIX filesystem try: relative_path = thislnk.relative_path except: eprint(f"No relative path for {lnkfile}, and abs path not yet implemented.") return -1 # and make it a Unix-style path relative_path = posixpath.join(*thislnk.relative_path.split('\\')) # Not used for anything, but here if we want it. linktype = "file" if thislnk.file_attribute_flags & 0x10: linktype = "dir" # Get preferred name, and strip any " - Shortcut" pn = os.path.splitext(lnkfile)[:-1][0] # remove the .lnk part, but preserve any other dots symlink_path = re.sub(' - Shortcut$', "",pn) if linktype == "dir" symlink_path += ".shortcut" # take action if debuglevel >= 5: eprint(f"Want symlink \"{symlink_path}\" ({linktype})--> \"{relative_path}\"") ready_to_delete = False if os.path.exists(symlink_path): current_source = "" try: current_source = os.path.realpath(symlink_path) except: eprint(f"Unable to determine current_source for {symlink_path} which already exists.") normpath = os.path.normpath(os.path.join(os.path.dirname(symlink_path),relative_path)) #eprint(f"Please ensure {current_source} matches {relative_path} which is {normpath}") if normpath != current_source: # The existing symlink points to something else. Not sure how to proceed. # probably need to add a --force option to overwrite eprint(f"Warning: Existing symlink {symlink_path} points to {current_source} and not {normpath} as requested") else: # it exists and already points to what we want it to point to. if debuglevel >= 3: eprint(f"symlink {symlink_path} already points to {current_source} which is {normpath}") ready_to_delete = True else: # symlink does not exist, so make it! if debuglevel >= 1: eprint(f"ln -s {relative_path} {symlink_path}") if not dryrun: try: os.symlink(relative_path,symlink_path) ready_to_delete = True except Exception as e: eprint(e) if delete: if ready_to_delete: if debuglevel >= 2: eprint(f"rm {lnkfile}") if not dryrun: os.remove(lnkfile) return 0 def list_all_child_lnk_files(indir): mylist = [] for root, dirs, files in os.walk(indir): for name in files: if ".lnk" in name: mylist.append(os.path.join(root,name)) #eprint(os.path.join(root,name)) return mylist def replace_all_symlinks( indir , delete = False , debuglevel = debuglevel , dryrun = False ): result = 0 # get list of all lnk files mylist = list_all_child_lnk_files(indir) for lnkfile in mylist: replace_one_symlink( lnkfile , delete = delete , debuglevel = debuglevel , dryrun = dryrun ) return result if __name__ == "__main__": print("running the main loop")