iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0

前言

前一回探討了限制的方式,也驗證了可行性。 這邊接著修改原始碼。

原始碼

  • 原本在 list-videos 的 API 中,其實已經有爬這個使用者的所有影片,也有個別計算容量。 所以應該在使用者針對檔案進行異動時,比方說 convert 或是 upload。
  • 然而先前我們探討過 Lambda 本身允許上傳進來的資料只能 60MB 所以才會讓使用者拿回 presigned URL 後直接從瀏覽器扔進 S3。 就別再繞進 Lambda Function 了。
  • 我們先行修改 list-videos 這個 Function。
import os, json, boto3, jwt, urllib.parse, time
from decimal import Decimal  # ✅ 新增這行

s3 = boto3.client("s3")
dynamodb = boto3.resource("dynamodb")

BUCKET_NAME = os.environ.get("BUCKET_NAME", "exsky-backup-media")
SECRET = os.environ.get("JWT_SECRET", "mysecret")
USAGE_TABLE = os.environ.get("USAGE_TABLE", "vlog-usage")

usage_table = dynamodb.Table(USAGE_TABLE)

def lambda_handler(event, context):
    method = event.get("requestContext", {}).get("http", {}).get("method")

    # --- CORS ---
    if method == "OPTIONS":
        return {
            "statusCode": 200,
            "headers": {
                "Access-Control-Allow-Origin": "https://vlog.nipapa.tw",
                "Access-Control-Allow-Methods": "GET,OPTIONS",
                "Access-Control-Allow-Headers": "Authorization,Content-Type",
            },
            "body": ""
        }

    # --- JWT 驗證 ---
    headers = event.get("headers", {}) or {}
    auth = headers.get("authorization") or headers.get("Authorization") or ""
    if not auth.startswith("Bearer "):
        return _unauthorized("Missing token")
    token = auth.split(" ")[1]

    try:
        decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
        username = decoded.get("username", "unknown")
    except Exception:
        return _unauthorized("Invalid token")

    try:
        # --- 列出影片 ---
        resp = s3.list_objects_v2(Bucket=BUCKET_NAME, Prefix=f"{username}/videos/")
        resp_converted = s3.list_objects_v2(Bucket=BUCKET_NAME, Prefix=f"{username}/converted/")

        items = []
        total_size_bytes = 0

        for obj in resp.get("Contents", []):
            key = obj["Key"]
            if key.endswith("/") or key.startswith(f"{username}/covers/"):
                continue
            total_size_bytes += obj["Size"]

            converted_key = f"{username}/converted/{os.path.splitext(os.path.basename(key))[0]}.mp4"
            if any([x["Key"].endswith(os.path.splitext(os.path.basename(key))[0]+".mp4") for x in resp_converted.get("Contents", [])]):
                key = converted_key
                video_url = s3.generate_presigned_url(
                    "get_object",
                    Params={"Bucket": BUCKET_NAME, "Key": converted_key},
                    ExpiresIn=3600
                )
            else:
                video_url = s3.generate_presigned_url(
                    "get_object",
                    Params={"Bucket": BUCKET_NAME, "Key": key},
                    ExpiresIn=3600
                )

            basename = os.path.basename(key)
            cover_key = f"{username}/covers/{os.path.splitext(basename)[0]}.jpg"
            subtitle_key = f"{username}/subtitles/{os.path.splitext(basename)[0]}.vtt.vtt"

            cover_url = _try_presigned(cover_key)
            subtitle_url = _try_presigned(subtitle_key, rewrite=True)
            decoded_name = urllib.parse.unquote(basename)

            items.append({
                "key": key,
                "video_url": video_url,
                "cover_url": cover_url,
                "subtitle_url": subtitle_url,
                "size": obj["Size"],
                "decodedName": decoded_name
            })

        # ✅ 加上 converted 檔案的容量
        for obj in resp_converted.get("Contents", []):
            if not obj["Key"].endswith("/"):
                total_size_bytes += obj["Size"]

        # ✅ 用 Decimal 表示
        total_size_mb = Decimal(str(round(total_size_bytes / (1024 * 1024), 2)))

        # ✅ 更新 DynamoDB 使用量紀錄(全部 Decimal)
        usage_table.update_item(
            Key={"username": username},
            UpdateExpression="SET total_storage_mb = :s, item_count = :c, last_update = :t",
            ExpressionAttributeValues={
                ":s": total_size_mb,
                ":c": Decimal(len(items)),
                ":t": Decimal(int(time.time()))
            }
        )

    except Exception as e:
        return _server_error(str(e))

    return {
        "statusCode": 200,
        "headers": {"Access-Control-Allow-Origin": "https://vlog.nipapa.tw"},
        "body": json.dumps({
            "videos": items,
            "total_storage_mb": float(total_size_mb),
            "item_count": len(items)
        }, ensure_ascii=False)
    }


# ---- helpers ----
def _try_presigned(key, rewrite=False):
    try:
        s3.head_object(Bucket=BUCKET_NAME, Key=key)
        url = s3.generate_presigned_url(
            "get_object",
            Params={"Bucket": BUCKET_NAME, "Key": key},
            ExpiresIn=3600
        )
        if rewrite:
            url = url.replace("exsky-backup-media.s3.amazonaws.com", "vlog.nipapa.tw")
        return url
    except Exception:
        return None


def _unauthorized(msg):
    return {
        "statusCode": 401,
        "headers": {"Access-Control-Allow-Origin": "https://vlog.nipapa.tw"},
        "body": json.dumps({"error": msg})
    }

def _server_error(msg):
    return {
        "statusCode": 500,
        "headers": {"Access-Control-Allow-Origin": "https://vlog.nipapa.tw"},
        "body": json.dumps({"error": msg})
    }

結論

  • 因為修改了 list-videos 的回傳格式,所以間接影響到 script.js 讀取與呈現。 晚點再來更新

上一篇
【Day 27】 限制會員存放容量上限 (上)
系列文
無法成為片師也想拍 Vlog?!個人影音小工具的誕生!28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言