前一回探討了限制的方式,也驗證了可行性。 這邊接著修改原始碼。
list-videos 的 API 中,其實已經有爬這個使用者的所有影片,也有個別計算容量。 所以應該在使用者針對檔案進行異動時,比方說 convert 或是 upload。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})
    }