Files
shopdb-flask/scripts/fix_drive_letter_sharepaths.py

124 lines
4.7 KiB
Python

"""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()