205 lines
9.6 KiB
PHP
205 lines
9.6 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="fa" dir="rtl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>آپلودر امن کلاینتساید (Spot Player)</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>body { font-family: Tahoma, system-ui, sans-serif; }</style>
|
|
</head>
|
|
<body class="bg-gray-900 text-white min-h-screen flex items-center justify-center p-4">
|
|
|
|
<div class="bg-gray-800 rounded-xl shadow-2xl p-8 w-full max-w-lg border border-gray-700">
|
|
<h2 class="text-2xl font-bold mb-6 text-center text-blue-400">آپلودر امن Spot Player</h2>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm text-gray-400 mb-1">شناسه دوره (Course ID)</label>
|
|
<input type="number" id="courseId" value="1" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-blue-500">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm text-gray-400 mb-1">عنوان ویدیو</label>
|
|
<input type="text" id="videoTitle" placeholder="مثلاً: جلسه اول" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-blue-500">
|
|
</div>
|
|
|
|
<div class="border-2 border-dashed border-gray-600 rounded-lg p-6 text-center hover:border-blue-500 transition cursor-pointer relative">
|
|
<input type="file" id="videoFile" accept="video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
|
|
<div id="fileLabel" class="text-gray-400">فایل ویدیو را اینجا رها کنید یا کلیک کنید</div>
|
|
</div>
|
|
|
|
<!-- لاگ عملیات -->
|
|
<div class="bg-black rounded p-3 h-40 overflow-y-auto text-xs font-mono text-green-400" id="logs">
|
|
> آماده برای پردازش...
|
|
</div>
|
|
|
|
<button id="btnStart" onclick="processAndUpload()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded disabled:opacity-50 disabled:cursor-not-allowed transition">
|
|
شروع رمزنگاری و آپلود
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE_URL = '/api/web'; // آدرس API لاراول
|
|
|
|
function log(msg) {
|
|
const el = document.getElementById('logs');
|
|
el.innerHTML += `<div>> ${msg}</div>`;
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
document.getElementById('videoFile').addEventListener('change', function(e) {
|
|
if(e.target.files[0]) {
|
|
document.getElementById('fileLabel').innerText = e.target.files[0].name;
|
|
log(`فایل انتخاب شد: ${e.target.files[0].name} (${(e.target.files[0].size / 1024 / 1024).toFixed(2)} MB)`);
|
|
}
|
|
});
|
|
|
|
// تبدیل بافر به رشته هگز
|
|
function buf2hex(buffer) {
|
|
return [...new Uint8Array(buffer)]
|
|
.map(x => x.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
// تبدیل UUID رشتهای به آرایه بایت (برای هدر فایل)
|
|
function uuidToBytes(uuid) {
|
|
const hex = uuid.replace(/-/g, '');
|
|
const bytes = new Uint8Array(16);
|
|
for (let i = 0; i < 16; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
return bytes;
|
|
}
|
|
|
|
async function processAndUpload() {
|
|
const fileInput = document.getElementById('videoFile');
|
|
const courseId = document.getElementById('courseId').value;
|
|
const title = document.getElementById('videoTitle').value;
|
|
|
|
if (!fileInput.files.length) return alert("لطفاً یک فایل انتخاب کنید");
|
|
if (!title) return alert("عنوان ویدیو الزامی است");
|
|
|
|
const file = fileInput.files[0];
|
|
const btn = document.getElementById('btnStart');
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
log("--- شروع عملیات ---");
|
|
|
|
// 1. تولید کلید (16 بایت) و نانس (8 بایت) و شناسه محتوا
|
|
log("تولید کلیدهای امنیتی...");
|
|
const keyBytes = window.crypto.getRandomValues(new Uint8Array(16));
|
|
const nonceBytes = window.crypto.getRandomValues(new Uint8Array(8));
|
|
const contentId = crypto.randomUUID();
|
|
|
|
// تبدیل کلید به فرمت هگز برای ارسال به دیتابیس
|
|
const contentKeyHex = buf2hex(keyBytes);
|
|
log(`شناسه محتوا: ${contentId}`);
|
|
|
|
// 2. خواندن فایل
|
|
log("در حال خواندن فایل در حافظه...");
|
|
const fileBuffer = await file.arrayBuffer();
|
|
|
|
// 3. رمزنگاری (AES-128-CTR)
|
|
log("در حال رمزنگاری (AES-128-CTR)...");
|
|
|
|
// ایمپورت کلید خام برای استفاده در Web Crypto API
|
|
const key = await window.crypto.subtle.importKey(
|
|
"raw", keyBytes, { name: "AES-CTR" }, false, ["encrypt"]
|
|
);
|
|
|
|
// ساخت کانتر بلاک (16 بایت): 8 بایت نانس + 8 بایت صفر
|
|
const counterBlock = new Uint8Array(16);
|
|
counterBlock.set(nonceBytes, 0);
|
|
// 8 بایت آخر صفر میماند (Big Endian Counter از صفر شروع میشود)
|
|
|
|
const encryptedData = await window.crypto.subtle.encrypt(
|
|
{
|
|
name: "AES-CTR",
|
|
counter: counterBlock,
|
|
length: 64 // تعداد بیتهای کانتر (8 بایت = 64 بیت)
|
|
},
|
|
key,
|
|
fileBuffer
|
|
);
|
|
|
|
log("رمزنگاری تکمیل شد.");
|
|
|
|
// 4. ساخت هدر فایل (Custom Header)
|
|
// فرمت: MYPLR1 (6 bytes) + Version (1 byte) + ContentID (16 bytes) + Nonce (8 bytes)
|
|
log("در حال بستهبندی فایل...");
|
|
|
|
const magic = new TextEncoder().encode("MYPLR1");
|
|
const version = new Uint8Array([1]);
|
|
const contentIdBytes = uuidToBytes(contentId);
|
|
|
|
// محاسبه سایز کل
|
|
const headerSize = magic.length + version.length + contentIdBytes.length + nonceBytes.length;
|
|
const totalSize = headerSize + encryptedData.byteLength;
|
|
|
|
const finalBuffer = new Uint8Array(totalSize);
|
|
|
|
let offset = 0;
|
|
finalBuffer.set(magic, offset); offset += magic.length;
|
|
finalBuffer.set(version, offset); offset += version.length;
|
|
finalBuffer.set(contentIdBytes, offset); offset += contentIdBytes.length;
|
|
finalBuffer.set(nonceBytes, offset); offset += nonceBytes.length;
|
|
finalBuffer.set(new Uint8Array(encryptedData), offset);
|
|
|
|
const finalBlob = new Blob([finalBuffer], { type: "application/octet-stream" });
|
|
|
|
// 5. آپلود به سرور
|
|
log("در حال آپلود به سرور...");
|
|
|
|
const formData = new FormData();
|
|
// نام فیلد باید با کنترلر لاراول یکی باشد (video_file)
|
|
formData.append('video_file', finalBlob, `${contentId}.spot`);
|
|
formData.append('title', title);
|
|
formData.append('content_id', contentId);
|
|
formData.append('content_key', contentKeyHex); // ارسال کلید به صورت هگز
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', `${API_BASE_URL}/courses/${courseId}/videos`, true);
|
|
|
|
// هدر برای احراز هویت (در صورت نیاز)
|
|
// xhr.setRequestHeader('Authorization', 'Bearer ...');
|
|
|
|
xhr.upload.onprogress = function(e) {
|
|
if (e.lengthComputable) {
|
|
const percent = Math.round((e.loaded / e.total) * 100);
|
|
log(`پیشرفت آپلود: ${percent}%`);
|
|
btn.innerText = `در حال آپلود... ${percent}%`;
|
|
}
|
|
};
|
|
|
|
xhr.onload = function() {
|
|
if (xhr.status === 200 || xhr.status === 201) {
|
|
log("✅ موفقیت! ویدیو با موفقیت ذخیره شد.");
|
|
btn.innerText = "آپلود موفقیتآمیز بود";
|
|
btn.classList.remove('bg-blue-600');
|
|
btn.classList.add('bg-green-600');
|
|
console.log(JSON.parse(xhr.responseText));
|
|
} else {
|
|
log("❌ خطا در سمت سرور: " + xhr.responseText);
|
|
btn.disabled = false;
|
|
btn.innerText = "تلاش مجدد";
|
|
}
|
|
};
|
|
|
|
xhr.onerror = function() {
|
|
log("❌ خطای شبکه");
|
|
btn.disabled = false;
|
|
btn.innerText = "تلاش مجدد";
|
|
};
|
|
|
|
xhr.send(formData);
|
|
|
|
} catch (e) {
|
|
log("ERROR: " + e.message);
|
|
console.error(e);
|
|
btn.disabled = false;
|
|
btn.innerText = "شروع مجدد";
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |