diff --git a/app/static/uploads/14b1a79b788446e7a1f92d96aa938497_Promised_land.png b/app/static/uploads/14b1a79b788446e7a1f92d96aa938497_Promised_land.png deleted file mode 100644 index 4d17129..0000000 Binary files a/app/static/uploads/14b1a79b788446e7a1f92d96aa938497_Promised_land.png and /dev/null differ diff --git a/app/static/uploads/17128b355b264d0784ff7f51dca2ae63_Wasteland-673x1024.png b/app/static/uploads/17128b355b264d0784ff7f51dca2ae63_Wasteland-673x1024.png deleted file mode 100644 index 388c95b..0000000 Binary files a/app/static/uploads/17128b355b264d0784ff7f51dca2ae63_Wasteland-673x1024.png and /dev/null differ diff --git a/app/static/uploads/4d2d254cbc554c8ba9b7f89443270d89_Sapiens.jpg b/app/static/uploads/4d2d254cbc554c8ba9b7f89443270d89_Sapiens.jpg deleted file mode 100644 index 34687a4..0000000 Binary files a/app/static/uploads/4d2d254cbc554c8ba9b7f89443270d89_Sapiens.jpg and /dev/null differ diff --git a/app/static/uploads/52931d6b6e1a4f3e88258b3a9971c2e2_John-Doerr-Measure-What-Matters-OKRs-The-Simple-Idea-that-Drives-10x-Growth-670x1024.jpg b/app/static/uploads/52931d6b6e1a4f3e88258b3a9971c2e2_John-Doerr-Measure-What-Matters-OKRs-The-Simple-Idea-that-Drives-10x-Growth-670x1024.jpg deleted file mode 100644 index 2ae4cb0..0000000 Binary files a/app/static/uploads/52931d6b6e1a4f3e88258b3a9971c2e2_John-Doerr-Measure-What-Matters-OKRs-The-Simple-Idea-that-Drives-10x-Growth-670x1024.jpg and /dev/null differ diff --git a/app/static/uploads/5d4a2e0c62b7439c84d55eb297e9e0a8_Becoming.png b/app/static/uploads/5d4a2e0c62b7439c84d55eb297e9e0a8_Becoming.png deleted file mode 100644 index 80ce97b..0000000 Binary files a/app/static/uploads/5d4a2e0c62b7439c84d55eb297e9e0a8_Becoming.png and /dev/null differ diff --git a/app/static/uploads/6955c7e6e66342b1a63fd177a5ef5c67_The_science_of_living.png b/app/static/uploads/6955c7e6e66342b1a63fd177a5ef5c67_The_science_of_living.png deleted file mode 100644 index 9ec8156..0000000 Binary files a/app/static/uploads/6955c7e6e66342b1a63fd177a5ef5c67_The_science_of_living.png and /dev/null differ diff --git a/app/static/uploads/79dadba91c1e441f8fa867760c2688f6_mans_seaching_for_meaning.png b/app/static/uploads/79dadba91c1e441f8fa867760c2688f6_mans_seaching_for_meaning.png deleted file mode 100644 index 3432eae..0000000 Binary files a/app/static/uploads/79dadba91c1e441f8fa867760c2688f6_mans_seaching_for_meaning.png and /dev/null differ diff --git a/app/static/uploads/7afcce6baf3e410090917ca8cb56cdc4_coaching_for_performace.png b/app/static/uploads/7afcce6baf3e410090917ca8cb56cdc4_coaching_for_performace.png deleted file mode 100644 index 7d1e702..0000000 Binary files a/app/static/uploads/7afcce6baf3e410090917ca8cb56cdc4_coaching_for_performace.png and /dev/null differ diff --git a/app/static/uploads/897ff6b6b14b4a1cb520c1f7a30cb05c_transformational_hr.png b/app/static/uploads/897ff6b6b14b4a1cb520c1f7a30cb05c_transformational_hr.png deleted file mode 100644 index 6696035..0000000 Binary files a/app/static/uploads/897ff6b6b14b4a1cb520c1f7a30cb05c_transformational_hr.png and /dev/null differ diff --git a/app/static/uploads/946362cb8b9f4f9d8eb85bac147d1af8_Emotional_Inteligence.png b/app/static/uploads/946362cb8b9f4f9d8eb85bac147d1af8_Emotional_Inteligence.png deleted file mode 100644 index 13c4e3b..0000000 Binary files a/app/static/uploads/946362cb8b9f4f9d8eb85bac147d1af8_Emotional_Inteligence.png and /dev/null differ diff --git a/app/static/uploads/9a833b4100d042fcb0f1546b36942301_american_dirt.png b/app/static/uploads/9a833b4100d042fcb0f1546b36942301_american_dirt.png deleted file mode 100644 index ff7c3c3..0000000 Binary files a/app/static/uploads/9a833b4100d042fcb0f1546b36942301_american_dirt.png and /dev/null differ diff --git a/app/static/uploads/9f9cd01861e840769cbb88ca6899464c_ALiveOnOurPlanet.png b/app/static/uploads/9f9cd01861e840769cbb88ca6899464c_ALiveOnOurPlanet.png deleted file mode 100644 index d229888..0000000 Binary files a/app/static/uploads/9f9cd01861e840769cbb88ca6899464c_ALiveOnOurPlanet.png and /dev/null differ diff --git a/app/static/uploads/a3022c997e294c46ad9cb6dca084fa05_phyc_book.png b/app/static/uploads/a3022c997e294c46ad9cb6dca084fa05_phyc_book.png deleted file mode 100644 index 1f4e766..0000000 Binary files a/app/static/uploads/a3022c997e294c46ad9cb6dca084fa05_phyc_book.png and /dev/null differ diff --git a/app/static/uploads/a7434856541c40a9b1e278e34ae6ea10_the_humans.jpg b/app/static/uploads/a7434856541c40a9b1e278e34ae6ea10_the_humans.jpg deleted file mode 100644 index 5d9163f..0000000 Binary files a/app/static/uploads/a7434856541c40a9b1e278e34ae6ea10_the_humans.jpg and /dev/null differ diff --git a/app/static/uploads/c322c12fa1bc4d3ab75c5e52b323d0c3_Scattered_Minds.png b/app/static/uploads/c322c12fa1bc4d3ab75c5e52b323d0c3_Scattered_Minds.png deleted file mode 100644 index 0049922..0000000 Binary files a/app/static/uploads/c322c12fa1bc4d3ab75c5e52b323d0c3_Scattered_Minds.png and /dev/null differ diff --git a/app/static/uploads/c32e7e8850be454e8eafa481b42e1a7a_chimp.png b/app/static/uploads/c32e7e8850be454e8eafa481b42e1a7a_chimp.png deleted file mode 100644 index 3f2e093..0000000 Binary files a/app/static/uploads/c32e7e8850be454e8eafa481b42e1a7a_chimp.png and /dev/null differ diff --git a/app/static/uploads/c44ff271e7eb4feb8424e91637ec1e4b_Rise.png b/app/static/uploads/c44ff271e7eb4feb8424e91637ec1e4b_Rise.png deleted file mode 100644 index d0b2b5b..0000000 Binary files a/app/static/uploads/c44ff271e7eb4feb8424e91637ec1e4b_Rise.png and /dev/null differ diff --git a/app/static/uploads/cae6bb3a3ca44481bc1db3f1efe12ef5_HowToBreakUpWithYourPhone.png b/app/static/uploads/cae6bb3a3ca44481bc1db3f1efe12ef5_HowToBreakUpWithYourPhone.png deleted file mode 100644 index 11c88d2..0000000 Binary files a/app/static/uploads/cae6bb3a3ca44481bc1db3f1efe12ef5_HowToBreakUpWithYourPhone.png and /dev/null differ diff --git a/app/static/uploads/d18d611af55b45ffb7879999df9d9dad_NowDiscoverYourStrengths.png b/app/static/uploads/d18d611af55b45ffb7879999df9d9dad_NowDiscoverYourStrengths.png deleted file mode 100644 index 9d6f421..0000000 Binary files a/app/static/uploads/d18d611af55b45ffb7879999df9d9dad_NowDiscoverYourStrengths.png and /dev/null differ diff --git a/app/static/uploads/d2b4e33e40aa4a38a54ab8a6efd00eb4_Invisible-Women-RBD.png b/app/static/uploads/d2b4e33e40aa4a38a54ab8a6efd00eb4_Invisible-Women-RBD.png deleted file mode 100644 index d9a8ef7..0000000 Binary files a/app/static/uploads/d2b4e33e40aa4a38a54ab8a6efd00eb4_Invisible-Women-RBD.png and /dev/null differ diff --git a/app/static/uploads/e05a0b9cd98a44cdbd1a6503398d03c4_HowToAvoidAClimateDisaster.png b/app/static/uploads/e05a0b9cd98a44cdbd1a6503398d03c4_HowToAvoidAClimateDisaster.png deleted file mode 100644 index 3c62bda..0000000 Binary files a/app/static/uploads/e05a0b9cd98a44cdbd1a6503398d03c4_HowToAvoidAClimateDisaster.png and /dev/null differ diff --git a/app/static/uploads/e814dc1f06234769a201f5403cde297c_Show_me_the_numbers.png b/app/static/uploads/e814dc1f06234769a201f5403cde297c_Show_me_the_numbers.png deleted file mode 100644 index a010ab8..0000000 Binary files a/app/static/uploads/e814dc1f06234769a201f5403cde297c_Show_me_the_numbers.png and /dev/null differ diff --git a/app/static/uploads/f95771e321b1483ba5adcbc2976fcc0c_download.jpeg b/app/static/uploads/f95771e321b1483ba5adcbc2976fcc0c_download.jpeg deleted file mode 100644 index 5b00f95..0000000 Binary files a/app/static/uploads/f95771e321b1483ba5adcbc2976fcc0c_download.jpeg and /dev/null differ diff --git a/app/static/uploads/f985bd2240054d218384b9dea03bad00_Why_we_sleep.png b/app/static/uploads/f985bd2240054d218384b9dea03bad00_Why_we_sleep.png deleted file mode 100644 index 4643cea..0000000 Binary files a/app/static/uploads/f985bd2240054d218384b9dea03bad00_Why_we_sleep.png and /dev/null differ diff --git a/app/storage.py b/app/storage.py index f4cf761..ec9d8e6 100644 --- a/app/storage.py +++ b/app/storage.py @@ -81,18 +81,8 @@ def presign_put(extension: str) -> dict[str, str]: def public_url(key: str) -> str: - """Resolve a stored S3 key to a public HTTPS URL. - - Legacy-filename shim: rows written by the pre-S3 code stored bare - filenames like ``_.png``. We deploy this code before - running ``scripts/migrate_uploads_to_s3.py``, so during the cutover - window we'll see both bare filenames and ``books/.`` keys. - Once the migration runs, every row carries the prefix and the - ``"/" not in key`` branch becomes dead code — remove it then. - """ + """Resolve a stored S3 key to a public HTTPS URL.""" if not key: return "" - if "/" not in key: - key = f"{UPLOAD_PREFIX}{key}" base = current_app.config["S3_PUBLIC_BASE_URL"].rstrip("/") return f"{base}/{key}" diff --git a/entrypoint.sh b/entrypoint.sh index c74b105..20837aa 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -14,13 +14,6 @@ export DATABASE_URL="postgresql+psycopg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:$ # task sees `head` and no-ops. flask db upgrade -# One-shot data migration: copy the legacy disk-based book covers into S3 -# and prefix the DB rows with `books/`. Idempotent — re-running is a fast -# no-op once every file is in S3 and every row is prefixed. Concurrent -# boots are safe because both phases tolerate redoing already-done work. -# Removable once `app/static/uploads/` is deleted from the repo. -python scripts/migrate_uploads_to_s3.py - # `--access-logfile -` writes access logs to stdout so they land in # CloudWatch via the awslogs driver. 3 sync workers fits 1 vCPU comfortably. exec gunicorn \ diff --git a/scripts/migrate_uploads_to_s3.py b/scripts/migrate_uploads_to_s3.py deleted file mode 100644 index 5cb2013..0000000 --- a/scripts/migrate_uploads_to_s3.py +++ /dev/null @@ -1,130 +0,0 @@ -"""One-shot: copy app/static/uploads/* to S3 and prefix existing DB rows. - -Runs once per environment as part of the disk -> S3 cutover. Idempotent — -re-running uploads only objects S3 doesn't already have, and updates only -DB rows that don't yet carry the ``books/`` prefix. - -Usage:: - - DATABASE_URL=... S3_BUCKET=... AWS_REGION=... \\ - python scripts/migrate_uploads_to_s3.py - -Optional env: - S3_ENDPOINT_URL point at MinIO/LocalStack instead of AWS - DRY_RUN=1 report what would happen without writing -""" - -from __future__ import annotations - -import mimetypes -import os -import sys -from pathlib import Path - -import boto3 -from botocore.exceptions import ClientError - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT)) - -from app import create_app # noqa: E402 -from app.extensions import db # noqa: E402 -from app.models import Book # noqa: E402 -from app.storage import UPLOAD_PREFIX # noqa: E402 - -UPLOADS_DIR = REPO_ROOT / "app" / "static" / "uploads" -DRY_RUN = os.environ.get("DRY_RUN") == "1" - - -def _s3_client(app): - return boto3.client( - "s3", - region_name=app.config["S3_REGION"], - endpoint_url=app.config.get("S3_ENDPOINT_URL") or None, - ) - - -def _exists(s3, bucket: str, key: str) -> bool: - try: - s3.head_object(Bucket=bucket, Key=key) - return True - except ClientError as e: - if e.response["Error"]["Code"] in ("404", "NoSuchKey", "NotFound"): - return False - raise - - -def upload_files(app) -> int: - if not UPLOADS_DIR.is_dir(): - print(f"No uploads directory at {UPLOADS_DIR} — nothing to copy.") - return 0 - - bucket = app.config["S3_BUCKET"] - s3 = _s3_client(app) - uploaded = 0 - - for path in sorted(UPLOADS_DIR.iterdir()): - if not path.is_file(): - continue - key = f"{UPLOAD_PREFIX}{path.name}" - if _exists(s3, bucket, key): - print(f" skip (exists): {key}") - continue - content_type, _ = mimetypes.guess_type(path.name) - if DRY_RUN: - print(f" would upload: {path} -> s3://{bucket}/{key} ({content_type})") - continue - with path.open("rb") as fh: - s3.put_object( - Bucket=bucket, - Key=key, - Body=fh, - ContentType=content_type or "application/octet-stream", - ) - print(f" uploaded: {key}") - uploaded += 1 - return uploaded - - -def prefix_db_rows(app) -> int: - """Prepend ``books/`` to photo_filename rows that don't already carry it.""" - with app.app_context(): - rows = db.session.scalars(db.select(Book).where(Book.photo_filename.isnot(None))).all() - to_update = [b for b in rows if not b.photo_filename.startswith(UPLOAD_PREFIX)] - - if DRY_RUN: - for b in to_update: - print(f" would update Book {b.id}: {b.photo_filename!r} -> {UPLOAD_PREFIX + b.photo_filename!r}") - return len(to_update) - - if not to_update: - return 0 - - for b in to_update: - b.photo_filename = UPLOAD_PREFIX + b.photo_filename - db.session.commit() - return len(to_update) - - -def main() -> int: - app = create_app() - if not app.config.get("S3_BUCKET"): - print("S3_BUCKET is not set; aborting.", file=sys.stderr) - return 1 - - mode = "DRY RUN" if DRY_RUN else "LIVE" - print(f"== Migrating uploads to S3 [{mode}] ==") - print(f" bucket: {app.config['S3_BUCKET']}") - print(f" region: {app.config['S3_REGION']}") - if app.config.get("S3_ENDPOINT_URL"): - print(f" endpoint: {app.config['S3_ENDPOINT_URL']}") - - n_uploaded = upload_files(app) - n_updated = prefix_db_rows(app) - - print(f"\nDone. Uploaded {n_uploaded} object(s); updated {n_updated} DB row(s).") - return 0 - - -if __name__ == "__main__": - sys.exit(main())