Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import flask
|
| 2 |
from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
|
| 3 |
from flask_caching import Cache
|
|
@@ -6,7 +7,7 @@ import os
|
|
| 6 |
import logging
|
| 7 |
import threading
|
| 8 |
import time
|
| 9 |
-
from datetime import datetime
|
| 10 |
from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
|
| 11 |
from werkzeug.utils import secure_filename
|
| 12 |
import requests
|
|
@@ -41,9 +42,9 @@ BASE_STYLE = '''
|
|
| 41 |
--text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
| 42 |
--glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 43 |
--delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
|
| 44 |
-
--note-color: #6a5acd;
|
| 45 |
}
|
| 46 |
-
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(
|
| 47 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 48 |
html { scroll-behavior: smooth; }
|
| 49 |
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
|
|
@@ -53,7 +54,8 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robo
|
|
| 53 |
.view-toggle { display: flex; align-items: center; gap: 5px; }
|
| 54 |
.view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); }
|
| 55 |
.view-toggle button:hover, .view-toggle button.active { color: var(--primary); }
|
| 56 |
-
h2
|
|
|
|
| 57 |
.breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
| 58 |
.breadcrumbs a { color: var(--accent); text-decoration: none; }
|
| 59 |
.breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
|
|
@@ -64,6 +66,7 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 64 |
.download-btn { background: var(--secondary); }
|
| 65 |
.delete-btn { background: var(--delete-color); }
|
| 66 |
.folder-btn { background: var(--folder-color); }
|
|
|
|
| 67 |
.flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); }
|
| 68 |
.flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); }
|
| 69 |
.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; }
|
|
@@ -77,7 +80,6 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 77 |
.item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); }
|
| 78 |
.item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; }
|
| 79 |
.item-info { font-size: 0.75em; color: var(--text-muted); }
|
| 80 |
-
.item-actions { display: none; }
|
| 81 |
.file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
|
| 82 |
.file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
|
| 83 |
.file-grid.list-view .item:hover { transform: translateY(0); }
|
|
@@ -97,14 +99,81 @@ input, select, textarea { width: 100%; padding: 14px; margin: 8px 0; border: 1px
|
|
| 97 |
#selection-bar { position: fixed; bottom: -120px; left: 10px; right: 10px; background: var(--glass-bg); backdrop-filter: blur(10px); padding: 10px; border-radius: 15px; box-shadow: var(--shadow); z-index: 1000; display: flex; justify-content: space-around; align-items: center; transition: bottom 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
|
| 98 |
#selection-bar.visible { bottom: 10px; }
|
| 99 |
#selection-bar .btn { margin: 0 5px; padding: 10px 15px; font-size: 0.9em; flex-grow: 1; }
|
| 100 |
-
#move-modal .modal-content
|
| 101 |
.fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; }
|
| 102 |
.fab { width: 56px; height: 56px; background: var(--accent); border-radius: 50%; border: none; box-shadow: var(--shadow); color: white; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.3s; }
|
| 103 |
.fab:active { transform: scale(0.9); }
|
| 104 |
.loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: auto; }
|
| 105 |
-
.
|
| 106 |
-
.
|
| 107 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
'''
|
| 109 |
|
| 110 |
def find_node_by_id(filesystem, node_id):
|
|
@@ -154,7 +223,6 @@ def get_all_folders(filesystem, exclude_ids=None):
|
|
| 154 |
if exclude_ids is None:
|
| 155 |
exclude_ids = set()
|
| 156 |
folders = []
|
| 157 |
-
|
| 158 |
def traverse(node, path_prefix):
|
| 159 |
if node.get('type') == 'folder':
|
| 160 |
if node.get('id') not in exclude_ids:
|
|
@@ -163,7 +231,6 @@ def get_all_folders(filesystem, exclude_ids=None):
|
|
| 163 |
new_prefix = f"{path_prefix}{node.get('name', '')}/" if node.get('id') != 'root' else ""
|
| 164 |
for child in node.get('children', []):
|
| 165 |
traverse(child, new_prefix)
|
| 166 |
-
|
| 167 |
traverse(filesystem, "")
|
| 168 |
return sorted(folders, key=lambda x: x['name'].lower())
|
| 169 |
|
|
@@ -203,15 +270,16 @@ def load_data():
|
|
| 203 |
try:
|
| 204 |
download_db_from_hf()
|
| 205 |
with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
|
| 206 |
-
if not isinstance(data, dict): data = {'users': {}}
|
| 207 |
data.setdefault('users', {})
|
|
|
|
| 208 |
for tma_user_id_str, user_data_item in data['users'].items():
|
| 209 |
initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
|
| 210 |
user_data_item.setdefault('reminders', [])
|
| 211 |
return data
|
| 212 |
except Exception as e:
|
| 213 |
logging.error(f"Error loading data: {e}")
|
| 214 |
-
return {'users': {}}
|
| 215 |
|
| 216 |
def save_data(data):
|
| 217 |
try:
|
|
@@ -234,17 +302,17 @@ def upload_db_to_hf():
|
|
| 234 |
def download_db_from_hf():
|
| 235 |
if not HF_TOKEN_READ:
|
| 236 |
if not os.path.exists(DATA_FILE):
|
| 237 |
-
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
|
| 238 |
return
|
| 239 |
try:
|
| 240 |
hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
|
| 241 |
except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
|
| 242 |
if not os.path.exists(DATA_FILE):
|
| 243 |
-
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
|
| 244 |
except Exception as e:
|
| 245 |
logging.error(f"Error downloading database: {e}")
|
| 246 |
if not os.path.exists(DATA_FILE):
|
| 247 |
-
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
|
| 248 |
|
| 249 |
def periodic_backup():
|
| 250 |
while True:
|
|
@@ -460,7 +528,8 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 460 |
|
| 461 |
<div id="selection-bar">
|
| 462 |
<span id="selection-count"></span>
|
| 463 |
-
<button id="selection-
|
|
|
|
| 464 |
<button class="btn" style="background: var(--accent);" onclick="showMoveModal()"><i class="fa-solid fa-arrow-right-to-bracket"></i></button>
|
| 465 |
<button class="btn delete-btn" onclick="deleteSelected()"><i class="fa-solid fa-trash-can"></i></button>
|
| 466 |
<button class="btn" style="background: #555;" onclick="toggleSelectionMode(false)">Отмена</button>
|
|
@@ -476,20 +545,29 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 476 |
</div></div>
|
| 477 |
|
| 478 |
<div class="modal" id="fab-modal"><div class="modal-content">
|
| 479 |
-
<h4
|
| 480 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
<input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
|
| 482 |
<input type="file" name="files" id="file-input" multiple required onchange="document.getElementById('upload-btn-modal').click()">
|
| 483 |
-
<
|
| 484 |
-
<button type="submit" id="upload-btn-modal" style="display:none;"></button>
|
| 485 |
</form>
|
| 486 |
-
<
|
| 487 |
-
<form method="POST" action="{{ url_for('create_folder_tma') }}">
|
| 488 |
<input type="hidden" name="parent_folder_id" value="{{ current_folder_id }}">
|
| 489 |
<input type="text" name="folder_name" placeholder="Имя новой папки" required>
|
| 490 |
-
<button type="submit" class="btn folder-btn" style="width:100%"
|
| 491 |
</form>
|
| 492 |
-
<button class="btn" style="background: #555; width: 100%; margin-top: 10px;" onclick="closeFabModal()"
|
| 493 |
</div></div>
|
| 494 |
|
| 495 |
<div class="modal" id="note-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
|
@@ -515,6 +593,20 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 515 |
</div>
|
| 516 |
</div></div>
|
| 517 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
<div class="fab-container"><button id="fab" class="fab"><i class="fa-solid fa-plus"></i></button></div>
|
| 519 |
|
| 520 |
<script>
|
|
@@ -593,6 +685,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 593 |
const selectionBar = document.getElementById('selection-bar');
|
| 594 |
const selectionCount = document.getElementById('selection-count');
|
| 595 |
const selectionDownloadBtn = document.getElementById('selection-download-btn');
|
|
|
|
| 596 |
const allItems = document.querySelectorAll('.item');
|
| 597 |
function toggleSelectionMode(enable) {
|
| 598 |
selectionMode = enable;
|
|
@@ -604,9 +697,8 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 604 |
selectionCount.textContent = `Выбрано: ${selectedItems.size}`;
|
| 605 |
const firstSelectedId = selectedItems.values().next().value;
|
| 606 |
const itemElement = document.querySelector(`.item[data-id='${firstSelectedId}']`);
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
} else { selectionDownloadBtn.style.display = 'none'; }
|
| 610 |
}
|
| 611 |
allItems.forEach(item => {
|
| 612 |
item.addEventListener('pointerdown', e => {
|
|
@@ -755,6 +847,78 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 755 |
}
|
| 756 |
});
|
| 757 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
document.getElementById('reminders-btn').addEventListener('click', openRemindersModal);
|
| 759 |
const gridViewBtn = document.getElementById('grid-view-btn');
|
| 760 |
const listViewBtn = document.getElementById('list-view-btn');
|
|
@@ -771,8 +935,14 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 771 |
const fab = document.getElementById('fab');
|
| 772 |
const fabModal = document.getElementById('fab-modal');
|
| 773 |
fab.addEventListener('click', () => { haptic.impactOccurred('medium'); fabModal.style.display = 'flex'; });
|
| 774 |
-
function closeFabModal() {
|
|
|
|
|
|
|
|
|
|
| 775 |
fabModal.addEventListener('click', e => { if (e.target.id === 'fab-modal') closeFabModal(); });
|
|
|
|
|
|
|
|
|
|
| 776 |
document.addEventListener('DOMContentLoaded', () => {
|
| 777 |
setView(localStorage.getItem('viewMode') || 'grid');
|
| 778 |
const currentFolderId = '{{ current_folder_id }}';
|
|
@@ -967,11 +1137,8 @@ def batch_download_tma():
|
|
| 967 |
if 'telegram_user_id' not in session: return Response("Unauthorized", 401)
|
| 968 |
file_ids_str = request.args.get('file_ids')
|
| 969 |
if not file_ids_str: return Response("No file IDs provided", 400)
|
| 970 |
-
|
| 971 |
file_ids = file_ids_str.split(',')
|
| 972 |
-
|
| 973 |
temp_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
|
| 974 |
-
|
| 975 |
try:
|
| 976 |
with zipfile.ZipFile(temp_zip_file.name, 'w', zipfile.ZIP_DEFLATED) as zf:
|
| 977 |
for file_id in file_ids:
|
|
@@ -1206,6 +1373,148 @@ def tma_logout():
|
|
| 1206 |
flash('Вы вышли из сессии приложения.')
|
| 1207 |
return redirect(url_for('tma_entry_page'))
|
| 1208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
ADMIN_LOGIN_HTML = '''
|
| 1210 |
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
|
| 1211 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -1703,11 +2012,11 @@ if __name__ == '__main__':
|
|
| 1703 |
download_db_from_hf()
|
| 1704 |
else:
|
| 1705 |
if not os.path.exists(DATA_FILE):
|
| 1706 |
-
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
|
| 1707 |
|
| 1708 |
if HF_TOKEN_WRITE:
|
| 1709 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 1710 |
|
| 1711 |
threading.Thread(target=check_reminders, daemon=True).start()
|
| 1712 |
|
| 1713 |
-
app.run(debug=False, host='0.0.0.0', port=7860)
|
|
|
|
| 1 |
+
|
| 2 |
import flask
|
| 3 |
from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context
|
| 4 |
from flask_caching import Cache
|
|
|
|
| 7 |
import logging
|
| 8 |
import threading
|
| 9 |
import time
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils
|
| 12 |
from werkzeug.utils import secure_filename
|
| 13 |
import requests
|
|
|
|
| 42 |
--text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
| 43 |
--glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 44 |
--delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
|
| 45 |
+
--note-color: #6a5acd; --share-color: #4caf50;
|
| 46 |
}
|
| 47 |
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(3 ৩৬۰deg); } }
|
| 48 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 49 |
html { scroll-behavior: smooth; }
|
| 50 |
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
|
|
|
|
| 54 |
.view-toggle { display: flex; align-items: center; gap: 5px; }
|
| 55 |
.view-toggle button { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); }
|
| 56 |
.view-toggle button:hover, .view-toggle button.active { color: var(--primary); }
|
| 57 |
+
h2, h3, h4, h5 { color: var(--text-dark); }
|
| 58 |
+
h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; }
|
| 59 |
.breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
| 60 |
.breadcrumbs a { color: var(--accent); text-decoration: none; }
|
| 61 |
.breadcrumbs span { margin: 0 5px; color: var(--text-muted); }
|
|
|
|
| 66 |
.download-btn { background: var(--secondary); }
|
| 67 |
.delete-btn { background: var(--delete-color); }
|
| 68 |
.folder-btn { background: var(--folder-color); }
|
| 69 |
+
.share-btn { background: var(--share-color); }
|
| 70 |
.flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); }
|
| 71 |
.flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); }
|
| 72 |
.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; }
|
|
|
|
| 80 |
.item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); }
|
| 81 |
.item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; }
|
| 82 |
.item-info { font-size: 0.75em; color: var(--text-muted); }
|
|
|
|
| 83 |
.file-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
|
| 84 |
.file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; }
|
| 85 |
.file-grid.list-view .item:hover { transform: translateY(0); }
|
|
|
|
| 99 |
#selection-bar { position: fixed; bottom: -120px; left: 10px; right: 10px; background: var(--glass-bg); backdrop-filter: blur(10px); padding: 10px; border-radius: 15px; box-shadow: var(--shadow); z-index: 1000; display: flex; justify-content: space-around; align-items: center; transition: bottom 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
|
| 100 |
#selection-bar.visible { bottom: 10px; }
|
| 101 |
#selection-bar .btn { margin: 0 5px; padding: 10px 15px; font-size: 0.9em; flex-grow: 1; }
|
| 102 |
+
#move-modal .modal-content { padding: 20px; max-width: 400px; }
|
| 103 |
.fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; }
|
| 104 |
.fab { width: 56px; height: 56px; background: var(--accent); border-radius: 50%; border: none; box-shadow: var(--shadow); color: white; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.3s; }
|
| 105 |
.fab:active { transform: scale(0.9); }
|
| 106 |
.loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: auto; }
|
| 107 |
+
#fab-modal .modal-content { padding: 20px; max-width: 400px; background: var(--card-bg-dark); text-align: center; }
|
| 108 |
+
.fab-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px; }
|
| 109 |
+
.fab-option { display: flex; flex-direction: column; align-items: center; justify-content: center; background: #2a2a2a; border-radius: 12px; padding: 15px; cursor: pointer; transition: var(--transition); text-decoration:none; color: var(--text-dark); }
|
| 110 |
+
.fab-option:hover { background: #333; transform: translateY(-3px); }
|
| 111 |
+
.fab-option i { font-size: 2em; margin-bottom: 8px; }
|
| 112 |
+
#fab-option-upload i { color: var(--secondary); }
|
| 113 |
+
#fab-option-note i { color: var(--note-color); }
|
| 114 |
+
#fab-option-folder i { color: var(--folder-color); }
|
| 115 |
+
#create-folder-form { display: none; margin-top: 15px; }
|
| 116 |
+
.shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
|
| 117 |
+
.shared-link-item:last-child { border-bottom: none; }
|
| 118 |
+
.shared-link-info { text-align: left; }
|
| 119 |
+
.shared-link-info strong { word-break: break-all; }
|
| 120 |
+
.shared-link-info small { color: var(--text-muted); display: block; }
|
| 121 |
+
.shared-link-actions button { background: none; border: none; color: var(--text-muted); font-size: 1.1em; cursor: pointer; padding: 5px; }
|
| 122 |
+
'''
|
| 123 |
+
|
| 124 |
+
PUBLIC_SHARE_PAGE_HTML = '''
|
| 125 |
+
<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 126 |
+
<title>Общая папка: {{ folder.name }}</title>
|
| 127 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 128 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&display=swap" rel="stylesheet">
|
| 129 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
| 130 |
+
<style>''' + BASE_STYLE + '''
|
| 131 |
+
body { padding-bottom: 30px; }
|
| 132 |
+
.public-header { padding: 15px; text-align: center; border-bottom: 1px solid #333; margin-bottom: 20px; }
|
| 133 |
+
.item { cursor: default; }
|
| 134 |
+
.item .download-icon { position: absolute; top: 10px; right: 10px; font-size: 1.2em; color: var(--text-muted); cursor: pointer; transition: var(--transition); }
|
| 135 |
+
.item .download-icon:hover { color: var(--secondary); }
|
| 136 |
+
.list-view .item .download-icon { position: static; margin-left: auto; padding: 5px 10px; }
|
| 137 |
+
</style></head><body>
|
| 138 |
+
<div class="public-header">
|
| 139 |
+
<h1>Общая папка</h1>
|
| 140 |
+
<h2>{{ folder.name }}</h2>
|
| 141 |
+
<p style="color: var(--text-muted);">Автор: {{ user.first_name or user.telegram_username }}</p>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="container" style="padding-top: 15px;">
|
| 144 |
+
<div class="file-grid list-view">
|
| 145 |
+
{% for item in items %}
|
| 146 |
+
<div class="item {{ item.type }}">
|
| 147 |
+
<div class="item-preview-wrapper">
|
| 148 |
+
{% if item.type == 'folder' %}
|
| 149 |
+
<a href="{{ url_for('shared_folder_view', link_id=link.id, subfolder_id=item.id) }}" style="text-decoration: none; color: inherit;">
|
| 150 |
+
<div class="item-preview"><i class="fa-solid fa-folder"></i></div>
|
| 151 |
+
</a>
|
| 152 |
+
{% elif item.type == 'note' %}
|
| 153 |
+
<div class="item-preview"><i class="fa-solid fa-note-sticky"></i></div>
|
| 154 |
+
{% elif item.file_type == 'image' %}<div class="item-preview" style="background-image: url({{ hf_file_url_jinja(item.path) }}); background-size: cover; background-position: center;"></div>
|
| 155 |
+
{% else %}<div class="item-preview" style="font-size: 1.8em; display: flex; align-items: center; justify-content: center;"><i class="fa-solid fa-file"></i></div>{% endif %}
|
| 156 |
+
</div>
|
| 157 |
+
<div class="item-name-info">
|
| 158 |
+
<p class="item-name">
|
| 159 |
+
{% if item.type == 'folder' %}
|
| 160 |
+
<a href="{{ url_for('shared_folder_view', link_id=link.id, subfolder_id=item.id) }}" style="text-decoration: none; color: inherit;">{{ item.name }}</a>
|
| 161 |
+
{% else %}
|
| 162 |
+
{{ item.title if item.type == 'note' else item.original_filename }}
|
| 163 |
+
{% endif %}
|
| 164 |
+
</p>
|
| 165 |
+
<p class="item-info">{% if item.type == 'file' %}{{ item.upload_date }}{% elif item.type == 'note' %}{{ item.modified_date }}{% endif %}</p>
|
| 166 |
+
</div>
|
| 167 |
+
{% if item.type != 'folder' %}
|
| 168 |
+
<a href="{{ url_for('public_download_via_link', link_id=link.id, item_id=item.id) }}" class="download-icon" title="Скачать">
|
| 169 |
+
<i class="fa-solid fa-download"></i>
|
| 170 |
+
</a>
|
| 171 |
+
{% endif %}
|
| 172 |
+
</div>
|
| 173 |
+
{% endfor %}
|
| 174 |
+
{% if not items %}<p>Эта папка пуста.</p>{% endif %}
|
| 175 |
+
</div>
|
| 176 |
+
</div></body></html>
|
| 177 |
'''
|
| 178 |
|
| 179 |
def find_node_by_id(filesystem, node_id):
|
|
|
|
| 223 |
if exclude_ids is None:
|
| 224 |
exclude_ids = set()
|
| 225 |
folders = []
|
|
|
|
| 226 |
def traverse(node, path_prefix):
|
| 227 |
if node.get('type') == 'folder':
|
| 228 |
if node.get('id') not in exclude_ids:
|
|
|
|
| 231 |
new_prefix = f"{path_prefix}{node.get('name', '')}/" if node.get('id') != 'root' else ""
|
| 232 |
for child in node.get('children', []):
|
| 233 |
traverse(child, new_prefix)
|
|
|
|
| 234 |
traverse(filesystem, "")
|
| 235 |
return sorted(folders, key=lambda x: x['name'].lower())
|
| 236 |
|
|
|
|
| 270 |
try:
|
| 271 |
download_db_from_hf()
|
| 272 |
with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file)
|
| 273 |
+
if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}}
|
| 274 |
data.setdefault('users', {})
|
| 275 |
+
data.setdefault('shared_links', {})
|
| 276 |
for tma_user_id_str, user_data_item in data['users'].items():
|
| 277 |
initialize_user_filesystem_tma(user_data_item, tma_user_id_str)
|
| 278 |
user_data_item.setdefault('reminders', [])
|
| 279 |
return data
|
| 280 |
except Exception as e:
|
| 281 |
logging.error(f"Error loading data: {e}")
|
| 282 |
+
return {'users': {}, 'shared_links': {}}
|
| 283 |
|
| 284 |
def save_data(data):
|
| 285 |
try:
|
|
|
|
| 302 |
def download_db_from_hf():
|
| 303 |
if not HF_TOKEN_READ:
|
| 304 |
if not os.path.exists(DATA_FILE):
|
| 305 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
|
| 306 |
return
|
| 307 |
try:
|
| 308 |
hf_hub_download(repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False)
|
| 309 |
except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
|
| 310 |
if not os.path.exists(DATA_FILE):
|
| 311 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
|
| 312 |
except Exception as e:
|
| 313 |
logging.error(f"Error downloading database: {e}")
|
| 314 |
if not os.path.exists(DATA_FILE):
|
| 315 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
|
| 316 |
|
| 317 |
def periodic_backup():
|
| 318 |
while True:
|
|
|
|
| 528 |
|
| 529 |
<div id="selection-bar">
|
| 530 |
<span id="selection-count"></span>
|
| 531 |
+
<button id="selection-share-btn" class="btn share-btn" onclick="openShareModal()" style="display:none;"><i class="fa-solid fa-share-alt"></i></button>
|
| 532 |
+
<button id="selection-download-btn" class="btn download-btn" onclick="downloadSingleSelected()" style="display:none;"><i class="fa-solid fa-download"></i></button>
|
| 533 |
<button class="btn" style="background: var(--accent);" onclick="showMoveModal()"><i class="fa-solid fa-arrow-right-to-bracket"></i></button>
|
| 534 |
<button class="btn delete-btn" onclick="deleteSelected()"><i class="fa-solid fa-trash-can"></i></button>
|
| 535 |
<button class="btn" style="background: #555;" onclick="toggleSelectionMode(false)">Отмена</button>
|
|
|
|
| 545 |
</div></div>
|
| 546 |
|
| 547 |
<div class="modal" id="fab-modal"><div class="modal-content">
|
| 548 |
+
<h4>Добавить в "{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}"</h4>
|
| 549 |
+
<div class="fab-options">
|
| 550 |
+
<label for="file-input" class="fab-option" id="fab-option-upload">
|
| 551 |
+
<i class="fa-solid fa-upload"></i><span>Файлы</span>
|
| 552 |
+
</label>
|
| 553 |
+
<div class="fab-option" id="fab-option-note" onclick="openNoteModal()">
|
| 554 |
+
<i class="fa-solid fa-note-sticky"></i><span>Заметку</span>
|
| 555 |
+
</div>
|
| 556 |
+
<div class="fab-option" id="fab-option-folder">
|
| 557 |
+
<i class="fa-solid fa-folder-plus"></i><span>Папку</span>
|
| 558 |
+
</div>
|
| 559 |
+
</div>
|
| 560 |
+
<form id="upload-form" method="POST" enctype="multipart/form-data" action="{{ url_for('tma_dashboard') }}" style="display:none;">
|
| 561 |
<input type="hidden" name="current_folder_id" value="{{ current_folder_id }}">
|
| 562 |
<input type="file" name="files" id="file-input" multiple required onchange="document.getElementById('upload-btn-modal').click()">
|
| 563 |
+
<button type="submit" id="upload-btn-modal"></button>
|
|
|
|
| 564 |
</form>
|
| 565 |
+
<form method="POST" action="{{ url_for('create_folder_tma') }}" id="create-folder-form">
|
|
|
|
| 566 |
<input type="hidden" name="parent_folder_id" value="{{ current_folder_id }}">
|
| 567 |
<input type="text" name="folder_name" placeholder="Имя новой папки" required>
|
| 568 |
+
<button type="submit" class="btn folder-btn" style="width:100%">Создать</button>
|
| 569 |
</form>
|
| 570 |
+
<button class="btn" style="background: #555; width: 100%; margin-top: 10px;" onclick="closeFabModal()">Закрыть</button>
|
| 571 |
</div></div>
|
| 572 |
|
| 573 |
<div class="modal" id="note-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
|
|
|
| 593 |
</div>
|
| 594 |
</div></div>
|
| 595 |
|
| 596 |
+
<div class="modal" id="share-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 597 |
+
<h4>Поделиться папкой</h4>
|
| 598 |
+
<div id="existing-links-list" style="max-height: 30vh; overflow-y: auto; margin-bottom: 15px;"></div>
|
| 599 |
+
<h5 style="margin-top: 15px;">Создать новую ссылку</h5>
|
| 600 |
+
<input type="text" id="share-link-name" placeholder="Название ссылки (необязательно)">
|
| 601 |
+
<select id="share-link-duration">
|
| 602 |
+
<option value="1">1 час</option><option value="24">24 часа</option><option value="168">7 дней</option><option value="0">Всегда</option>
|
| 603 |
+
</select>
|
| 604 |
+
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
| 605 |
+
<button class="btn share-btn" style="flex-grow: 1;" onclick="createShareLink()">Создать</button>
|
| 606 |
+
<button class="btn" style="background: #555; flex-grow: 1;" onclick="closeShareModal()">Закрыть</button>
|
| 607 |
+
</div>
|
| 608 |
+
</div></div>
|
| 609 |
+
|
| 610 |
<div class="fab-container"><button id="fab" class="fab"><i class="fa-solid fa-plus"></i></button></div>
|
| 611 |
|
| 612 |
<script>
|
|
|
|
| 685 |
const selectionBar = document.getElementById('selection-bar');
|
| 686 |
const selectionCount = document.getElementById('selection-count');
|
| 687 |
const selectionDownloadBtn = document.getElementById('selection-download-btn');
|
| 688 |
+
const selectionShareBtn = document.getElementById('selection-share-btn');
|
| 689 |
const allItems = document.querySelectorAll('.item');
|
| 690 |
function toggleSelectionMode(enable) {
|
| 691 |
selectionMode = enable;
|
|
|
|
| 697 |
selectionCount.textContent = `Выбрано: ${selectedItems.size}`;
|
| 698 |
const firstSelectedId = selectedItems.values().next().value;
|
| 699 |
const itemElement = document.querySelector(`.item[data-id='${firstSelectedId}']`);
|
| 700 |
+
selectionDownloadBtn.style.display = (selectedItems.size === 1 && itemElement?.dataset.type === 'file') ? 'inline-block' : 'none';
|
| 701 |
+
selectionShareBtn.style.display = (selectedItems.size === 1 && itemElement?.dataset.type === 'folder') ? 'inline-block' : 'none';
|
|
|
|
| 702 |
}
|
| 703 |
allItems.forEach(item => {
|
| 704 |
item.addEventListener('pointerdown', e => {
|
|
|
|
| 847 |
}
|
| 848 |
});
|
| 849 |
}
|
| 850 |
+
function closeShareModal() { document.getElementById('share-modal').style.display = 'none'; }
|
| 851 |
+
async function openShareModal() {
|
| 852 |
+
if (selectedItems.size !== 1) return;
|
| 853 |
+
haptic.impactOccurred('light');
|
| 854 |
+
const folderId = selectedItems.values().next().value;
|
| 855 |
+
const listEl = document.getElementById('existing-links-list');
|
| 856 |
+
listEl.innerHTML = '<div class="loading-spinner"></div>';
|
| 857 |
+
document.getElementById('share-modal').style.display = 'flex';
|
| 858 |
+
const response = await fetch(`{{ url_for('get_public_links', folder_id='FOLDER_ID') }}`.replace('FOLDER_ID', folderId));
|
| 859 |
+
const data = await response.json();
|
| 860 |
+
listEl.innerHTML = '';
|
| 861 |
+
if (data.status === 'success' && data.links.length > 0) {
|
| 862 |
+
data.links.forEach(link => {
|
| 863 |
+
const el = document.createElement('div');
|
| 864 |
+
el.className = 'shared-link-item';
|
| 865 |
+
const expiration = link.expires_at ? new Date(link.expires_at).toLocaleString() : 'Никогда';
|
| 866 |
+
el.innerHTML = `
|
| 867 |
+
<div class="shared-link-info">
|
| 868 |
+
<strong>${link.name || 'Безымянная ссылка'}</strong>
|
| 869 |
+
<small>Истекает: ${expiration}</small>
|
| 870 |
+
</div>
|
| 871 |
+
<div class="shared-link-actions">
|
| 872 |
+
<button onclick="copyToClipboard('${link.url}')" title="Копировать"><i class="fa-solid fa-copy"></i></button>
|
| 873 |
+
<button onclick="deleteShareLink('${link.id}')" title="Удалить"><i class="fa-solid fa-trash"></i></button>
|
| 874 |
+
</div>`;
|
| 875 |
+
listEl.appendChild(el);
|
| 876 |
+
});
|
| 877 |
+
} else {
|
| 878 |
+
listEl.innerHTML = '<p>Публичных ссылок для этой папки нет.</p>';
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
async function createShareLink() {
|
| 882 |
+
const folderId = selectedItems.values().next().value;
|
| 883 |
+
const name = document.getElementById('share-link-name').value;
|
| 884 |
+
const duration_hours = document.getElementById('share-link-duration').value;
|
| 885 |
+
const response = await fetch('{{ url_for("create_public_link") }}', {
|
| 886 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 887 |
+
body: JSON.stringify({ folder_id: folderId, name: name, duration_hours: parseInt(duration_hours) })
|
| 888 |
+
});
|
| 889 |
+
const result = await response.json();
|
| 890 |
+
if (result.status === 'success') {
|
| 891 |
+
haptic.notificationOccurred('success');
|
| 892 |
+
openShareModal();
|
| 893 |
+
copyToClipboard(result.url);
|
| 894 |
+
Telegram.WebApp.showAlert('Ссылка создана и скопирована!');
|
| 895 |
+
} else {
|
| 896 |
+
haptic.notificationOccurred('error');
|
| 897 |
+
Telegram.WebApp.showAlert(result.message || 'Ошибка создания ссылки.');
|
| 898 |
+
}
|
| 899 |
+
}
|
| 900 |
+
async function deleteShareLink(linkId) {
|
| 901 |
+
Telegram.WebApp.showConfirm('Удалить эту публичную ссылку?', async (ok) => {
|
| 902 |
+
if(ok) {
|
| 903 |
+
haptic.impactOccurred('heavy');
|
| 904 |
+
const response = await fetch('{{ url_for("delete_public_link") }}', {
|
| 905 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 906 |
+
body: JSON.stringify({ link_id: linkId })
|
| 907 |
+
});
|
| 908 |
+
const result = await response.json();
|
| 909 |
+
if (result.status === 'success') { openShareModal(); }
|
| 910 |
+
else { Telegram.WebApp.showAlert('Ошибка удаления.'); }
|
| 911 |
+
}
|
| 912 |
+
});
|
| 913 |
+
}
|
| 914 |
+
function copyToClipboard(text) {
|
| 915 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 916 |
+
haptic.notificationOccurred('success');
|
| 917 |
+
Telegram.WebApp.showAlert('Скопировано!');
|
| 918 |
+
}, () => {
|
| 919 |
+
haptic.notificationOccurred('error');
|
| 920 |
+
});
|
| 921 |
+
}
|
| 922 |
document.getElementById('reminders-btn').addEventListener('click', openRemindersModal);
|
| 923 |
const gridViewBtn = document.getElementById('grid-view-btn');
|
| 924 |
const listViewBtn = document.getElementById('list-view-btn');
|
|
|
|
| 935 |
const fab = document.getElementById('fab');
|
| 936 |
const fabModal = document.getElementById('fab-modal');
|
| 937 |
fab.addEventListener('click', () => { haptic.impactOccurred('medium'); fabModal.style.display = 'flex'; });
|
| 938 |
+
function closeFabModal() {
|
| 939 |
+
fabModal.style.display = 'none';
|
| 940 |
+
document.getElementById('create-folder-form').style.display = 'none';
|
| 941 |
+
}
|
| 942 |
fabModal.addEventListener('click', e => { if (e.target.id === 'fab-modal') closeFabModal(); });
|
| 943 |
+
document.getElementById('fab-option-folder').addEventListener('click', () => {
|
| 944 |
+
document.getElementById('create-folder-form').style.display = 'block';
|
| 945 |
+
});
|
| 946 |
document.addEventListener('DOMContentLoaded', () => {
|
| 947 |
setView(localStorage.getItem('viewMode') || 'grid');
|
| 948 |
const currentFolderId = '{{ current_folder_id }}';
|
|
|
|
| 1137 |
if 'telegram_user_id' not in session: return Response("Unauthorized", 401)
|
| 1138 |
file_ids_str = request.args.get('file_ids')
|
| 1139 |
if not file_ids_str: return Response("No file IDs provided", 400)
|
|
|
|
| 1140 |
file_ids = file_ids_str.split(',')
|
|
|
|
| 1141 |
temp_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
|
|
|
|
| 1142 |
try:
|
| 1143 |
with zipfile.ZipFile(temp_zip_file.name, 'w', zipfile.ZIP_DEFLATED) as zf:
|
| 1144 |
for file_id in file_ids:
|
|
|
|
| 1373 |
flash('Вы вышли из сессии приложения.')
|
| 1374 |
return redirect(url_for('tma_entry_page'))
|
| 1375 |
|
| 1376 |
+
@app.route('/create_public_link', methods=['POST'])
|
| 1377 |
+
def create_public_link():
|
| 1378 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1379 |
+
tma_user_id = session['telegram_user_id']
|
| 1380 |
+
data = load_data()
|
| 1381 |
+
user_data = data['users'].get(tma_user_id)
|
| 1382 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1383 |
+
|
| 1384 |
+
payload = request.json
|
| 1385 |
+
folder_id = payload.get('folder_id')
|
| 1386 |
+
name = payload.get('name')
|
| 1387 |
+
duration_hours = payload.get('duration_hours', 0)
|
| 1388 |
+
|
| 1389 |
+
folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id)
|
| 1390 |
+
if not folder_node or folder_node.get('type') != 'folder':
|
| 1391 |
+
return jsonify({'status': 'error', 'message': 'Папка не найдена.'}), 404
|
| 1392 |
+
|
| 1393 |
+
now = datetime.now(pytz.utc)
|
| 1394 |
+
expires_at = None
|
| 1395 |
+
if duration_hours > 0:
|
| 1396 |
+
expires_at = now + timedelta(hours=duration_hours)
|
| 1397 |
+
expires_at_iso = expires_at.isoformat()
|
| 1398 |
+
else:
|
| 1399 |
+
expires_at_iso = None
|
| 1400 |
+
|
| 1401 |
+
link_id = uuid.uuid4().hex
|
| 1402 |
+
link_data = {
|
| 1403 |
+
'id': link_id,
|
| 1404 |
+
'user_id': tma_user_id,
|
| 1405 |
+
'folder_id': folder_id,
|
| 1406 |
+
'name': name,
|
| 1407 |
+
'created_at': now.isoformat(),
|
| 1408 |
+
'expires_at': expires_at_iso
|
| 1409 |
+
}
|
| 1410 |
+
data['shared_links'][link_id] = link_data
|
| 1411 |
+
|
| 1412 |
+
folder_node.setdefault('public_links', []).append(link_id)
|
| 1413 |
+
|
| 1414 |
+
try:
|
| 1415 |
+
save_data(data)
|
| 1416 |
+
public_url = url_for('shared_folder_view', link_id=link_id, _external=True)
|
| 1417 |
+
return jsonify({'status': 'success', 'url': public_url})
|
| 1418 |
+
except Exception as e:
|
| 1419 |
+
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 1420 |
+
|
| 1421 |
+
@app.route('/delete_public_link', methods=['POST'])
|
| 1422 |
+
def delete_public_link():
|
| 1423 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1424 |
+
tma_user_id = session['telegram_user_id']
|
| 1425 |
+
data = load_data()
|
| 1426 |
+
|
| 1427 |
+
link_id = request.json.get('link_id')
|
| 1428 |
+
link_data = data['shared_links'].get(link_id)
|
| 1429 |
+
|
| 1430 |
+
if not link_data or link_data.get('user_id') != tma_user_id:
|
| 1431 |
+
return jsonify({'status': 'error', 'message': 'Ссылка не найдена или нет доступа.'}), 404
|
| 1432 |
+
|
| 1433 |
+
folder_id = link_data.get('folder_id')
|
| 1434 |
+
user_data = data['users'].get(tma_user_id)
|
| 1435 |
+
if user_data:
|
| 1436 |
+
folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
|
| 1437 |
+
if folder and 'public_links' in folder:
|
| 1438 |
+
folder['public_links'] = [l for l in folder['public_links'] if l != link_id]
|
| 1439 |
+
|
| 1440 |
+
del data['shared_links'][link_id]
|
| 1441 |
+
|
| 1442 |
+
try:
|
| 1443 |
+
save_data(data)
|
| 1444 |
+
return jsonify({'status': 'success'})
|
| 1445 |
+
except Exception as e:
|
| 1446 |
+
return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500
|
| 1447 |
+
|
| 1448 |
+
@app.route('/get_public_links/<folder_id>')
|
| 1449 |
+
def get_public_links(folder_id):
|
| 1450 |
+
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401
|
| 1451 |
+
tma_user_id = session['telegram_user_id']
|
| 1452 |
+
data = load_data()
|
| 1453 |
+
user_data = data['users'].get(tma_user_id)
|
| 1454 |
+
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404
|
| 1455 |
+
|
| 1456 |
+
folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
|
| 1457 |
+
if not folder: return jsonify({'status': 'error', 'message': 'Папка не найдена.'}), 404
|
| 1458 |
+
|
| 1459 |
+
link_ids = folder.get('public_links', [])
|
| 1460 |
+
links_details = []
|
| 1461 |
+
for link_id in link_ids:
|
| 1462 |
+
link_data = data['shared_links'].get(link_id)
|
| 1463 |
+
if link_data:
|
| 1464 |
+
link_data['url'] = url_for('shared_folder_view', link_id=link_id, _external=True)
|
| 1465 |
+
links_details.append(link_data)
|
| 1466 |
+
|
| 1467 |
+
return jsonify({'status': 'success', 'links': links_details})
|
| 1468 |
+
|
| 1469 |
+
@app.route('/shared/<link_id>')
|
| 1470 |
+
@app.route('/shared/<link_id>/<subfolder_id>')
|
| 1471 |
+
def shared_folder_view(link_id, subfolder_id=None):
|
| 1472 |
+
data = load_data()
|
| 1473 |
+
link_data = data['shared_links'].get(link_id)
|
| 1474 |
+
|
| 1475 |
+
if not link_data: return "Ссылка недействительна.", 404
|
| 1476 |
+
|
| 1477 |
+
if link_data.get('expires_at'):
|
| 1478 |
+
expires_at = datetime.fromisoformat(link_data['expires_at'])
|
| 1479 |
+
if datetime.now(pytz.utc) > expires_at:
|
| 1480 |
+
return "Срок действия ссылки истек.", 410
|
| 1481 |
+
|
| 1482 |
+
user_id = link_data['user_id']
|
| 1483 |
+
user_data = data['users'].get(user_id)
|
| 1484 |
+
if not user_data: return "Владелец не найден.", 404
|
| 1485 |
+
|
| 1486 |
+
folder_id_to_show = subfolder_id if subfolder_id else link_data['folder_id']
|
| 1487 |
+
folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id_to_show)
|
| 1488 |
+
|
| 1489 |
+
if not folder_node or folder_node.get('type') != 'folder':
|
| 1490 |
+
return "Папка не найдена.", 404
|
| 1491 |
+
|
| 1492 |
+
items_in_folder = sorted(folder_node.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', x.get('title', ''))).lower()))
|
| 1493 |
+
|
| 1494 |
+
return render_template_string(PUBLIC_SHARE_PAGE_HTML, folder=folder_node, items=items_in_folder, user=user_data, link=link_data, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}")
|
| 1495 |
+
|
| 1496 |
+
@app.route('/public_download/<link_id>/<item_id>')
|
| 1497 |
+
def public_download_via_link(link_id, item_id):
|
| 1498 |
+
data = load_data()
|
| 1499 |
+
link_data = data['shared_links'].get(link_id)
|
| 1500 |
+
if not link_data: return Response("Ссылка неде��ствительна.", status=404)
|
| 1501 |
+
|
| 1502 |
+
if link_data.get('expires_at'):
|
| 1503 |
+
expires_at = datetime.fromisoformat(link_data['expires_at'])
|
| 1504 |
+
if datetime.now(pytz.utc) > expires_at:
|
| 1505 |
+
return Response("Срок действия ссылки истек.", status=410)
|
| 1506 |
+
|
| 1507 |
+
user_id = link_data['user_id']
|
| 1508 |
+
user_data = data['users'].get(user_id)
|
| 1509 |
+
if not user_data: return Response("Владелец не найден.", status=404)
|
| 1510 |
+
|
| 1511 |
+
item_node, _ = find_node_by_id(user_data['filesystem'], item_id)
|
| 1512 |
+
if not item_node: return Response("Элемент не найден.", status=404)
|
| 1513 |
+
|
| 1514 |
+
token = uuid.uuid4().hex
|
| 1515 |
+
cache.set(f"download_token_{token}", item_node, timeout=300)
|
| 1516 |
+
return redirect(url_for('public_download', token=token))
|
| 1517 |
+
|
| 1518 |
ADMIN_LOGIN_HTML = '''
|
| 1519 |
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Admin Login</title>
|
| 1520 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
| 2012 |
download_db_from_hf()
|
| 2013 |
else:
|
| 2014 |
if not os.path.exists(DATA_FILE):
|
| 2015 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}}, f)
|
| 2016 |
|
| 2017 |
if HF_TOKEN_WRITE:
|
| 2018 |
threading.Thread(target=periodic_backup, daemon=True).start()
|
| 2019 |
|
| 2020 |
threading.Thread(target=check_reminders, daemon=True).start()
|
| 2021 |
|
| 2022 |
+
app.run(debug=False, host='0.0.0.0', port=7860)
|