This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Copy and verify existing private local objects into the configured S3 namespace.
|
||||
|
||||
No local objects or database rows are deleted or changed. Stop uploads/workers
|
||||
before copying and keep them stopped until both switch to the same S3 settings.
|
||||
Run with PYTHONPATH=backend and the service .env exported in the environment.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from botocore.exceptions import ClientError
|
||||
from app.config import settings
|
||||
from app.storage import KEY, storage
|
||||
|
||||
|
||||
def migrate(source_dir):
|
||||
config=settings()
|
||||
if config.storage_backend!='s3':raise ValueError('Configure STORAGE_BACKEND=s3 and S3_BUCKET before migrating')
|
||||
root=Path(source_dir).resolve()/'objects'
|
||||
if not root.is_dir() or root.is_symlink():raise ValueError('Source directory must contain the original objects directory (not a symlink)')
|
||||
service=storage();copied=existing=0
|
||||
for path in sorted(root.rglob('*')):
|
||||
if path.is_symlink():raise ValueError('Symlinks are not permitted in the source object tree')
|
||||
if not path.is_file():continue
|
||||
key=path.relative_to(root).as_posix()
|
||||
if not KEY.fullmatch(key):raise ValueError('Unrecognized source object; migration stopped without deleting any files')
|
||||
data=path.read_bytes();digest=hashlib.sha256(data).digest()
|
||||
try:remote=service.get(key)
|
||||
except ClientError as exc:
|
||||
if exc.response.get('Error',{}).get('Code') not in ('NoSuchKey','404','NotFound'):raise
|
||||
else:
|
||||
if hashlib.sha256(remote).digest()!=digest:
|
||||
raise ValueError(f'Destination object differs; refusing to overwrite {key}')
|
||||
existing+=1;continue
|
||||
content_type='application/json' if key.endswith('.json') else 'image/svg+xml' if key.endswith('.svg') else 'application/octet-stream'
|
||||
service.put(key,data,content_type)
|
||||
if hashlib.sha256(service.get(key)).digest()!=digest:raise ValueError('Destination verification failed')
|
||||
copied+=1
|
||||
return {'copied_and_verified':copied,'already_identical':existing}
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source-dir',type=Path,default=None,help='Old STORAGE_DIR, e.g. ./data or /data; must contain objects/')
|
||||
args=parser.parse_args()
|
||||
print(migrate(args.source_dir or settings().storage_dir))
|
||||
|
||||
if __name__=='__main__':main()
|
||||
مرجع در شماره جدید
مسدود کردن کاربر