Eluza133 commited on
Commit
9f6bef7
·
verified ·
1 Parent(s): 2c98c55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -149
app.py CHANGED
@@ -14,11 +14,12 @@ from werkzeug.utils import secure_filename
14
  import requests
15
  from io import BytesIO
16
  import uuid
 
17
 
18
  # --- Configuration ---
19
  app = Flask(__name__)
20
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_mini_app_unique")
21
- BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4') # MUST be set
22
  DATA_FILE = 'cloudeng_mini_app_data.json'
23
  REPO_ID = "Eluza133/Z1e1u" # Same HF Repo
24
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
@@ -62,7 +63,6 @@ def add_node(filesystem, parent_id, node_data):
62
  if parent_node and parent_node.get('type') == 'folder':
63
  if 'children' not in parent_node:
64
  parent_node['children'] = []
65
- # Prevent adding duplicates by id
66
  existing_ids = {child.get('id') for child in parent_node['children']}
67
  if node_data.get('id') not in existing_ids:
68
  parent_node['children'].append(node_data)
@@ -74,8 +74,7 @@ def remove_node(filesystem, node_id):
74
  if node_to_remove and parent_node and 'children' in parent_node:
75
  original_length = len(parent_node['children'])
76
  parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
77
- return len(parent_node['children']) < original_length # Return True if something was removed
78
- # Handle root node deletion attempt (should not happen normally)
79
  if node_to_remove and node_id == filesystem.get('id'):
80
  logging.warning("Attempted to remove root node directly.")
81
  return False
@@ -84,9 +83,7 @@ def remove_node(filesystem, node_id):
84
  def get_node_path_list(filesystem, node_id):
85
  path_list = []
86
  current_id = node_id
87
-
88
  processed_ids = set()
89
-
90
  while current_id and current_id not in processed_ids:
91
  processed_ids.add(current_id)
92
  node, parent = find_node_by_id(filesystem, current_id)
@@ -99,26 +96,20 @@ def get_node_path_list(filesystem, node_id):
99
  if not parent:
100
  break
101
  parent_id = parent.get('id')
102
- if parent_id == current_id: # Prevent infinite loop if parent is self
103
  logging.error(f"Filesystem loop detected at node {current_id}")
104
  break
105
  current_id = parent_id
106
-
107
- # Ensure root is always first if found, otherwise add default root
108
  if not any(p['id'] == 'root' for p in path_list):
109
  path_list.append({'id': 'root', 'name': 'Root'})
110
-
111
- # Filter out potential duplicates while preserving order, then reverse
112
  final_path = []
113
  seen_ids = set()
114
  for item in reversed(path_list):
115
  if item['id'] not in seen_ids:
116
  final_path.append(item)
117
  seen_ids.add(item['id'])
118
-
119
  return final_path
120
 
121
-
122
  def initialize_user_filesystem(user_data):
123
  if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict):
124
  user_data['filesystem'] = {
@@ -129,7 +120,7 @@ def initialize_user_filesystem(user_data):
129
  }
130
 
131
  # --- Data Loading/Saving ---
132
- @cache.memoize(timeout=120) # Cache for 2 minutes
133
  def load_data():
134
  try:
135
  download_db_from_hf()
@@ -139,7 +130,6 @@ def load_data():
139
  logging.warning("Data file is not a dict, initializing empty.")
140
  return {'users': {}}
141
  data.setdefault('users', {})
142
- # Ensure all users have a valid filesystem structure
143
  for user_id, user_data in data['users'].items():
144
  initialize_user_filesystem(user_data)
145
  logging.info("Data loaded and filesystems checked/initialized.")
@@ -158,14 +148,11 @@ def save_data(data):
158
  try:
159
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
160
  json.dump(data, file, ensure_ascii=False, indent=4)
161
- # Upload immediately after saving
162
  upload_db_to_hf()
163
- cache.clear() # Clear cache after saving
164
  logging.info("Data saved locally and upload to HF initiated.")
165
  except Exception as e:
166
  logging.error(f"Error saving data: {e}")
167
- # Consider not raising here to potentially allow app to continue
168
- # raise
169
 
170
  def upload_db_to_hf():
171
  if not HF_TOKEN_WRITE:
@@ -180,7 +167,7 @@ def upload_db_to_hf():
180
  repo_type="dataset",
181
  token=HF_TOKEN_WRITE,
182
  commit_message=f"Backup MiniApp {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
183
- run_as_future=True # Upload in background
184
  )
185
  logging.info("Database upload to Hugging Face scheduled.")
186
  except Exception as e:
@@ -202,8 +189,8 @@ def download_db_from_hf():
202
  token=HF_TOKEN_READ,
203
  local_dir=".",
204
  local_dir_use_symlinks=False,
205
- force_download=True, # Ensure we get the latest
206
- etag_timeout=10 # Short timeout for checking freshness
207
  )
208
  logging.info("Database downloaded from Hugging Face")
209
  except hf_utils.RepositoryNotFoundError:
@@ -232,7 +219,8 @@ def get_file_type(filename):
232
  return 'other'
233
 
234
  # --- Telegram Validation ---
235
- def check_telegram_authorization(auth_data: str, bot_token: str) -> dict | None:
 
236
  if not auth_data or not bot_token or bot_token == 'YOUR_BOT_TOKEN':
237
  logging.warning("Validation skipped: Missing auth_data or valid BOT_TOKEN.")
238
  return None
@@ -469,7 +457,6 @@ HTML_TEMPLATE = """
469
  showError("Ошибка: Данные авторизации отсутствуют.");
470
  throw new Error("Not authenticated");
471
  }
472
- // Include initData in every authenticated request
473
  body.initData = validatedInitData;
474
 
475
  try {
@@ -521,7 +508,6 @@ HTML_TEMPLATE = """
521
  flashDiv.textContent = message;
522
  flashContainerEl.innerHTML = ''; // Clear previous messages
523
  flashContainerEl.appendChild(flashDiv);
