File: PC-Phone USB Sync/restore-symlinks-from-backup.py

#!/usr/bin/env python3
"""
======================================================================================
restore-symlinks-from-backup.py - a PC-Phone USB Sync app utility script.

This script has the same terms of use as that of the app.  It's available
online at quixotely.com/PC-Phone USB Sync/restore-symlinks-from-backup.py.

Run immediately after a SYNC, with a command line like this:

    python3 restore-symlinks-from-backup.py <TO-path>

Where the required <TO-Path> argument is the relative or absolute pathname
of the content folder which was TO in the preceding SYNC.  This can be run
in Terminal on Linux, and Command Prompt, Power Shell, and WSL on Windows.
You must have a Python 3.x installed on the device where you run this. 

This script restores all symlinks (only) in TO that were removed or changed 
by the preceding SYNC, and were saved by the SYNC in TO's __bkp__ backups 
folder.  Use this to restore symlinks on Linux or Windows which were deleted 
because they did not exist on the removable drive which was FROM in the SYNC.

macOS doesn't require this because it forges symlinks on removable drives,
so they will generally survive roundtrips to other platforms.  Android 
doesn't require this because it doesn't support symlinks in usable folders.

This script suffices in this specific usage context, but in general, you're 
better off avoiding symlinks altogether in content folders propagated to 
removable drives and devices that don't support them.  See example ahead.

Caution: this works only after a SYNC that removes symlinks.  It will restore
symlinks in TO whether they were deleted by omission in FROM, or changed by 
differences in FROM.  Use this only if you're sure that the SYNC removed
symlinks in TO because they were absent on a FROM removable drive (or other).

More details:
    https://quicotely.com/PC-Phone USB Sync/Tech-Notes.html#n1

See also:
    https://learning-python.com/android-deltas-sync/_etc/find-all-symlinks.py
======================================================================================
"""


import sys, os, glob, shutil

LISTONLY = False    # True = list backed-up symlinks without restoring them

def exit(msg, label=True): 
    print('Error: ' + msg if label else msg)
    sys.exit(1)

if len(sys.argv) != 2:
    exit('Usage: python3 <TO-path>', label=False)

TO = sys.argv[1]
if not os.path.isdir(TO):
    exit('TO path is not a folder')

TObkp = TO + os.sep + '__bkp__'
if not os.path.isdir(TObkp):
    exit('TO path has no __bkp__ folder')

bkppatt = 'date??????-time??????'                     # ignore '*--UNDONE', '__undoable__'
backups = glob.glob(os.path.join(TObkp, bkppatt))     # mergeall prunes use 'date*-time*'
if not backups:
    exit('TO path has no usable backups')

latestbackup = sorted(backups)[-1]                     # latest by date/time folder name
numlinks = 0

for (dirhere, subshere, fileshere) in os.walk(latestbackup):
    for filename in fileshere + subshere:                      # links in both lists
        bkppath = os.path.join(dirhere, filename)
        if os.path.islink(bkppath):
            numlinks += 1

            if LISTONLY:
                print('Found:', bkppath)
                continue

            # map TO/__bkp__/date-time/path to TO/path
            bkpprefix = len(latestbackup) + len(os.sep)
            TOpath = os.path.join(TO, bkppath[bkpprefix:])

            # partly yoinked from Mergeall's cpall.copylink() [tbd: FWP()?]

            # windows dir-link arg
            if sys.platform.startswith('win') and os.path.isdir(bkppath):
                dirarg = dict(target_is_directory=True)
            else:
                dirarg ={}

            # remove current link if present           # lexists: link, not its target
            if os.path.lexists(TOpath):                # else os.symlink() will fail
                os.remove(TOpath)

            # copy linkpath over 
            linkPath = os.readlink(bkppath)            # the from link's pathname str
            os.symlink(linkPath, TOpath, **dirarg)     # store pathname as new link
            shutil.copystat(bkppath, TOpath, follow_symlinks=False)

            # report
            print('Restored: [%s] => [%s]' % (bkppath, TOpath))

print('Done: %s backed-up symlinks found %s.' %
                      (numlinks, ('(only)' if LISTONLY else 'and restored')))



"""
======================================================================================
Example output:

% python3 restore-symlinks-from-backup.py test-restore 
Restored: [/Users/me/Desktop/test-restore/__bkp__/date010203-time010203/link-file.txt] => [/Users/me/Desktop/test-restore/link-file.txt]
Restored: [/Users/me/Desktop/test-restore/__bkp__/date010203-time010203/link-folder-file.txt] => [/Users/me/Desktop/test-restore/link-folder-file.txt]
Restored: [/Users/me/Desktop/test-restore/__bkp__/date010203-time010203/link-folder.txt] => [/Users/me/Desktop/test-restore/link-folder.txt]
Restored: [/Users/me/Desktop/test-restore/__bkp__/date010203-time010203/folder/link-parent-file.txt] => [/Users/me/Desktop/test-restore/folder/link-parent-file.txt]
Restored: [/Users/me/Desktop/test-restore/__bkp__/date010203-time010203/folder/link-file.txt] => [/Users/me/Desktop/test-restore/folder/link-file.txt]
Restored: [/Users/me/Desktop/test-restore/__bkp__/date010203-time010203/folder/link-parent.txt] => [/Users/me/Desktop/test-restore/folder/link-parent.txt]
Done: 6 backed-up symlinks found and restored.
======================================================================================
"""