終於放假可以喘口氣了,持續挖坑與填坑!
import os, json, boto3, jwt
s3 = boto3.client("s3")
BUCKET_NAME = os.environ.get("BUCKET_NAME", "exsky-backup-media")
SECRET = os.environ.get("JWT_SECRET", "mysecret")
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": "DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Authorization,Content-Type",
},
"body": ""
}
if method != "DELETE":
return {"statusCode": 405, "body": "Method not allowed"}
# --- 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")
# --- 解析 body 取得要刪的檔案 key ---
try:
body = json.loads(event.get("body", "{}"))
key = body.get("key")
if not key or not key.startswith(f"{username}/"):
return _bad_request("Invalid key")
except Exception:
return _bad_request("Invalid body")
# --- 刪除影片 & cover ---
try:
# 刪影片
s3.delete_object(Bucket=BUCKET_NAME, Key=key)
# 推導封面名稱
base = os.path.basename(key)
cover_key = f"{username}/covers/{os.path.splitext(base)[0]}.jpg"
# 嘗試刪封面(若不存在會 404,不會影響主要刪除)
try:
s3.delete_object(Bucket=BUCKET_NAME, Key=cover_key)
except Exception:
pass
return {
"statusCode": 200,
"headers": {"Access-Control-Allow-Origin": "https://vlog.nipapa.tw"},
"body": json.dumps({
"message": f"Deleted {key} and cover {cover_key}"
}, ensure_ascii=False)
}
except Exception as e:
return _server_error(str(e))
# ---- helpers ----
def _unauthorized(msg):
return {
"statusCode": 401,
"headers": {"Access-Control-Allow-Origin": "https://vlog.nipapa.tw"},
"body": json.dumps({"error": msg})
}
def _bad_request(msg):
return {
"statusCode": 400,
"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})
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:<AWS_ID>:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:<AWS_ID>:log-group:/aws/lambda/delete-vlog-video:*"
]
},
{
"Effect": "Allow",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::exsky-backup-media/*"
}
]
}
const API_BASE = "https://iwlw3i3ys4.execute-api.ap-northeast-1.amazonaws.com/prod";
const form = document.getElementById("uploadForm");
const fileInput = document.getElementById("fileInput");
const statusDiv = document.getElementById("status");
const progressBar = document.getElementById("progressBar");
const logoutBtn = document.getElementById("logoutBtn");
const welcomeUser = document.getElementById("welcomeUser");
// 🚀 頁面載入時,先驗證 JWT
document.addEventListener("DOMContentLoaded", async () => {
const token = await validateToken();
if (!token) return;
console.log("✅ JWT 驗證成功,顯示上傳功能");
// 顯示 UI
document.getElementById("uploadSection").style.display = "block";
logoutBtn.style.display = "inline-block";
// 綁定登出事件
logoutBtn.addEventListener("click", () => {
localStorage.removeItem("jwt");
alert("已登出!");
window.location.href = "login.html";
});
// 取得使用者名稱並顯示
try {
const res = await fetch(`${API_BASE}/validate`, {
method: "GET",
headers: { "Authorization": "Bearer " + token }
});
const data = await res.json();
if (data.username) {
welcomeUser.textContent = `👋 Hi, ${data.username}`;
}
} catch (err) {
console.error("❌ 無法取得使用者名稱:", err);
}
});
// ✅ 驗證 JWT 是否有效
async function validateToken() {
const token = localStorage.getItem("jwt");
if (!token) {
window.location.href = "login.html";
return null;
}
try {
const res = await fetch(`${API_BASE}/validate`, {
method: "GET",
headers: { "Authorization": "Bearer " + token }
});
if (res.status === 401) {
localStorage.removeItem("jwt");
window.location.href = "login.html";
return null;
}
return token;
} catch (err) {
console.error("❌ 驗證 API 錯誤:", err);
return null;
}
}
// ✅ 上傳影片
form.addEventListener("submit", async (e) => {
e.preventDefault();
const file = fileInput.files[0];
if (!file) {
statusDiv.textContent = "⚠️ 請先選擇影片!";
return;
}
const token = await validateToken();
if (!token) return;
statusDiv.textContent = "正在請求上傳網址...";
progressBar.value = 0;
try {
const res = await fetch(`${API_BASE}/generate-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify({
filename: file.name,
contentType: file.type // ✅ 傳給 Lambda
})
});
const data = await res.json();
if (!res.ok || !data.url) {
statusDiv.textContent = "❌ 無法取得上傳 URL";
return;
}
const uploadUrl = data.url;
statusDiv.textContent = "正在上傳影片...";
const xhr = new XMLHttpRequest();
xhr.open("PUT", uploadUrl, true);
xhr.setRequestHeader("Content-Type", file.type);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressBar.value = percent;
}
};
xhr.onload = () => {
if (xhr.status === 200) {
statusDiv.textContent = "✅ 上傳完成!";
} else {
statusDiv.textContent = `❌ 上傳失敗:${xhr.statusText}`;
}
};
xhr.onerror = () => {
statusDiv.textContent = "❌ 上傳錯誤,請稍後再試";
};
xhr.send(file);
} catch (err) {
console.error("❌ 上傳流程錯誤:", err);
statusDiv.textContent = "❌ 上傳失敗";
}
});
// ✅ 載入並顯示影片清單
async function loadVideos() {
const token = localStorage.getItem("jwt");
if (!token) {
alert("請先登入!");
window.location.href = "login.html";
return;
}
try {
const res = await fetch(`${API_BASE}/list-videos`, {
method: "GET",
headers: { "Authorization": "Bearer " + token }
});
const data = await res.json();
if (!res.ok) {
console.error("❌ list-videos 錯誤:", data);
return;
}
const videoListDiv = document.getElementById("videoList");
videoListDiv.innerHTML = ""; // 清空舊內容
data.forEach(item => {
// 外層卡片
const card = document.createElement("div");
card.className = "video-card";
// 縮圖
if (item.cover_url) {
const img = document.createElement("img");
img.src = item.cover_url;
img.alt = item.decodedName;
card.appendChild(img);
} else {
const placeholder = document.createElement("div");
placeholder.className = "placeholder";
placeholder.textContent = "🎬 無縮圖";
card.appendChild(placeholder);
}
// 檔名連結
const link = document.createElement("a");
link.href = item.video_url;
link.target = "_blank";
link.textContent = item.decodedName;
card.appendChild(link);
// 檔案大小
const sizeInfo = document.createElement("div");
sizeInfo.className = "size";
sizeInfo.textContent = `${(item.size / 1024 / 1024).toFixed(2)} MB`;
card.appendChild(sizeInfo);
// ❌ 刪除按鈕
const delBtn = document.createElement("button");
delBtn.textContent = "刪除";
delBtn.style.marginTop = "8px";
delBtn.onclick = async () => {
if (!confirm(`確定刪除 ${item.decodedName}?`)) return;
const token = localStorage.getItem("jwt");
try {
const res = await fetch(`${API_BASE}/delete-video`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify({ key: item.key })
});
const result = await res.json();
if (res.ok) {
alert("✅ 已刪除");
card.remove(); // 直接移除卡片
} else {
alert("❌ 刪除失敗:" + (result.error || "未知錯誤"));
}
} catch (err) {
console.error("刪除失敗:", err);
}
};
card.appendChild(delBtn);
videoListDiv.appendChild(card);
});
} catch (err) {
console.error("❌ 載入影片失敗:", err);
}
}
/generate-url
一旦確立身份,用來產生上傳影片到 S3 的臨時網址
/login
使用者在登入頁面打完帳號密碼,透過此 API 送給後端程式進行 登入程序
/login
將請求轉導到 網站的
login.html
頁面
/register
使用者在登入頁面打完帳號密碼,透過此 API 送給後端程式進行 註冊程序
/validate
用來檢查使用者瀏覽器中的 jwt 是否有效,藉此 驗證登入狀態
/list-videos
用來檢視之前上傳過的影片、列出清單用來呈現在頁面上
/delete-video
刪除指定影片 / 也順邊刪除 cover
當檔案新增到 S3 Bucket 後自動觸發,用 ffmpeg 去產出截圖。
/generate-subtitle
/translate-url