524
- // Auto-remove after 5 seconds
525
  setTimeout(() => {
526
  if (flashDiv.parentNode === flashContainerEl) {
527
  flashContainerEl.removeChild(flashDiv);
@@ -564,24 +550,21 @@ HTML_TEMPLATE = """
564
  let previewHtml = '';
565
  let actionsHtml = '';
566
  let filenameDisplay = item.original_filename || item.name || 'Unnamed';
567
-
568
- // Truncate long filenames for display
569
  const maxLen = 25;
570
  let truncatedFilename = filenameDisplay.length > maxLen
571
  ? filenameDisplay.substring(0, maxLen - 3) + '...'
572
  : filenameDisplay;
573
 
574
-
575
  if (item.type === 'folder') {
576
  previewHtml = `<a href="#" class="item-preview" title="Перейти в папку ${item.name}" onclick="event.preventDefault(); loadFolderContent('${item.id}')">📁</a>`;
577
  actionsHtml = `
578
  <button class="btn folder-btn" onclick="loadFolderContent('${item.id}')">Открыть</button>
579
  <button class="btn delete-btn" onclick="deleteFolder('${item.id}', '${item.name}')">Удалить</button>
580
  `;
581
- truncatedFilename = item.name; // Use folder name
582
  } else if (item.type === 'file') {
583
  const previewable = ['image', 'video', 'pdf', 'text'].includes(item.file_type);
584
- const dlUrl = `/download/${item.id}`; // Simple download URL
585
  let viewFuncCall = '';
586
 
587
  if (item.file_type === 'image') {
@@ -625,9 +608,11 @@ HTML_TEMPLATE = """
625
  modal.style.display = 'flex';
626
 
627
  try {
628
- // For PDF, use Google Docs viewer for broader compatibility
629
  if (type === 'pdf') {
630
- modalContent.innerHTML = `<iframe src="https://docs.google.com/gview?url=${encodeURIComponent(window.location.origin + srcOrUrl)}&embedded=true" title="Просмотр PDF"></iframe>`;
 
 
 
631
  } else if (type === 'image') {
632
  modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
633
  } else if (type === 'video') {
@@ -650,7 +635,6 @@ HTML_TEMPLATE = """
650
 
651
  function closeModal(event) {
652
  const modal = document.getElementById('mediaModal');
653
- // Close if clicking on the background, not the content itself
654
  if (event.target === modal) {
655
  closeModalManual();
656
  }
@@ -660,10 +644,10 @@ HTML_TEMPLATE = """
660
  const modal = document.getElementById('mediaModal');
661
  modal.style.display = 'none';
662
  const video = modal.querySelector('video');
663
- if (video) { video.pause(); video.src = ''; } // Stop playback and clear source
664
  const iframe = modal.querySelector('iframe');
665
- if (iframe) iframe.src = 'about:blank'; // Clear iframe
666
- document.getElementById('modalContent').innerHTML = ''; // Clear content
667
  }
668
 
669
  // --- Folder Operations ---
@@ -675,7 +659,7 @@ HTML_TEMPLATE = """
675
  if (data.status === 'ok') {
676
  currentItems = data.items || [];
677
  renderBreadcrumbs(data.breadcrumbs || [{'id': 'root', 'name': 'Root'}]);
678
- renderItems(currentItems.sort((a, b) => (a.type !== 'folder') - (b.type !== 'folder') || a.name.localeCompare(b.name))); // Folders first, then by name
679
  } else {
680
  showFlash(data.message || 'Не удалось загрузить содержимое папки.', 'error');
681
  }
@@ -719,40 +703,45 @@ HTML_TEMPLATE = """
719
  }
720
 
721
  async function deleteFolder(folderId, folderName) {
722
- if (!confirm(`Вы уверены, что хотите удалить папку "${folderName}"? Папку можно удалить только если она пуста.`)) {
723
- return;
724
- }
725
- try {
726
- const data = await apiCall(`/delete_folder/${folderId}`, 'POST', { current_folder_id: currentFolderId });
727
- if (data.status === 'ok') {
728
- showFlash(`Папка "${folderName}" удалена.`);
729
- loadFolderContent(currentFolderId); // Refresh
730
- } else {
731
- showFlash(data.message || 'Не удалось удалить папку.', 'error');
732
- }
733
- } catch (error) {
734
- // Error handled by apiCall
735
- }
 
 
 
 
 
736
  }
737
 
738
  async function deleteFile(fileId, fileName) {
739
- if (!confirm(`Вы уверены, что хотите удалить файл "${fileName}"?`)) {
740
- return;
741
- }
742
- try {
743
- const data = await apiCall(`/delete_file/${fileId}`, 'POST', { current_folder_id: currentFolderId });
744
- if (data.status === 'ok') {
745
- showFlash(`Файл "${fileName}" удален.`);
746
- loadFolderContent(currentFolderId); // Refresh
747
- } else {
748
- showFlash(data.message || 'Не удалось удалить файл.', 'error');
 
 
 
749
  }
750
- } catch (error) {
751
- // Error handled by apiCall
752
- }
753
  }
754
 
755
-
756
  // --- File Upload ---
757
  function handleFileUpload(event) {
758
  event.preventDefault();
@@ -777,7 +766,7 @@ HTML_TEMPLATE = """
777
  formData.append('files', files[i]);
778
  }
779
  formData.append('current_folder_id', currentFolderId);
780
- formData.append('initData', validatedInitData); // Add initData here
781
 
782
  const xhr = new XMLHttpRequest();
783
 
@@ -793,7 +782,7 @@ HTML_TEMPLATE = """
793
  uploadBtn.disabled = false;
794
  uploadBtn.textContent = 'Загрузить файлы сюда';
795
  progressContainer.style.display = 'none';
796
- fileInput.value = ''; // Reset file input
797
 
798
  if (xhr.status >= 200 && xhr.status < 300) {
799
  try {
@@ -827,7 +816,6 @@ HTML_TEMPLATE = """
827
  });
828
 
829
  xhr.open('POST', '/upload', true);
830
- // Do not set Content-Type, browser will set it correctly for FormData
831
  xhr.send(formData);
832
  }
833
 
@@ -836,7 +824,6 @@ HTML_TEMPLATE = """
836
  function initializeApp() {
837
  tg.ready();
838
  tg.expand();
839
- // Set background color based on theme
840
  document.body.style.backgroundColor = tg.themeParams.bg_color || '#ffffff';
841
  tg.setHeaderColor(tg.themeParams.secondary_bg_color || '#f1f1f1');
842
 
@@ -844,9 +831,8 @@ HTML_TEMPLATE = """
844
  showError("Ошибка: Не удалось получить данные авторизации Telegram (initData). Попробуйте перезапустить Mini App.");
845
  return;
846
  }
847
- validatedInitData = tg.initData; // Store raw initData
848
 
849
- // Validate initData with backend
850
  fetch('/validate_init_data', {
851
  method: 'POST',
852
  headers: { 'Content-Type': 'application/json' },
@@ -856,9 +842,9 @@ HTML_TEMPLATE = """
856
  .then(data => {
857
  if (data.status === 'ok' && data.user) {
858
  currentUser = data.user;
859
- userInfoHeaderEl.textContent = `Пользователь: ${currentUser.first_name || ''} ${currentUser.last_name || ''} (@${currentUser.username || currentUser.id})`;
860
  showAppContent();
861
- loadFolderContent('root'); // Load root folder initially
862
  } else {
863
  throw new Error(data.message || 'Не удалось верифицировать пользователя.');
864
  }
@@ -866,23 +852,23 @@ HTML_TEMPLATE = """
866
  .catch(error => {
867
  console.error("Validation failed:", error);
868
  showError(`Ошибка авторизации: ${error.message}. Попробуйте перезапустить.`);
869
- validatedInitData = null; // Invalidate data on error
870
  });
871
 
872
- // Add event listeners
873
  uploadForm.addEventListener('submit', handleFileUpload);
874
  createFolderBtn.addEventListener('click', handleCreateFolder);
875
 
876
- // Handle back button (optional, can conflict with folder navigation)
877
  // tg.BackButton.onClick(() => {
878
- // // Implement back navigation logic if needed, e.g., go to parent folder
879
- // console.log("Back button clicked");
 
 
 
 
 
880
  // });
881
- // if (currentFolderId !== 'root') {
882
- // tg.BackButton.show();
883
- // } else {
884
- // tg.BackButton.hide();
885
- // }
886
  }
887
 
888
  // --- Start the App ---
@@ -919,24 +905,22 @@ def validate_init_data():
919
  if tg_user_id not in users:
920
  logging.info(f"New user detected: {tg_user_id}. Initializing filesystem.")
921
  users[tg_user_id] = {
922
- 'user_info': user_info, # Store basic TG info
923
  'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
924
  }
925
  initialize_user_filesystem(users[tg_user_id])
926
  try:
927
- save_data(db_data) # Save immediately for new user
928
  except Exception as e:
929
  logging.error(f"Failed to save data for new user {tg_user_id}: {e}")
930
- # Allow login but maybe show warning later? Or deny login?
931
  return jsonify({"status": "error", "message": "Ошибка сохранения данных нового пользователя."}), 500
932
- elif 'user_info' not in users[tg_user_id]: # Update user info if missing
933
  users[tg_user_id]['user_info'] = user_info
934
  try:
935
  save_data(db_data)
936
  except Exception as e:
937
  logging.warning(f"Failed to update user_info for {tg_user_id}: {e}")
938
 
939
- # Ensure filesystem is initialized for existing users too (backward compatibility)
940
  if 'filesystem' not in users[tg_user_id]:
941
  initialize_user_filesystem(users[tg_user_id])
942
  try:
@@ -944,7 +928,6 @@ def validate_init_data():
944
  except Exception as e:
945
  logging.warning(f"Failed to initialize filesystem for existing user {tg_user_id}: {e}")
946
 
947
-
948
  return jsonify({"status": "ok", "user": user_info})
949
  else:
950
  logging.warning(f"Validation failed for initData: {init_data[:100]}...")
@@ -975,16 +958,15 @@ def get_dashboard_data():
975
 
976
  if not current_folder or current_folder.get('type') != 'folder':
977
  logging.warning(f"Folder {folder_id} not found for user {tg_user_id}. Defaulting to root.")
978
- folder_id = 'root' # Default to root if invalid folder requested
979
  current_folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
980
- if not current_folder: # Should not happen if root always exists
981
  logging.error(f"CRITICAL: Root folder not found for user {tg_user_id}")
982
  return jsonify({"status": "error", "message": "Критическая ошибка: Корневая папка отсутствует"}), 500
983
 
984
  items_in_folder = current_folder.get('children', [])
985
  breadcrumbs = get_node_path_list(user_data['filesystem'], folder_id)
986
 
987
- # Prepare data for JSON (avoid sending huge structures if possible)
988
  current_folder_info = {
989
  'id': current_folder.get('id'),
990
  'name': current_folder.get('name', 'Root')
@@ -1042,7 +1024,6 @@ def upload_files():
1042
  unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
1043
  file_id = uuid.uuid4().hex
1044
 
1045
- # Use TG User ID in the path
1046
  hf_path = f"cloud_files/{tg_user_id}/{current_folder_id}/{unique_filename}"
1047
  temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
1048
 
@@ -1065,10 +1046,9 @@ def upload_files():
1065
  uploaded_count += 1
1066
  needs_save = True
1067
  else:
1068
- # This case (add_node failing after folder check) should be rare
1069
  errors.append(f"Ошибка добавления метаданных для {original_filename}.")
1070
  logging.error(f"Failed add_node for {file_id} to {current_folder_id} for {tg_user_id}")
1071
- try: # Attempt to clean up orphaned HF file
1072
  api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
1073
  except Exception as del_err:
1074
  logging.error(f"Failed deleting orphaned HF file {hf_path}: {del_err}")
@@ -1093,7 +1073,7 @@ def upload_files():
1093
  final_message += " Ошибки: " + "; ".join(errors)
1094
 
1095
  return jsonify({
1096
- "status": "ok" if not errors else "error", # Consider status based on errors
1097
  "message": final_message
1098
  })
1099
 
@@ -1115,8 +1095,13 @@ def create_folder():
1115
 
1116
  if not folder_name:
1117
  return jsonify({'status': 'error', 'message': 'Имя папки не может быть пустым!'}), 400
 
1118
  if not folder_name.replace(' ', '').replace('-', '').replace('_', '').isalnum():
1119
- return jsonify({'status': 'error', 'message': 'Имя папки содержит недопустимые символы.'}), 400
 
 
 
 
1120
 
1121
  db_data = load_data()
1122
  user_data = db_data.get('users', {}).get(tg_user_id)
@@ -1135,26 +1120,18 @@ def create_folder():
1135
  return jsonify({'status': 'ok', 'message': f'Папка "{folder_name}" создана.'})
1136
  except Exception as e:
1137
  logging.error(f"Create folder save error ({tg_user_id}): {e}")
1138
- # Attempt to remove node if save failed? Complex recovery.
1139
  return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных.'}), 500
1140
  else:
1141
- # Could fail if parent_folder_id is invalid or already contains that ID (UUID collision unlikely)
1142
  return jsonify({'status': 'error', 'message': 'Не удалось найти родительскую папку или добавить узел.'}), 400
1143
 
1144
 
1145
  @app.route('/download/<file_id>')
1146
  def download_file_route(file_id):
1147
- """ Serves the file for download. NO AUTH HERE - relies on unguessable ID and/or public repo. """
1148
- # Security Note: This route currently lacks explicit auth per request.
1149
- # Assumes file_id is hard to guess. For private repos, access depends
1150
- # solely on HF_TOKEN_READ being correctly configured and the repo permissions.
1151
- # A more secure approach would involve validating initData or a temporary token first.
1152
-
1153
- db_data = load_data() # Load data to find file path
1154
  file_node = None
1155
  owner_user_id = None
1156
 
1157
- # Search across all users (less efficient, but needed without user context)
1158
  for user_id, user_data in db_data.get('users', {}).items():
1159
  if 'filesystem' in user_data:
1160
  node, _ = find_node_by_id(user_data['filesystem'], file_id)
@@ -1180,14 +1157,17 @@ def download_file_route(file_id):
1180
  if HF_TOKEN_READ:
1181
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1182
 
1183
- # Use stream=True for potentially large files
1184
- response = requests.get(file_url, headers=headers, stream=True, timeout=30) # 30 sec timeout
1185
- response.raise_for_status() # Check for 4xx/5xx errors
 
 
 
 
1186
 
1187
- # Stream the response
1188
  return Response(response.iter_content(chunk_size=8192),
1189
  mimetype=response.headers.get('Content-Type', 'application/octet-stream'),
1190
- headers={"Content-Disposition": f"attachment; filename=\"{original_filename}\""})
1191
 
1192
  except requests.exceptions.RequestException as e:
1193
  logging.error(f"Error downloading file from HF ({hf_path}, owner: {owner_user_id}): {e}")
@@ -1202,7 +1182,7 @@ def download_file_route(file_id):
1202
  def delete_file_route(file_id):
1203
  """ Deletes a file for the validated user. """
1204
  data = request.get_json()
1205
- if not data or 'initData' not in data or 'current_folder_id' not in data: # current_folder_id used for context/logging
1206
  return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1207
 
1208
  user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
@@ -1228,7 +1208,6 @@ def delete_file_route(file_id):
1228
  original_filename = file_node.get('original_filename', 'файл')
1229
  needs_save = False
1230
 
1231
- # Attempt HF deletion first (if path exists)
1232
  if hf_path:
1233
  try:
1234
  api = HfApi()
@@ -1239,19 +1218,16 @@ def delete_file_route(file_id):
1239
  logging.info(f"Deleted file {hf_path} from HF Hub for user {tg_user_id}")
1240
  except hf_utils.EntryNotFoundError:
1241
  logging.warning(f"File {hf_path} not found on HF Hub for delete attempt ({tg_user_id}).")
1242
- # Continue to remove from DB anyway
1243
  except Exception as e:
1244
  logging.error(f"Error deleting file from HF Hub ({hf_path}, {tg_user_id}): {e}")
1245
- return jsonify({'status': 'error', 'message': f'Ошибка удаления файла с сервера: {e}'}), 500
 
1246
 
1247
- # Remove from DB structure
1248
  if remove_node(user_data['filesystem'], file_id):
1249
  needs_save = True
1250
  logging.info(f"Removed file node {file_id} from DB for user {tg_user_id}")
1251
  else:
1252
- # This would be strange if find_node_by_id succeeded earlier
1253
  logging.error(f"Failed to remove file node {file_id} from DB structure for {tg_user_id} after HF delete.")
1254
- # Potentially inconsistent state, but proceed.
1255
 
1256
  if needs_save:
1257
  try:
@@ -1261,11 +1237,9 @@ def delete_file_route(file_id):
1261
  logging.error(f"Delete file DB save error ({tg_user_id}): {e}")
1262
  return jsonify({'status': 'error', 'message': 'Файл удален с сервера, но ошибка сохранения ��азы данных.'}), 500
1263
  else:
1264
- # If HF path was missing and remove_node failed (e.g., file not found initially)
1265
  return jsonify({'status': 'error', 'message': 'Файл не найден в базе данных для удаления.'}), 404
1266
 
1267
 
1268
-
1269
  @app.route('/delete_folder/<folder_id>', methods=['POST'])
1270
  def delete_folder_route(folder_id):
1271
  """ Deletes an empty folder for the validated user. """
@@ -1273,7 +1247,7 @@ def delete_folder_route(folder_id):
1273
  return jsonify({'status': 'error', 'message': 'Нельзя удалить корневую папку!'}), 400
1274
 
1275
  data = request.get_json()
1276
- if not data or 'initData' not in data or 'current_folder_id' not in data: # current_folder_id is parent context
1277
  return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1278
 
1279
  user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
@@ -1297,9 +1271,6 @@ def delete_folder_route(folder_id):
1297
  if folder_node.get('children'):
1298
  return jsonify({'status': 'error', 'message': f'Папку "{folder_name}" можно удалить только если она пуста.'}), 400
1299
 
1300
- # Note: We don't explicitly delete folders on HF Hub here, as HF manages directories implicitly.
1301
- # If the folder structure on HF needs cleanup, it might require separate logic or rely on deleting parent folders.
1302
-
1303
  if remove_node(user_data['filesystem'], folder_id):
1304
  try:
1305
  save_data(db_data)
@@ -1314,7 +1285,7 @@ def delete_folder_route(folder_id):
1314
 
1315
  @app.route('/get_text_content/<file_id>')
1316
  def get_text_content_route(file_id):
1317
- """ Serves text file content. NO AUTH HERE - see download route notes. """
1318
  db_data = load_data()
1319
  file_node = None
1320
  owner_user_id = None
@@ -1341,27 +1312,26 @@ def get_text_content_route(file_id):
1341
  if HF_TOKEN_READ:
1342
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1343
 
1344
- response = requests.get(file_url, headers=headers, timeout=15) # 15 sec timeout for text files
1345
  response.raise_for_status()
1346
 
1347
- # Limit preview size
1348
  max_preview_size = 1 * 1024 * 1024 # 1 MB
1349
  if len(response.content) > max_preview_size:
1350
  return Response("Файл слишком большой для предпросмотра (>1MB).", status=413)
1351
 
1352
- # Try common encodings
1353
  text_content = None
1354
- try: text_content = response.content.decode('utf-8')
1355
- except UnicodeDecodeError:
1356
- try: text_content = response.content.decode('cp1251') # Windows Cyrillic
1357
- except UnicodeDecodeError:
1358
- try: text_content = response.content.decode('latin-1') # Fallback
1359
- except Exception: pass
 
1360
 
1361
  if text_content is None:
1362
  return Response("Не удалось определить кодировку файла.", status=500)
1363
 
1364
- return Response(text_content, mimetype='text/plain; charset=utf-8') # Specify charset
1365
 
1366
  except requests.exceptions.RequestException as e:
1367
  logging.error(f"Error fetching text content from HF ({hf_path}, owner {owner_user_id}): {e}")
@@ -1374,14 +1344,7 @@ def get_text_content_route(file_id):
1374
 
1375
  @app.route('/preview_thumb/<file_id>')
1376
  def preview_thumb_route(file_id):
1377
- """ Serves a potentially resized image preview. NO AUTH HERE. """
1378
- # Placeholder: This currently just redirects to the full download.
1379
- # Real implementation would require image processing (e.g., Pillow)
1380
- # to generate thumbnails server-side, or rely on client-side rendering only.
1381
- # For simplicity, we redirect.
1382
- # return redirect(url_for('download_file_route', file_id=file_id))
1383
-
1384
- # Attempt direct serving like download, relying on browser caching/rendering
1385
  db_data = load_data()
1386
  file_node = None
1387
  owner_user_id = None
@@ -1398,7 +1361,6 @@ def preview_thumb_route(file_id):
1398
  hf_path = file_node.get('path')
1399
  if not hf_path: return Response("Путь к файлу не найден", status=500)
1400
 
1401
- # Serve directly without download=true for potential browser caching
1402
  file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}"
1403
 
1404
  try:
@@ -1406,10 +1368,12 @@ def preview_thumb_route(file_id):
1406
  if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1407
  response = requests.get(file_url, headers=headers, stream=True, timeout=20)
1408
  response.raise_for_status()
 
1409
  return Response(response.iter_content(chunk_size=8192), mimetype=response.headers.get('Content-Type', 'image/jpeg'))
1410
  except requests.exceptions.RequestException as e:
1411
  logging.error(f"Error fetching preview from HF ({hf_path}, owner: {owner_user_id}): {e}")
1412
- return Response(f"Ошибка загрузки превью ({e.response.status_code if e.response else 502})", status=e.response.status_code if e.response else 502)
 
1413
  except Exception as e:
1414
  logging.error(f"Unexpected error during preview ({hf_path}, owner: {owner_user_id}): {e}")
1415
  return Response("Внутренняя ошибка сервера при загрузке превью", status=500)
@@ -1427,11 +1391,8 @@ if __name__ == '__main__':
1427
  if not HF_TOKEN_READ:
1428
  logging.warning("HF_TOKEN_READ is not set. File downloads/previews might fail for private repos.")
1429
 
1430
- # Initial DB download before starting server
1431
  logging.info("Performing initial database download...")
1432
  download_db_from_hf()
1433
  logging.info("Initial download attempt complete.")
1434
 
1435
- # No periodic backup thread in this version for simplicity. Save triggers upload.
1436
-
1437
  app.run(debug=False, host='0.0.0.0', port=7860)
 
14
  import requests
15
  from io import BytesIO
16
  import uuid
17
+ from typing import Union, Optional # <-- Импорт добавлен
18
 
19
  # --- Configuration ---
20
  app = Flask(__name__)
21
  app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_mini_app_unique")
22
+ BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') # MUST be set
23
  DATA_FILE = 'cloudeng_mini_app_data.json'
24
  REPO_ID = "Eluza133/Z1e1u" # Same HF Repo
25
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
 
63
  if parent_node and parent_node.get('type') == 'folder':
64
  if 'children' not in parent_node:
65
  parent_node['children'] = []
 
66
  existing_ids = {child.get('id') for child in parent_node['children']}
67
  if node_data.get('id') not in existing_ids:
68
  parent_node['children'].append(node_data)
 
74
  if node_to_remove and parent_node and 'children' in parent_node:
75
  original_length = len(parent_node['children'])
76
  parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id]
77
+ return len(parent_node['children']) < original_length
 
78
  if node_to_remove and node_id == filesystem.get('id'):
79
  logging.warning("Attempted to remove root node directly.")
80
  return False
 
83
  def get_node_path_list(filesystem, node_id):
84
  path_list = []
85
  current_id = node_id
 
86
  processed_ids = set()
 
87
  while current_id and current_id not in processed_ids:
88
  processed_ids.add(current_id)
89
  node, parent = find_node_by_id(filesystem, current_id)
 
96
  if not parent:
97
  break
98
  parent_id = parent.get('id')
99
+ if parent_id == current_id:
100
  logging.error(f"Filesystem loop detected at node {current_id}")
101
  break
102
  current_id = parent_id
 
 
103
  if not any(p['id'] == 'root' for p in path_list):
104
  path_list.append({'id': 'root', 'name': 'Root'})
 
 
105
  final_path = []
106
  seen_ids = set()
107
  for item in reversed(path_list):
108
  if item['id'] not in seen_ids:
109
  final_path.append(item)
110
  seen_ids.add(item['id'])
 
111
  return final_path
112
 
 
113
  def initialize_user_filesystem(user_data):
114
  if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict):
115
  user_data['filesystem'] = {
 
120
  }
121
 
122
  # --- Data Loading/Saving ---
123
+ @cache.memoize(timeout=120)
124
  def load_data():
125
  try:
126
  download_db_from_hf()
 
130
  logging.warning("Data file is not a dict, initializing empty.")
131
  return {'users': {}}
132
  data.setdefault('users', {})
 
133
  for user_id, user_data in data['users'].items():
134
  initialize_user_filesystem(user_data)
135
  logging.info("Data loaded and filesystems checked/initialized.")
 
148
  try:
149
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
150
  json.dump(data, file, ensure_ascii=False, indent=4)
 
151
  upload_db_to_hf()
152
+ cache.clear()
153
  logging.info("Data saved locally and upload to HF initiated.")
154
  except Exception as e:
155
  logging.error(f"Error saving data: {e}")
 
 
156
 
157
  def upload_db_to_hf():
158
  if not HF_TOKEN_WRITE:
 
167
  repo_type="dataset",
168
  token=HF_TOKEN_WRITE,
169
  commit_message=f"Backup MiniApp {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
170
+ run_as_future=True
171
  )
172
  logging.info("Database upload to Hugging Face scheduled.")
173
  except Exception as e:
 
189
  token=HF_TOKEN_READ,
190
  local_dir=".",
191
  local_dir_use_symlinks=False,
192
+ force_download=True,
193
+ etag_timeout=10
194
  )
195
  logging.info("Database downloaded from Hugging Face")
196
  except hf_utils.RepositoryNotFoundError:
 
219
  return 'other'
220
 
221
  # --- Telegram Validation ---
222
+ # Use Optional[dict] which is equivalent to Union[dict, None]
223
+ def check_telegram_authorization(auth_data: str, bot_token: str) -> Optional[dict]:
224
  if not auth_data or not bot_token or bot_token == 'YOUR_BOT_TOKEN':
225
  logging.warning("Validation skipped: Missing auth_data or valid BOT_TOKEN.")
226
  return None
 
457
  showError("Ошибка: Данные авторизации отсутствуют.");
458
  throw new Error("Not authenticated");
459
  }
 
460
  body.initData = validatedInitData;
461
 
462
  try {
 
508
  flashDiv.textContent = message;
509
  flashContainerEl.innerHTML = ''; // Clear previous messages
510
  flashContainerEl.appendChild(flashDiv);
 
511
  setTimeout(() => {
512
  if (flashDiv.parentNode === flashContainerEl) {
513
  flashContainerEl.removeChild(flashDiv);
 
550
  let previewHtml = '';
551
  let actionsHtml = '';
552
  let filenameDisplay = item.original_filename || item.name || 'Unnamed';
 
 
553
  const maxLen = 25;
554
  let truncatedFilename = filenameDisplay.length > maxLen
555
  ? filenameDisplay.substring(0, maxLen - 3) + '...'
556
  : filenameDisplay;
557
 
 
558
  if (item.type === 'folder') {
559
  previewHtml = `<a href="#" class="item-preview" title="Перейти в папку ${item.name}" onclick="event.preventDefault(); loadFolderContent('${item.id}')">📁</a>`;
560
  actionsHtml = `
561
  <button class="btn folder-btn" onclick="loadFolderContent('${item.id}')">Открыть</button>
562
  <button class="btn delete-btn" onclick="deleteFolder('${item.id}', '${item.name}')">Удалить</button>
563
  `;
564
+ truncatedFilename = item.name;
565
  } else if (item.type === 'file') {
566
  const previewable = ['image', 'video', 'pdf', 'text'].includes(item.file_type);
567
+ const dlUrl = `/download/${item.id}`;
568
  let viewFuncCall = '';
569
 
570
  if (item.file_type === 'image') {
 
608
  modal.style.display = 'flex';
609
 
610
  try {
 
611
  if (type === 'pdf') {
612
+ // Check if running inside Telegram, if so, maybe open externally?
613
+ // Or use a viewer that works well in iframe like PDF.js (more complex setup)
614
+ // Using Google Docs viewer as a common fallback:
615
+ modalContent.innerHTML = `<iframe src="https://docs.google.com/gview?url=${encodeURIComponent(window.location.origin + srcOrUrl)}&embedded=true" title="Просмотр PDF"></iframe>`;
616
  } else if (type === 'image') {
617
  modalContent.innerHTML = `<img src="${srcOrUrl}" alt="Просмотр изображения">`;
618
  } else if (type === 'video') {
 
635
 
636
  function closeModal(event) {
637
  const modal = document.getElementById('mediaModal');
 
638
  if (event.target === modal) {
639
  closeModalManual();
640
  }
 
644
  const modal = document.getElementById('mediaModal');
645
  modal.style.display = 'none';
646
  const video = modal.querySelector('video');
647
+ if (video) { video.pause(); video.src = ''; }
648
  const iframe = modal.querySelector('iframe');
649
+ if (iframe) iframe.src = 'about:blank';
650
+ document.getElementById('modalContent').innerHTML = '';
651
  }
652
 
653
  // --- Folder Operations ---
 
659
  if (data.status === 'ok') {
660
  currentItems = data.items || [];
661
  renderBreadcrumbs(data.breadcrumbs || [{'id': 'root', 'name': 'Root'}]);
662
+ renderItems(currentItems.sort((a, b) => (a.type !== 'folder') - (b.type !== 'folder') || (a.name || a.original_filename || '').localeCompare(b.name || b.original_filename || '')));
663
  } else {
664
  showFlash(data.message || 'Не удалось загрузить содержимое папки.', 'error');
665
  }
 
703
  }
704
 
705
  async function deleteFolder(folderId, folderName) {
706
+ // Check if folder is empty client-side (optional optimization)
707
+ // const folderIsEmpty = !currentItems.some(item => item.parent_id === folderId);
708
+ // if (!folderIsEmpty) { showFlash('Папку можно удалить только если она пуста (клиентская проверка).', 'error'); return; }
709
+
710
+ tg.showConfirm(`Вы уверены, что хотите удалить папку "${folderName}"? Папку можно удалить только если она пуста.`, async (confirmed) => {
711
+ if (confirmed) {
712
+ try {
713
+ const data = await apiCall(`/delete_folder/${folderId}`, 'POST', { current_folder_id: currentFolderId });
714
+ if (data.status === 'ok') {
715
+ showFlash(`Папка "${folderName}" удалена.`);
716
+ loadFolderContent(currentFolderId); // Refresh
717
+ } else {
718
+ showFlash(data.message || 'Не удалось удалить папку.', 'error');
719
+ }
720
+ } catch (error) {
721
+ // Error handled by apiCall
722
+ }
723
+ }
724
+ });
725
  }
726
 
727
  async function deleteFile(fileId, fileName) {
728
+ tg.showConfirm(`Вы уверены, что хотите удалить файл "${fileName}"?`, async (confirmed) => {
729
+ if (confirmed) {
730
+ try {
731
+ const data = await apiCall(`/delete_file/${fileId}`, 'POST', { current_folder_id: currentFolderId });
732
+ if (data.status === 'ok') {
733
+ showFlash(`Файл "${fileName}" удален.`);
734
+ loadFolderContent(currentFolderId); // Refresh
735
+ } else {
736
+ showFlash(data.message || 'Не удалось удалить файл.', 'error');
737
+ }
738
+ } catch (error) {
739
+ // Error handled by apiCall
740
+ }
741
  }
742
+ });
 
 
743
  }
744
 
 
745
  // --- File Upload ---
746
  function handleFileUpload(event) {
747
  event.preventDefault();
 
766
  formData.append('files', files[i]);
767
  }
768
  formData.append('current_folder_id', currentFolderId);
769
+ formData.append('initData', validatedInitData);
770
 
771
  const xhr = new XMLHttpRequest();
772
 
 
782
  uploadBtn.disabled = false;
783
  uploadBtn.textContent = 'Загрузить файлы сюда';
784
  progressContainer.style.display = 'none';
785
+ fileInput.value = '';
786
 
787
  if (xhr.status >= 200 && xhr.status < 300) {
788
  try {
 
816
  });
817
 
818
  xhr.open('POST', '/upload', true);
 
819
  xhr.send(formData);
820
  }
821
 
 
824
  function initializeApp() {
825
  tg.ready();
826
  tg.expand();
 
827
  document.body.style.backgroundColor = tg.themeParams.bg_color || '#ffffff';
828
  tg.setHeaderColor(tg.themeParams.secondary_bg_color || '#f1f1f1');
829
 
 
831
  showError("Ошибка: Не удалось получить данные авторизации Telegram (initData). Попробуйте перезапустить Mini App.");
832
  return;
833
  }
834
+ validatedInitData = tg.initData;
835
 
 
836
  fetch('/validate_init_data', {
837
  method: 'POST',
838
  headers: { 'Content-Type': 'application/json' },
 
842
  .then(data => {
843
  if (data.status === 'ok' && data.user) {
844
  currentUser = data.user;
845
+ userInfoHeaderEl.textContent = `User: ${currentUser.first_name || ''} ${currentUser.last_name || ''} (@${currentUser.username || currentUser.id})`;
846
  showAppContent();
847
+ loadFolderContent('root');
848
  } else {
849
  throw new Error(data.message || 'Не удалось верифицировать пользователя.');
850
  }
 
852
  .catch(error => {
853
  console.error("Validation failed:", error);
854
  showError(`Ошибка авторизации: ${error.message}. Попробуйте перезапустить.`);
855
+ validatedInitData = null;
856
  });
857
 
 
858
  uploadForm.addEventListener('submit', handleFileUpload);
859
  createFolderBtn.addEventListener('click', handleCreateFolder);
860
 
861
+ // Optional: Back button handling
862
  // tg.BackButton.onClick(() => {
863
+ // const currentPath = getPathFromBreadcrumbs(); // Need function to get path array
864
+ // if (currentPath.length > 1) {
865
+ // const parentFolder = currentPath[currentPath.length - 2];
866
+ // loadFolderContent(parentFolder.id);
867
+ // } else {
868
+ // // Optionally close app or do nothing at root
869
+ // }
870
  // });
871
+ // Show/hide back button based on current folder depth
 
 
 
 
872
  }
873
 
874
  // --- Start the App ---
 
905
  if tg_user_id not in users:
906
  logging.info(f"New user detected: {tg_user_id}. Initializing filesystem.")
907
  users[tg_user_id] = {
908
+ 'user_info': user_info,
909
  'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
910
  }
911
  initialize_user_filesystem(users[tg_user_id])
912
  try:
913
+ save_data(db_data)
914
  except Exception as e:
915
  logging.error(f"Failed to save data for new user {tg_user_id}: {e}")
 
916
  return jsonify({"status": "error", "message": "Ошибка сохранения данных нового пользователя."}), 500
917
+ elif 'user_info' not in users[tg_user_id] or users[tg_user_id]['user_info'].get('username') != user_info.get('username'): # Update user info if missing or changed
918
  users[tg_user_id]['user_info'] = user_info
919
  try:
920
  save_data(db_data)
921
  except Exception as e:
922
  logging.warning(f"Failed to update user_info for {tg_user_id}: {e}")
923
 
 
924
  if 'filesystem' not in users[tg_user_id]:
925
  initialize_user_filesystem(users[tg_user_id])
926
  try:
 
928
  except Exception as e:
929
  logging.warning(f"Failed to initialize filesystem for existing user {tg_user_id}: {e}")
930
 
 
931
  return jsonify({"status": "ok", "user": user_info})
932
  else:
933
  logging.warning(f"Validation failed for initData: {init_data[:100]}...")
 
958
 
959
  if not current_folder or current_folder.get('type') != 'folder':
960
  logging.warning(f"Folder {folder_id} not found for user {tg_user_id}. Defaulting to root.")
961
+ folder_id = 'root'
962
  current_folder, _ = find_node_by_id(user_data['filesystem'], folder_id)
963
+ if not current_folder:
964
  logging.error(f"CRITICAL: Root folder not found for user {tg_user_id}")
965
  return jsonify({"status": "error", "message": "Критическая ошибка: Корневая папка отсутствует"}), 500
966
 
967
  items_in_folder = current_folder.get('children', [])
968
  breadcrumbs = get_node_path_list(user_data['filesystem'], folder_id)
969
 
 
970
  current_folder_info = {
971
  'id': current_folder.get('id'),
972
  'name': current_folder.get('name', 'Root')
 
1024
  unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
1025
  file_id = uuid.uuid4().hex
1026
 
 
1027
  hf_path = f"cloud_files/{tg_user_id}/{current_folder_id}/{unique_filename}"
1028
  temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
1029
 
 
1046
  uploaded_count += 1
1047
  needs_save = True
1048
  else:
 
1049
  errors.append(f"Ошибка добавления метаданных для {original_filename}.")
1050
  logging.error(f"Failed add_node for {file_id} to {current_folder_id} for {tg_user_id}")
1051
+ try:
1052
  api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
1053
  except Exception as del_err:
1054
  logging.error(f"Failed deleting orphaned HF file {hf_path}: {del_err}")
 
1073
  final_message += " Ошибки: " + "; ".join(errors)
1074
 
1075
  return jsonify({
1076
+ "status": "ok" if not errors else "error",
1077
  "message": final_message
1078
  })
1079
 
 
1095
 
1096
  if not folder_name:
1097
  return jsonify({'status': 'error', 'message': 'Имя папки не может быть пустым!'}), 400
1098
+ # Relaxed validation - check disallowed chars instead? Let's keep simpler for now.
1099
  if not folder_name.replace(' ', '').replace('-', '').replace('_', '').isalnum():
1100
+ if '/' in folder_name or '\\' in folder_name or ':' in folder_name: # Basic check
1101
+ return jsonify({'status': 'error', 'message': 'Имя папки содержит недопустимые символы.'}), 400
1102
+ # Allow broader names if basic check passes
1103
+ logging.warning(f"Folder name '{folder_name}' contains non-alphanumeric/space/dash/underscore characters, but allowing.")
1104
+
1105
 
1106
  db_data = load_data()
1107
  user_data = db_data.get('users', {}).get(tg_user_id)
 
1120
  return jsonify({'status': 'ok', 'message': f'Папка "{folder_name}" создана.'})
1121
  except Exception as e:
1122
  logging.error(f"Create folder save error ({tg_user_id}): {e}")
 
1123
  return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных.'}), 500
1124
  else:
 
1125
  return jsonify({'status': 'error', 'message': 'Не удалось найти родительскую папку или добавить узел.'}), 400
1126
 
1127
 
1128
  @app.route('/download/<file_id>')
1129
  def download_file_route(file_id):
1130
+ """ Serves the file for download. NO AUTH HERE. """
1131
+ db_data = load_data()
 
 
 
 
 
1132
  file_node = None
1133
  owner_user_id = None
1134
 
 
1135
  for user_id, user_data in db_data.get('users', {}).items():
1136
  if 'filesystem' in user_data:
1137
  node, _ = find_node_by_id(user_data['filesystem'], file_id)
 
1157
  if HF_TOKEN_READ:
1158
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1159
 
1160
+ response = requests.get(file_url, headers=headers, stream=True, timeout=30)
1161
+ response.raise_for_status()
1162
+
1163
+ # Use Content-Disposition header for filename
1164
+ encoded_filename = urlencode({'filename': original_filename})[9:] # Crude but often works
1165
+ # A more robust way involves RFC 6266 encoding, but keep it simple
1166
+ disposition = f"attachment; filename=\"{original_filename}\"; filename*=UTF-8''{encoded_filename}"
1167
 
 
1168
  return Response(response.iter_content(chunk_size=8192),
1169
  mimetype=response.headers.get('Content-Type', 'application/octet-stream'),
1170
+ headers={"Content-Disposition": disposition})
1171
 
1172
  except requests.exceptions.RequestException as e:
1173
  logging.error(f"Error downloading file from HF ({hf_path}, owner: {owner_user_id}): {e}")
 
1182
  def delete_file_route(file_id):
1183
  """ Deletes a file for the validated user. """
1184
  data = request.get_json()
1185
+ if not data or 'initData' not in data or 'current_folder_id' not in data:
1186
  return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1187
 
1188
  user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
 
1208
  original_filename = file_node.get('original_filename', 'файл')
1209
  needs_save = False
1210
 
 
1211
  if hf_path:
1212
  try:
1213
  api = HfApi()
 
1218
  logging.info(f"Deleted file {hf_path} from HF Hub for user {tg_user_id}")
1219
  except hf_utils.EntryNotFoundError:
1220
  logging.warning(f"File {hf_path} not found on HF Hub for delete attempt ({tg_user_id}).")
 
1221
  except Exception as e:
1222
  logging.error(f"Error deleting file from HF Hub ({hf_path}, {tg_user_id}): {e}")
1223
+ # Don't stop here, still try to remove from DB
1224
+ # return jsonify({'status': 'error', 'message': f'Ошибка удаления файла с сервера: {e}'}), 500
1225
 
 
1226
  if remove_node(user_data['filesystem'], file_id):
1227
  needs_save = True
1228
  logging.info(f"Removed file node {file_id} from DB for user {tg_user_id}")
1229
  else:
 
1230
  logging.error(f"Failed to remove file node {file_id} from DB structure for {tg_user_id} after HF delete.")
 
1231
 
1232
  if needs_save:
1233
  try:
 
1237
  logging.error(f"Delete file DB save error ({tg_user_id}): {e}")
1238
  return jsonify({'status': 'error', 'message': 'Файл удален с сервера, но ошибка сохранения ��азы данных.'}), 500
1239
  else:
 
1240
  return jsonify({'status': 'error', 'message': 'Файл не найден в базе данных для удаления.'}), 404
1241
 
1242
 
 
1243
  @app.route('/delete_folder/<folder_id>', methods=['POST'])
1244
  def delete_folder_route(folder_id):
1245
  """ Deletes an empty folder for the validated user. """
 
1247
  return jsonify({'status': 'error', 'message': 'Нельзя удалить корневую папку!'}), 400
1248
 
1249
  data = request.get_json()
1250
+ if not data or 'initData' not in data or 'current_folder_id' not in data:
1251
  return jsonify({"status": "error", "message": "Неполный запрос"}), 400
1252
 
1253
  user_info = check_telegram_authorization(data['initData'], BOT_TOKEN)
 
1271
  if folder_node.get('children'):
1272
  return jsonify({'status': 'error', 'message': f'Папку "{folder_name}" можно удалить только если она пуста.'}), 400
1273
 
 
 
 
1274
  if remove_node(user_data['filesystem'], folder_id):
1275
  try:
1276
  save_data(db_data)
 
1285
 
1286
  @app.route('/get_text_content/<file_id>')
1287
  def get_text_content_route(file_id):
1288
+ """ Serves text file content. NO AUTH HERE. """
1289
  db_data = load_data()
1290
  file_node = None
1291
  owner_user_id = None
 
1312
  if HF_TOKEN_READ:
1313
  headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1314
 
1315
+ response = requests.get(file_url, headers=headers, timeout=15)
1316
  response.raise_for_status()
1317
 
 
1318
  max_preview_size = 1 * 1024 * 1024 # 1 MB
1319
  if len(response.content) > max_preview_size:
1320
  return Response("Файл слишком большой для предпросмотра (>1MB).", status=413)
1321
 
 
1322
  text_content = None
1323
+ encodings_to_try = ['utf-8', 'cp1251', 'latin-1']
1324
+ for enc in encodings_to_try:
1325
+ try:
1326
+ text_content = response.content.decode(enc)
1327
+ break
1328
+ except UnicodeDecodeError:
1329
+ continue
1330
 
1331
  if text_content is None:
1332
  return Response("Не удалось определить кодировку файла.", status=500)
1333
 
1334
+ return Response(text_content, mimetype='text/plain; charset=utf-8')
1335
 
1336
  except requests.exceptions.RequestException as e:
1337
  logging.error(f"Error fetching text content from HF ({hf_path}, owner {owner_user_id}): {e}")
 
1344
 
1345
  @app.route('/preview_thumb/<file_id>')
1346
  def preview_thumb_route(file_id):
1347
+ """ Serves image previews. NO AUTH HERE. """
 
 
 
 
 
 
 
1348
  db_data = load_data()
1349
  file_node = None
1350
  owner_user_id = None
 
1361
  hf_path = file_node.get('path')
1362
  if not hf_path: return Response("Путь к файлу не найден", status=500)
1363
 
 
1364
  file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}"
1365
 
1366
  try:
 
1368
  if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
1369
  response = requests.get(file_url, headers=headers, stream=True, timeout=20)
1370
  response.raise_for_status()
1371
+ # Return directly, let browser handle image rendering
1372
  return Response(response.iter_content(chunk_size=8192), mimetype=response.headers.get('Content-Type', 'image/jpeg'))
1373
  except requests.exceptions.RequestException as e:
1374
  logging.error(f"Error fetching preview from HF ({hf_path}, owner: {owner_user_id}): {e}")
1375
+ status_code = e.response.status_code if e.response is not None else 502
1376
+ return Response(f"Ошибка загрузки превью ({status_code})", status=status_code)
1377
  except Exception as e:
1378
  logging.error(f"Unexpected error during preview ({hf_path}, owner: {owner_user_id}): {e}")
1379
  return Response("Внутренняя ошибка сервера при загрузке превью", status=500)
 
1391
  if not HF_TOKEN_READ:
1392
  logging.warning("HF_TOKEN_READ is not set. File downloads/previews might fail for private repos.")
1393
 
 
1394
  logging.info("Performing initial database download...")
1395
  download_db_from_hf()
1396
  logging.info("Initial download attempt complete.")
1397
 
 
 
1398
  app.run(debug=False, host='0.0.0.0', port=7860)