"""Rewrite backup revisions whose sharepath is a mapped drive, not a UNC path. WHY THESE EXIST The part-marker client writes its snapshot through the drive letter GE-Enforce mapped (W:), because that is the connection it authenticated - reaching the same server by another name costs a second authentication, or fails outright with error 1219. For a while it also RECORDED that path. A drive letter is not an answer to "where is this backup". W: exists only inside the SYSTEM logon session, only while the enforcement cycle runs, and only on the bay that wrote it. A tech opening the marker's page in ShopDB got a path that resolves to nothing anywhere. The client now records the UNC; this repairs the rows written before it did. WHAT IT CHANGES: backuprevisions.sharepath only. No revision is created, deleted, re-filed or re-hashed, and no file on the share is touched - the bytes are already where the UNC will point. Usage (on the target instance, in the app dir): venv\\Scripts\\python scripts\\fix_drive_letter_sharepaths.py \\ --drive W: --unc "\\\\fileserver\\share\\path" ... --commit # apply; dry run by default The --unc value is what that drive was mapped to - the SHARE root, without the directory the snapshots live in, since that part is already in the stored path. Check it against GE-Enforce.ps1's shopfloorShareRoot, or `net use` on a bay mid-cycle. Take a database backup before --commit. """ import argparse import os import re import sys from pathlib import Path # Run from anywhere: put the repo root (parent of scripts/) on the path. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv # Load .env before anything reads config. Without it DATABASE_URL stays unset # and config falls back to a hardcoded root@localhost URL: on a real deployment # that fails with a bare 1045 Access denied naming a user the site does not use, # and on a box where those credentials happen to work it is worse, because the # script would WRITE to the wrong database. load_dotenv(Path(__file__).resolve().parent.parent / '.env') from shopdb import create_app from shopdb.api import db from plugins.backups.models import BackupRevision DRIVEPATH = re.compile(r'^([A-Za-z]):(\\.*)$') def rewrite(sharepath, drive, unc): """The UNC form of one stored path, or None if it needs no change. Matches on the drive letter alone. A path that is already UNC, or sits on some other drive, is left exactly as it is - this repairs a known mistake rather than normalising everything it can reach. """ if not sharepath: return None match = DRIVEPATH.match(sharepath) if not match: return None if match.group(1).upper() != drive.rstrip(':').upper(): return None return unc.rstrip('\\') + match.group(2) def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('--drive', default='W:', help='Drive letter to rewrite (default W:)') parser.add_argument('--unc', required=True, help=r'Share root that drive pointed at, e.g. \\server\share') parser.add_argument('--kind', default=None, help='Limit to one backup kind (default: every kind)') parser.add_argument('--commit', action='store_true', help='Apply the changes. Dry run by default.') args = parser.parse_args() if not args.unc.startswith('\\\\'): raise SystemExit('--unc must be a UNC path starting with two backslashes') app = create_app() with app.app_context(): query = db.session.query(BackupRevision) if args.kind: query = query.filter(BackupRevision.backupkind == args.kind) changed = 0 for revision in query.order_by(BackupRevision.backuprevisionid).all(): newpath = rewrite(revision.sharepath, args.drive, args.unc) if newpath is None: continue changed += 1 print('revision {} {}\n{}-> {}'.format( revision.backuprevisionid, revision.sharepath, ' ' * (len(str(revision.backuprevisionid)) + 11), newpath)) if args.commit: revision.sharepath = newpath if not changed: print('Nothing to fix: no revision stores a {} path.'.format(args.drive)) return if args.commit: db.session.commit() print('\n{} revision(s) updated.'.format(changed)) else: print('\n{} revision(s) WOULD be updated. Re-run with --commit to apply.' .format(changed)) if __name__ == '__main__': main()