feat: DrHider MVP — two-pass document obfuscation service
Deploy contracts-flask / validate (push) Successful in 0s
Deploy contracts-flask / validate (push) Successful in 0s
This commit is contained in:
+49
@@ -103,6 +103,55 @@ def ci_cd():
|
||||
return render_template("ci-cd.html")
|
||||
|
||||
|
||||
@app.route("/DrHider")
|
||||
def drhider():
|
||||
return render_template("drhider.html")
|
||||
|
||||
|
||||
@app.route("/api/drhider", methods=["POST"])
|
||||
def api_drhider():
|
||||
"""Обфускация: файлы → ZIP с обезличенными копиями + mapping.csv."""
|
||||
import io
|
||||
import zipfile
|
||||
from flask import send_file
|
||||
|
||||
uploaded = request.files.getlist("files")
|
||||
if not uploaded:
|
||||
return jsonify({"ok": False, "error": "Нет файлов"}), 400
|
||||
|
||||
files = []
|
||||
for f in uploaded:
|
||||
if not f.filename:
|
||||
continue
|
||||
content = f.read()
|
||||
files.append((f.filename, content, f.content_type or ""))
|
||||
|
||||
if not files:
|
||||
return jsonify({"ok": False, "error": "Нет файлов"}), 400
|
||||
|
||||
try:
|
||||
from services.drhider import obfuscate_files
|
||||
from services.llm_client import HttpxLLMClient
|
||||
|
||||
# LLM-клиент для NER (если настроен)
|
||||
llm = HttpxLLMClient(LLM_URL, LLM_KEY, LLM_MODEL) if LLM_KEY else None
|
||||
|
||||
zip_data, csv_data = obfuscate_files(files, llm_client=llm)
|
||||
|
||||
buf = io.BytesIO()
|
||||
buf.write(zip_data)
|
||||
buf.seek(0)
|
||||
|
||||
return send_file(
|
||||
buf,
|
||||
mimetype="application/zip",
|
||||
as_attachment=True,
|
||||
download_name="drhider_output.zip"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return {"ok": True, "service": "contracts-flask"}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DrHider — обфускация документов</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117; --fg: #c9d1d9; --muted: #8b949e;
|
||||
--accent: #58a6ff; --green: #3fb950; --red: #f85149; --border: #30363d;
|
||||
--card-bg: #161b22; --orange: #d2991d;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font: 15px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--fg); max-width: 700px; margin: 0 auto; padding: 32px 20px 60px; }
|
||||
h1 { font-size: 22px; margin-bottom: 4px; }
|
||||
.sub { color: var(--muted); font-size: 14px; margin-bottom: 24px; }
|
||||
.nav { margin-bottom: 20px; }
|
||||
.nav a { color: var(--accent); text-decoration: none; font-size: 13px; }
|
||||
.nav a:hover { text-decoration: underline; }
|
||||
|
||||
#dropZone {
|
||||
border: 2px dashed var(--border); border-radius: 12px; padding: 40px 20px;
|
||||
text-align: center; cursor: pointer; transition: all .2s;
|
||||
background: var(--card-bg); margin-bottom: 16px;
|
||||
}
|
||||
#dropZone:hover, #dropZone.active { border-color: var(--accent); background: #0d1a28; }
|
||||
#dropZone p { color: var(--muted); margin: 4px 0; font-size: 14px; }
|
||||
#dropZone .big { font-size: 36px; }
|
||||
|
||||
#fileList { display: none; margin: 16px 0; }
|
||||
#fileList.show { display: block; }
|
||||
.file-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 6px; font-size: 13px; }
|
||||
.file-row .name { flex: 1; }
|
||||
.file-row .size { color: var(--muted); }
|
||||
.file-row .remove { cursor: pointer; color: var(--red); font-weight: bold; }
|
||||
|
||||
#status { display: none; margin: 16px 0; padding: 12px; border-radius: 8px; font-size: 13px; }
|
||||
#status.show { display: block; }
|
||||
#status.progress { background: #1a2a3a; border: 1px solid var(--accent); color: var(--accent); }
|
||||
#status.done { background: #1a2a1a; border: 1px solid var(--green); color: var(--green); }
|
||||
#status.error { background: #2a1a1a; border: 1px solid var(--red); color: var(--red); }
|
||||
|
||||
button {
|
||||
padding: 10px 24px; border: none; border-radius: 8px; cursor: pointer;
|
||||
font-size: 14px; font-weight: 600; transition: all .2s;
|
||||
}
|
||||
.btn-go { background: var(--green); color: #000; width: 100%; margin-top: 12px; }
|
||||
.btn-go:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-go:hover:not(:disabled) { background: #4cd964; }
|
||||
|
||||
.note { margin-top: 24px; padding: 12px 16px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; font-size: 12px; color: var(--muted); }
|
||||
.note strong { color: var(--orange); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="nav"><a href="/">← Основной сервис</a></div>
|
||||
|
||||
<h1>🛡️ DrHider — обфускация документов</h1>
|
||||
<p class="sub">Загрузите документы — получите ZIP с обезличенными копиями и таблицей соответствия. Без сохранения на сервере.</p>
|
||||
|
||||
<div id="dropZone">
|
||||
<div class="big">📁</div>
|
||||
<p>Выберите файлы или перетащите сюда</p>
|
||||
<p style="font-size:12px;">.docx .pdf .doc .zip</p>
|
||||
<input type="file" id="fileInput" multiple accept=".docx,.pdf,.doc,.zip" style="display:none">
|
||||
</div>
|
||||
|
||||
<div id="fileList"></div>
|
||||
|
||||
<button class="btn-go" id="btnGo" disabled>Обфусцировать</button>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<div class="note">
|
||||
<strong>⚡ Всё в памяти.</strong> Файлы не сохраняются на сервер. После обработки данные удаляются.<br>
|
||||
<strong>📋 mapping.csv</strong> — таблица «оригинал → замена» внутри ZIP.<br>
|
||||
<strong>🔒 Результат</strong> — обезличенные файлы можно загружать в основной сервис сверки.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const DZ = document.getElementById('dropZone');
|
||||
const INPUT = document.getElementById('fileInput');
|
||||
const LIST = document.getElementById('fileList');
|
||||
const BTN = document.getElementById('btnGo');
|
||||
const STATUS = document.getElementById('status');
|
||||
let files = [];
|
||||
|
||||
DZ.onclick = () => INPUT.click();
|
||||
INPUT.onchange = () => { addFiles(INPUT.files); INPUT.value = ''; };
|
||||
|
||||
DZ.ondragover = e => { e.preventDefault(); DZ.classList.add('active'); };
|
||||
DZ.ondragleave = () => DZ.classList.remove('active');
|
||||
DZ.ondrop = e => { e.preventDefault(); DZ.classList.remove('active'); addFiles(e.dataTransfer.files); };
|
||||
|
||||
function addFiles(fl) {
|
||||
for (let f of fl) {
|
||||
if (files.find(x => x.name === f.name && x.size === f.size)) continue;
|
||||
files.push(f);
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function remove(i) { files.splice(i, 1); render(); }
|
||||
|
||||
function render() {
|
||||
LIST.innerHTML = files.map((f, i) =>
|
||||
`<div class="file-row"><span class="name">${esc(f.name)}</span><span class="size">${fmt(f.size)}</span><span class="remove" onclick="remove(${i})">✕</span></div>`
|
||||
).join('');
|
||||
LIST.className = files.length ? 'show' : '';
|
||||
BTN.disabled = files.length === 0;
|
||||
}
|
||||
|
||||
BTN.onclick = async () => {
|
||||
if (!files.length) return;
|
||||
BTN.disabled = true;
|
||||
setStatus('progress', '⏳ Обработка...');
|
||||
|
||||
const fd = new FormData();
|
||||
files.forEach(f => fd.append('files', f));
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/drhider', { method: 'POST', body: fd });
|
||||
if (!resp.ok) throw new Error(await resp.text());
|
||||
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'drhider_output.zip';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
setStatus('done', '✅ Готово! ZIP скачан. Файлы обезличены, mapping.csv внутри.');
|
||||
} catch (e) {
|
||||
setStatus('error', '❌ Ошибка: ' + e.message);
|
||||
}
|
||||
BTN.disabled = false;
|
||||
};
|
||||
|
||||
function setStatus(cls, msg) { STATUS.className = 'show ' + cls; STATUS.textContent = msg; }
|
||||
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function fmt(n) { return n > 1e6 ? (n/1e6).toFixed(1)+' MB' : n > 1e3 ? (n/1e3).toFixed(0)+' KB' : n+' B'; }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user