Files
drhider/site/templates/index.html
T

285 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DrHider — обфускация документов</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
:root {
--bg: #f5f5f5;
--card: #ffffff;
--border: #e0e0e0;
--text: #1a1a1a;
--muted: #888;
--brand: #2563eb;
--red: #ef4444;
--green: #22c55e;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font: 14px system-ui, sans-serif; color: var(--text); background: var(--bg); min-height: 100vh; }
.topbar {
background: var(--card); border-bottom: 1px solid var(--border);
padding: 0 24px; height: 52px; display: flex; align-items: center; gap: 12px;
font-weight: 600; font-size: 15px;
}
.content { max-width: 780px; margin: 28px auto; padding: 0 16px; }
.card {
background: var(--card); border-radius: 10px;
border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,.05);
overflow: hidden;
}
.card-header {
background: #fafafa; padding: 10px 16px;
font-weight: 600; font-size: 14px;
border-bottom: 1px solid var(--border);
}
.card-body { padding: 16px; }
.file-input-wrap { margin-bottom: 12px; }
.file-input-wrap input[type="file"] { width: 100%; font-size: 14px; }
.table-wrap {
border: 1px solid var(--border); border-radius: 6px;
overflow-y: auto; max-height: 60vh;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
background: #fafafa; text-transform: uppercase; padding: 6px 10px;
border-right: 1px solid var(--border); text-align: left;
font-weight: 600; font-size: 11px; color: var(--muted);
}
td {
padding: 6px 10px; border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
tr:hover td { background: rgba(37,99,235,.03); }
.name-cell { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num-cell { text-align: right; white-space: nowrap; }
.empty-row td { color: var(--muted); text-align: center; padding: 24px; }
.remove-btn {
cursor: pointer; color: #f87171; background: none; border: none;
padding: 2px 6px; font-size: 15px; line-height: 1;
}
.remove-btn:hover { color: var(--red); }
.footer-bar {
margin-top: 12px; display: flex; justify-content: space-between; align-items: center;
font-size: 13px; color: var(--muted);
}
.btn {
height: 34px; border-radius: 6px; padding: 0 16px; font-size: 13px;
font-family: inherit; cursor: pointer; border: 1px solid var(--border);
background: var(--card); color: var(--text);
display: inline-flex; align-items: center; gap: 6px;
}
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn-primary:hover { opacity: .9; }
.btn:disabled { opacity: .4; cursor: not-allowed; }
.status { margin-top: 12px; padding: 8px 12px; border-radius: 6px; font-size: 13px; display: none; }
.status.success { background: #dcfce7; color: #166534; display: block; }
.status.error { background: #fef2f2; color: #991b1b; display: block; }
.status.info { background: #eff6ff; color: #1e40af; display: block; }
</style>
</head>
<body>
<div class="topbar">
🛡️ DrHider — обфускация документов
<span style="font-weight:400;color:var(--muted);font-size:12px;margin-left:auto;">v{{ version }}</span>
</div>
<div class="content">
<div class="card">
<div class="card-header">Выбор файлов</div>
<div class="card-body">
<p style="color:var(--muted);margin-bottom:12px;font-size:13px;">
Загрузите документы (.docx, .xlsx, .pptx, .pdf, .txt).<br>
DrHider заменит все персональные данные, реквизиты компаний, телефоны, email на фиктивные значения.
</p>
<div class="file-input-wrap">
<input type="file" id="fileInput" multiple accept=".docx,.xlsx,.pptx,.pdf,.txt,.doc,.xls,.ppt">
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Имя</th>
<th style="width:90px;">Размер</th>
<th style="width:90px;">Статус</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="fileList">
<tr class="empty-row"><td colspan="4">Нет выбранных файлов</td></tr>
</tbody>
</table>
</div>
<div class="footer-bar">
<span id="fileCount">0 файлов</span>
<button class="btn btn-primary" id="uploadBtn" disabled onclick="uploadFiles()">
🛡️ Обфусцировать
</button>
</div>
<div class="status" id="status"></div>
</div>
</div>
</div>
<script>
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const fileCount = document.getElementById('fileCount');
const uploadBtn = document.getElementById('uploadBtn');
const status = document.getElementById('status');
let selectedFiles = [];
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function renderFiles() {
if (selectedFiles.length === 0) {
fileList.innerHTML = '<tr class="empty-row"><td colspan="3">Нет выбранных файлов</td></tr>';
} else {
fileList.innerHTML = selectedFiles.map((f, i) =>
'<tr id="row-' + i + '">' +
'<td class="name-cell">' + f.name + '</td>' +
'<td class="num-cell">' + formatSize(f.size) + '</td>' +
'<td class="num-cell" id="status-' + i + '" style="font-size:12px;">—</td>' +
'<td><button class="remove-btn" onclick="removeFile(' + i + ')">✕</button></td>' +
'</tr>'
).join('');
}
fileCount.textContent = selectedFiles.length + ' файлов';
uploadBtn.disabled = selectedFiles.length === 0;
}
function removeFile(i) {
selectedFiles.splice(i, 1);
// Пересоздаём FileList через DataTransfer
const dt = new DataTransfer();
selectedFiles.forEach(f => dt.items.add(f));
fileInput.files = dt.files;
renderFiles();
}
fileInput.addEventListener('change', () => {
selectedFiles = Array.from(fileInput.files);
renderFiles();
});
function setStatus(idx, html) {
const el = document.getElementById('status-' + idx);
if (el) el.innerHTML = html;
}
async function uploadFiles() {
if (selectedFiles.length === 0) return;
uploadBtn.disabled = true;
const total = selectedFiles.length;
const results = []; // { name, blob } для JSZip
let ok = 0, fail = 0;
for (let i = 0; i < total; i++) {
const f = selectedFiles[i];
const n = i + 1;
setStatus(i, '⏳');
status.className = 'status info';
status.textContent = 'Файл ' + n + '/' + total + ': ' + f.name;
const t0 = performance.now();
const fd = new FormData();
fd.append('files', f, f.name);
try {
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/drhider');
xhr.responseType = 'blob';
xhr.timeout = 600000;
xhr.onload = () => {
if (xhr.status === 200) resolve(xhr.response);
else reject(new Error('Сервер ' + xhr.status));
};
xhr.onerror = () => reject(new Error('Сеть'));
xhr.ontimeout = () => reject(new Error('Таймаут'));
xhr.send(fd);
});
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
setStatus(i, '<span style="color:#22c55e">✓ ' + elapsed + 'с</span>');
results.push({ name: f.name, blob: blob });
ok++;
} catch (err) {
setStatus(i, '<span style="color:#ef4444">✗</span>');
fail++;
}
// Пауза между файлами
if (i < total - 1) await new Promise(r => setTimeout(r, 300));
}
// ── Упаковка в один ZIP ──
if (results.length > 0) {
status.className = 'status info';
status.textContent = 'Упаковка в ZIP...';
const zip = new JSZip();
for (const r of results) {
// Извлекаем обфусцированный файл из ZIP-ответа бэкенда
const innerZip = await JSZip.loadAsync(r.blob);
const files = Object.keys(innerZip.files).filter(k => !k.startsWith('mapping') && !innerZip.files[k].dir);
for (const name of files) {
const data = await innerZip.files[name].async('blob');
// Имя: оригинальное_имя_obfuscated.расширение
const ext = name.split('.').pop();
const base = r.name.replace(/\.[^.]+$/, '');
zip.file(base + '_obfuscated.' + ext, data);
}
// mapping.csv — только один (из первого файла или объединить)
if (innerZip.files['mapping.csv']) {
const csv = await innerZip.files['mapping.csv'].async('string');
const existing = zip.files['mapping.csv'];
if (existing) {
// Дописываем к существующему (кроме заголовка)
const lines = csv.split('\n').slice(1).join('\n');
zip.file('mapping.csv', (await existing.async('string')) + '\n' + lines);
} else {
zip.file('mapping.csv', csv);
}
}
}
const finalZip = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(finalZip);
const a = document.createElement('a');
a.href = url;
a.download = 'drhider_output.zip';
a.click();
URL.revokeObjectURL(url);
}
// ── Итог ──
if (fail === 0) {
status.className = 'status success';
status.textContent = '✅ Готово! ' + ok + '/' + total;
} else {
status.className = 'status error';
status.textContent = 'Готово: ' + ok + ' OK, ' + fail + ' ошибок из ' + total;
}
uploadBtn.disabled = false;
}
</script>
</body>
</html>