- {# Use JS to trigger download if direct link causes issues in TMA #}
- Скачать
- {% if previewable %}
-
- {% endif %}
-
-
- {% endif %}
-
- {% endfor %}
- {% if not items %}
Эта папка пуста.
{% endif %}
-
-
- {# Очистить сессию #}
- {# Logout doesn't make much sense in TMA context #}
-
-'''
template_context = {
- 'tg_id': tg_id,
- 'display_name': display_name,
+ 'telegram_id': telegram_id,
+ 'user_info': user_info,
'items': items_in_folder,
'current_folder_id': current_folder_id,
'current_folder': current_folder,
'breadcrumbs': breadcrumbs,
'repo_id': REPO_ID,
- 'HF_TOKEN_READ': HF_TOKEN_READ,
+ 'HF_TOKEN_READ': HF_TOKEN_READ, # Note: Passing tokens to JS isn't secure practice
'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
- 'os': os # Keep os if needed by templates, though unlikely now
+ 'os': os # os module likely not needed in template directly
}
- return render_template_string(dashboard_html, **template_context)
-
+ return render_template_string(DASHBOARD_HTML, **template_context)
-# --- File/Folder Operations ---
@app.route('/upload', methods=['POST'])
-def upload_file():
- if 'tg_id' not in session:
- return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
+def upload_files():
+ if 'telegram_id' not in session:
+ # Use 401 Unauthorized or 403 Forbidden
+ return jsonify({"status": "error", "message": "Не авторизован"}), 401
- if not HF_TOKEN_WRITE:
- # Return JSON for JS handler
- return jsonify({'status': 'error', 'message': 'Загрузка невозможна: токен для записи не настроен.'}), 403
+ telegram_id = session['telegram_id']
+ data = load_data()
+ user_data = data['users'].get(telegram_id)
- tg_id = session['tg_id']
- tg_id_str = str(tg_id)
- # Use tg_id for path to avoid issues with changing usernames
- user_identifier_for_path = tg_id_str
+ if not user_data:
+ return jsonify({"status": "error", "message": "Пользователь не найден"}), 404
- data = load_data()
- if tg_id_str not in data.get('users', {}):
- return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404
- user_data = data['users'][tg_id_str]
+ if not HF_TOKEN_WRITE:
+ flash('Загрузка невозможна: токен для записи не настроен.', 'error')
+ # Redirect back to the folder they were in
+ return redirect(url_for('dashboard', folder_id=request.form.get('current_folder_id', 'root')))
files = request.files.getlist('files')
+ target_folder_id = request.form.get('current_folder_id', 'root')
+
if not files or all(not f.filename for f in files):
- return jsonify({'status': 'error', 'message': 'Файлы для загрузки не выбраны.'}), 400
+ flash('Файлы для загрузки не выбраны.', 'error')
+ return redirect(url_for('dashboard', folder_id=target_folder_id))
if len(files) > 20:
- return jsonify({'status': 'error', 'message': 'Максимум 20 файлов за раз!'}), 400
-
- target_folder_id = request.form.get('current_folder_id', 'root')
- target_folder_node, _ = find_node_by_id(user_data.get('filesystem'), target_folder_id)
+ flash('Максимум 20 файлов за раз!', 'error')
+ return redirect(url_for('dashboard', folder_id=target_folder_id))
+ target_folder_node, _ = find_node_by_id(user_data.get('filesystem',{}), target_folder_id)
if not target_folder_node or target_folder_node.get('type') != 'folder':
- return jsonify({'status': 'error', 'message': 'Целевая папка для загрузки не найдена!'}), 404
+ flash('Целевая папка для загрузки не найдена!', 'error')
+ # Redirect to root if target folder is invalid
+ return redirect(url_for('dashboard'))
api = HfApi()
uploaded_count = 0
errors = []
- save_needed = False
for file in files:
if file and file.filename:
original_filename = secure_filename(file.filename)
name_part, ext_part = os.path.splitext(original_filename)
unique_suffix = uuid.uuid4().hex[:8]
- # Keep filename relatively simple for HF path
- unique_filename = f"{secure_filename(name_part)}_{unique_suffix}{ext_part}"
+ unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
file_id = uuid.uuid4().hex
- # Construct path using tg_id and target folder id
- hf_path = f"cloud_files/{user_identifier_for_path}/{target_folder_id}/{unique_filename}"
+ # Use telegram_id (as string) in path
+ hf_path = f"cloud_files/{str(telegram_id)}/{target_folder_id}/{unique_filename}"
temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
try:
@@ -1132,14 +1117,14 @@ def upload_file():
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
- commit_message=f"User {tg_id} uploaded {original_filename} to folder {target_folder_id}"
+ commit_message=f"User {telegram_id} uploaded {original_filename} to folder {target_folder_id}"
)
file_info = {
'type': 'file',
'id': file_id,
'original_filename': original_filename,
- 'unique_filename': unique_filename, # Store for potential reference
+ 'unique_filename': unique_filename,
'path': hf_path,
'file_type': get_file_type(original_filename),
'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
@@ -1147,347 +1132,372 @@ def upload_file():
if add_node(user_data['filesystem'], target_folder_id, file_info):
uploaded_count += 1
- save_needed = True
else:
- error_msg = f"Ошибка добавления метаданных для {original_filename} в папку {target_folder_id}."
- errors.append(error_msg)
- logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {tg_id}")
- # Attempt to clean up orphaned file on HF
+ errors.append(f"Ошибка добавления метаданных для {original_filename}.")
+ logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {telegram_id}")
+ # Attempt to clean up orphaned HF file
try:
api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
- logging.info(f"Cleaned up orphaned HF file: {hf_path}")
except Exception as del_err:
logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
except Exception as e:
- logging.error(f"Error uploading file {original_filename} for {tg_id}: {e}", exc_info=True)
+ logging.error(f"Error uploading file {original_filename} for {telegram_id}: {e}")
errors.append(f"Ошибка загрузки файла {original_filename}: {e}")
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
- if save_needed:
+ if uploaded_count > 0:
try:
save_data(data)
- logging.info(f"{uploaded_count} files uploaded successfully for user {tg_id}.")
+ flash(f'{uploaded_count} файл(ов) успешно загружено!')
except Exception as e:
- errors.append('Файлы загружены, но произошла ошибка сохранения метаданных.')
- logging.error(f"Error saving data after upload for {tg_id}: {e}", exc_info=True)
+ flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error')
+ logging.error(f"Error saving data after upload for {telegram_id}: {e}")
- final_message = ""
- if uploaded_count > 0:
- final_message += f'{uploaded_count} файл(ов) успешно загружено! '
if errors:
- final_message += "Ошибки: " + "; ".join(errors)
-
- status_code = 200 if uploaded_count > 0 and not errors else (500 if errors else 200)
- status_str = "success" if status_code == 200 else "error"
+ for error_msg in errors:
+ flash(error_msg, 'error')
- return jsonify({'status': status_str, 'message': final_message.strip()}), status_code
+ # Redirect back to the folder where files were uploaded
+ return redirect(url_for('dashboard', folder_id=target_folder_id))
@app.route('/create_folder', methods=['POST'])
def create_folder():
- if 'tg_id' not in session:
- return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
+ if 'telegram_id' not in session:
+ flash('Пожалуйста, авторизуйтесь.', 'error')
+ return redirect(url_for('index'))
- tg_id = session['tg_id']
- tg_id_str = str(tg_id)
+ telegram_id = session['telegram_id']
data = load_data()
- user_data = data['users'].get(tg_id_str)
+ user_data = data['users'].get(telegram_id)
if not user_data:
- return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404
+ flash('Пользователь не найден!', 'error')
+ session.clear()
+ return redirect(url_for('index'))
parent_folder_id = request.form.get('parent_folder_id', 'root')
- # Sanitize folder name - allow letters, numbers, spaces, underscore
- folder_name_raw = request.form.get('folder_name', '').strip()
- folder_name = "".join(c for c in folder_name_raw if c.isalnum() or c in (' ', '_')).strip()
-
+ folder_name = request.form.get('folder_name', '').strip()
if not folder_name:
- return jsonify({'status': 'error', 'message': 'Имя папки не может быть пустым или содержать недопустимые символы.'}), 400
- if len(folder_name) > 50: # Add length limit
- return jsonify({'status': 'error', 'message': 'Имя папки слишком длинное (макс 50 симв).'}), 400
+ flash('Имя папки не может быть пустым!', 'error')
+ return redirect(url_for('dashboard', folder_id=parent_folder_id))
+ # Allow more characters, restrict later if needed, basic validation
+ import re
+ if not re.match(r"^[a-zA-Z0-9 _.-]+$", folder_name):
+ flash('Имя папки содержит недопустимые символы.', 'error')
+ return redirect(url_for('dashboard', folder_id=parent_folder_id))
folder_id = uuid.uuid4().hex
folder_data = {
'type': 'folder',
'id': folder_id,
'name': folder_name,
- 'children': [] # Always initialize children for new folders
+ 'children': []
}
- # Ensure filesystem structure is valid before adding
- if 'filesystem' not in user_data or not isinstance(user_data.get('filesystem'), dict):
- initialize_user_filesystem(user_data)
-
-
- if add_node(user_data.get('filesystem'), parent_folder_id, folder_data):
+ if add_node(user_data.get('filesystem',{}), parent_folder_id, folder_data):
try:
save_data(data)
- return jsonify({'status': 'success', 'message': f'Папка "{folder_name}" успешно создана.'})
+ flash(f'Папка "{folder_name}" успешно создана.')
except Exception as e:
- logging.error(f"Create folder save error for user {tg_id}: {e}", exc_info=True)
- # Attempt to remove the added node if save fails? Maybe too complex.
- return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных при создании папки.'}), 500
+ flash('Ошибка сохранения данных при создании папки.', 'error')
+ logging.error(f"Create folder save error for {telegram_id}: {e}")
else:
- return jsonify({'status': 'error', 'message': 'Не удалось найти родительскую папку или добавить узел.'}), 404
+ flash('Не удалось найти родительскую папку.', 'error')
+
+ return redirect(url_for('dashboard', folder_id=parent_folder_id))
+
@app.route('/download/')
def download_file(file_id):
- tg_id_str = None
- is_admin_req = is_admin() # Check admin status early
+ current_user_id = session.get('telegram_id')
+ admin_access = is_admin() # Check if current session holder is admin
- if 'tg_id' in session:
- tg_id_str = str(session['tg_id'])
- elif not is_admin_req:
- flash('Пожалуйста, авторизуйтесь.') # Flash might not be visible
- return redirect(url_for('index')) # Redirect to main TMA page
+ if not current_user_id and not admin_access:
+ flash('Пожалуйста, авторизуйтесь для скачивания.')
+ return redirect(url_for('index')) # Redirect to auth page
data = load_data()
file_node = None
- user_tg_id_of_file = None
-
- # Try finding file for logged-in user first
- if tg_id_str and tg_id_str in data.get('users', {}):
- user_data = data['users'][tg_id_str]
- file_node, _ = find_node_by_id(user_data.get('filesystem'), file_id)
- if file_node:
- user_tg_id_of_file = tg_id_str
-
- # If not found for current user, admin can search all users
- if not file_node and is_admin_req:
- logging.info(f"Admin (TG ID: {session.get('tg_id')}) searching for file ID {file_id} across all users.")
+ owner_id = None
+
+ # 1. Try finding the file for the current user (if logged in)
+ if current_user_id:
+ user_data = data['users'].get(current_user_id)
+ if user_data and 'filesystem' in user_data:
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+ if file_node:
+ owner_id = current_user_id
+
+ # 2. If not found for current user OR if current user is admin and wants to search all
+ if not file_node and admin_access:
+ logging.info(f"Admin {current_user_id} searching for file ID {file_id} across all users.")
for user_id, udata in data.get('users', {}).items():
- if isinstance(udata, dict):
- node, _ = find_node_by_id(udata.get('filesystem'), file_id)
+ if 'filesystem' in udata:
+ node, _ = find_node_by_id(udata['filesystem'], file_id)
if node and node.get('type') == 'file':
file_node = node
- user_tg_id_of_file = user_id
- logging.info(f"Admin found file ID {file_id} belonging to user TG ID {user_tg_id_of_file}")
+ owner_id = user_id # Found the owner
+ logging.info(f"Admin {current_user_id} found file ID {file_id} belonging to user {owner_id}")
break
if not file_node or file_node.get('type') != 'file':
- # Flash might not be seen if redirected immediately
- # flash('Файл не найден!', 'error')
- logging.warning(f"File not found (ID: {file_id}) for user {tg_id_str} or admin search.")
- # Redirect back to wherever they came from, or root dashboard
- # Using referrer is unreliable; maybe redirect to root always on error?
- # return redirect(request.referrer or url_for('index'))
- return Response("Файл не найден", status=404)
+ flash('Файл не найден!', 'error')
+ # Redirect back to dashboard if logged in, or index if not
+ referer = request.referrer
+ if current_user_id and referer and ('dashboard' in referer or 'admhosto' in referer):
+ return redirect(referer)
+ elif current_user_id:
+ return redirect(url_for('dashboard'))
+ else: # Admin accessing directly without session? Should not happen ideally.
+ return redirect(url_for('admin_panel' if admin_access else 'index'))
hf_path = file_node.get('path')
original_filename = file_node.get('original_filename', 'downloaded_file')
if not hf_path:
- logging.error(f"File ID {file_id} (User: {user_tg_id_of_file}) has missing path in metadata.")
- return Response("Ошибка: Путь к файлу не найден в метаданных.", status=500)
+ flash('Ошибка: Путь к файлу не найден в метаданных.', 'error')
+ referer = request.referrer
+ if current_user_id and referer and ('dashboard' in referer or 'admhosto' in referer):
+ return redirect(referer)
+ elif current_user_id:
+ return redirect(url_for('dashboard'))
+ else:
+ return redirect(url_for('admin_panel' if admin_access else 'index'))
+
- # Generate download URL (direct access)
file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
- logging.info(f"Attempting download for file ID {file_id}, Path: {hf_path}, URL: {file_url}")
try:
headers = {}
- # Use read token if available (necessary for private repos)
if HF_TOKEN_READ:
headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
- # Stream the download
- response = requests.get(file_url, headers=headers, stream=True, timeout=60) # Add timeout
- response.raise_for_status() # Check for HTTP errors (4xx, 5xx)
+ response = requests.get(file_url, headers=headers, stream=True, timeout=60) # Added timeout
+ response.raise_for_status()
+
+ # Stream download for potentially large files
+ # file_content = BytesIO(response.content) # This loads whole file in memory
+ # return send_file(
+ # file_content,
+ # as_attachment=True,
+ # download_name=original_filename,
+ # mimetype='application/octet-stream'
+ # )
- # Stream the content back to the user
+ # Use Response streaming
return Response(
- stream_with_context(response.iter_content(chunk_size=8192)),
- headers={
- 'Content-Disposition': f'attachment; filename="{original_filename}"',
- 'Content-Type': response.headers.get('Content-Type', 'application/octet-stream'),
- 'Content-Length': response.headers.get('Content-Length')
- }
+ response.iter_content(chunk_size=8192),
+ mimetype='application/octet-stream',
+ headers={'Content-Disposition': f'attachment; filename="{original_filename}"'}
)
except requests.exceptions.RequestException as e:
- logging.error(f"Error downloading file from HF ({hf_path}): {e}")
- status_code = e.response.status_code if e.response is not None else 502
- return Response(f'Ошибка скачивания файла {original_filename} с сервера ({status_code}).', status=status_code)
+ logging.error(f"Error downloading file from HF ({hf_path}) for user {current_user_id or 'Admin'} (owner: {owner_id}): {e}")
+ flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error')
+ referer = request.referrer
+ if current_user_id and referer and ('dashboard' in referer or 'admhosto' in referer):
+ return redirect(referer)
+ elif current_user_id:
+ return redirect(url_for('dashboard'))
+ else:
+ return redirect(url_for('admin_panel' if admin_access else 'index'))
except Exception as e:
- logging.error(f"Unexpected error during download ({hf_path}): {e}", exc_info=True)
- return Response('Произошла непредвиденная ошибка при скачивании файла.', status=500)
+ logging.error(f"Unexpected error during download ({hf_path}) for user {current_user_id or 'Admin'} (owner: {owner_id}): {e}")
+ flash('Произошла непредвиденная ошибка при скачивании файла.', 'error')
+ referer = request.referrer
+ if current_user_id and referer and ('dashboard' in referer or 'admhosto' in referer):
+ return redirect(referer)
+ elif current_user_id:
+ return redirect(url_for('dashboard'))
+ else:
+ return redirect(url_for('admin_panel' if admin_access else 'index'))
@app.route('/delete_file/', methods=['POST'])
def delete_file(file_id):
- if 'tg_id' not in session:
- return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
+ if 'telegram_id' not in session:
+ flash('Пожалуйста, авторизуйтесь.', 'error')
+ return redirect(url_for('index'))
- tg_id = session['tg_id']
- tg_id_str = str(tg_id)
+ telegram_id = session['telegram_id']
data = load_data()
- user_data = data['users'].get(tg_id_str)
+ user_data = data['users'].get(telegram_id)
if not user_data:
- return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404
+ flash('Пользователь не найден!', 'error')
+ session.clear()
+ return redirect(url_for('index'))
+
+ file_node, parent_node = find_node_by_id(user_data.get('filesystem',{}), file_id)
+ # Get the folder ID we should redirect back to
+ current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root')
- file_node, parent_node = find_node_by_id(user_data.get('filesystem'), file_id)
- current_view_folder_id = request.form.get('current_view_folder_id', 'root') # Keep track for reload
if not file_node or file_node.get('type') != 'file' or not parent_node:
- return jsonify({'status': 'error', 'message': 'Файл не найден или не может быть удален.'}), 404
+ flash('Файл не найден или не может быть удален.', 'error')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
hf_path = file_node.get('path')
original_filename = file_node.get('original_filename', 'файл')
- save_needed = False
- error_occurred = False
- messages = []
- # Case 1: Path is missing in metadata, just remove from DB
+ # Handle deletion even if HF path is missing (clean up DB)
if not hf_path:
- logging.warning(f"File ID {file_id} (User: {tg_id}) has missing path. Deleting only metadata.")
+ logging.warning(f'HF path missing for file {file_id} ({original_filename}) user {telegram_id}. Removing from DB only.')
+ flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'warning')
if remove_node(user_data['filesystem'], file_id):
- save_needed = True
- messages.append(f'Метаданные файла {original_filename} удалены (путь отсутствовал).')
- else:
- messages.append('Не удалось удалить метаданные файла (путь отсутствовал).')
- error_occurred = True
-
- # Case 2: Path exists, attempt HF deletion first
- else:
- if not HF_TOKEN_WRITE:
- return jsonify({'status': 'error', 'message': 'Удаление невозможно: токен для записи не настроен.'}), 403
+ try:
+ save_data(data)
+ flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).')
+ except Exception as e:
+ flash('Ошибка сохранения данных после удаления метаданных.', 'error')
+ logging.error(f"Delete file metadata save error (no path) for {telegram_id}: {e}")
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
- try:
- api = HfApi()
- api.delete_file(
- path_in_repo=hf_path,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"User {tg_id} deleted file {original_filename} (ID: {file_id})"
- )
- logging.info(f"Deleted file {hf_path} from HF Hub for user {tg_id}")
- messages.append(f'Файл {original_filename} удален с сервера.')
-
- # Now remove from DB
- if remove_node(user_data['filesystem'], file_id):
- save_needed = True
- messages.append('Метаданные удалены из базы.')
- else:
- messages.append('Файл удален с сервера, но не найден в базе для удаления метаданных.')
- error_occurred = True # Metadata mismatch
-
- except hf_utils.EntryNotFoundError:
- logging.warning(f"File {hf_path} not found on HF Hub during delete for user {tg_id}. Removing from DB.")
- messages.append(f'Файл {original_filename} не найден на сервере.')
- if remove_node(user_data['filesystem'], file_id):
- save_needed = True
- messages.append('Удален из базы.')
- else:
- messages.append('Не найден ни на сервере, ни в базе данных.')
- error_occurred = True
- except Exception as e:
- logging.error(f"Error deleting file {hf_path} for {tg_id}: {e}", exc_info=True)
- messages.append(f'Ошибка удаления файла {original_filename} с сервера: {e}')
- error_occurred = True # Don't remove metadata if server deletion failed
+ # Check for write token before attempting HF deletion
+ if not HF_TOKEN_WRITE:
+ flash('Удаление невозможно: токен для записи не настроен.', 'error')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
+ try:
+ api = HfApi()
+ api.delete_file(
+ path_in_repo=hf_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"User {telegram_id} deleted file {original_filename} (ID: {file_id})"
+ )
+ logging.info(f"Deleted file {hf_path} from HF Hub for user {telegram_id}")
- if save_needed:
- try:
- save_data(data)
- messages.append('База данных обновлена.')
- except Exception as e:
- logging.error(f"Delete file DB update error for {tg_id}: {e}", exc_info=True)
- messages.append('Ошибка сохранения базы данных после удаления.')
- error_occurred = True
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} успешно удален!')
+ except Exception as e:
+ # Data saving failed after successful HF deletion - critical inconsistency
+ flash('Файл удален с сервера, но произошла КРИТИЧЕСКАЯ ошибка обновления базы данных. Свяжитесь с администратором.', 'error')
+ logging.error(f"CRITICAL: Delete file DB update error after HF deletion for {telegram_id}, file {file_id}: {e}")
+ else:
+ # Should not happen if file_node was found earlier
+ flash('Файл удален с сервера, но не найден в локальной базе данных для удаления метаданных.', 'error')
+ logging.error(f"Inconsistency: File {file_id} deleted from HF but not found in DB for user {telegram_id}")
- final_status = 'error' if error_occurred else 'success'
- final_message = " ".join(messages)
- return jsonify({'status': final_status, 'message': final_message}), 200 if final_status == 'success' else 500
+ except hf_utils.EntryNotFoundError:
+ logging.warning(f"File {hf_path} not found on HF Hub during delete attempt for user {telegram_id}. Removing from DB.")
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
+ except Exception as e:
+ flash('Ошибка сохранения данных после удаления метаданных (файл не найден на сервере).', 'error')
+ logging.error(f"Delete file metadata save error (HF not found) for {telegram_id}: {e}")
+ else:
+ flash('Файл не найден ни на сервере, ни в базе данных.', 'error')
+ except Exception as e:
+ logging.error(f"Error deleting file {hf_path} for {telegram_id}: {e}")
+ flash(f'Ошибка удаления файла {original_filename}: {e}', 'error')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
@app.route('/delete_folder/', methods=['POST'])
def delete_folder(folder_id):
- if 'tg_id' not in session:
- return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
+ if 'telegram_id' not in session:
+ flash('Пожалуйста, авторизуйтесь.', 'error')
+ return redirect(url_for('index'))
if folder_id == 'root':
- return jsonify({'status': 'error', 'message': 'Нельзя удалить корневую папку!'}), 400
+ flash('Нельзя удалить корневую папку!', 'error')
+ # Determine current view folder to redirect back correctly
+ current_view_folder_id = request.form.get('current_view_folder_id', 'root')
+ return redirect(url_for('dashboard', folder_id=current_view_folder_id))
+
- tg_id = session['tg_id']
- tg_id_str = str(tg_id)
+ telegram_id = session['telegram_id']
data = load_data()
- user_data = data['users'].get(tg_id_str)
+ user_data = data['users'].get(telegram_id)
if not user_data:
- return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404
+ flash('Пользователь не найден!', 'error')
+ session.clear()
+ return redirect(url_for('index'))
- folder_node, parent_node = find_node_by_id(user_data.get('filesystem'), folder_id)
- current_view_folder_id = request.form.get('current_view_folder_id', 'root')
+ folder_node, parent_node = find_node_by_id(user_data.get('filesystem',{}), folder_id)
+ # Get the folder ID we should redirect back to (parent of the deleted folder)
+ redirect_to_folder_id = parent_node.get('id', 'root') if parent_node else 'root'
if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
- return jsonify({'status': 'error', 'message': 'Папка не найдена или не может быть удалена.'}), 404
+ flash('Папка не найдена или не может быть удалена.', 'error')
+ return redirect(url_for('dashboard', folder_id=redirect_to_folder_id)) # Redirect to parent
+
folder_name = folder_node.get('name', 'папка')
# Check if folder is empty
if folder_node.get('children'):
- return jsonify({'status': 'error', 'message': f'Папку "{folder_name}" можно удалить только если она пуста.'}), 400
+ flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error')
+ # Redirect back to the folder containing the one attempted to delete
+ return redirect(url_for('dashboard', folder_id=parent_node.get('id', 'root')))
- # No HF deletion needed for empty folders, just remove metadata
+ # Remove the empty folder node from the parent's children list
if remove_node(user_data['filesystem'], folder_id):
try:
save_data(data)
- # Determine redirect target (parent folder)
- redirect_to_folder_id = parent_node.get('id', 'root')
- return jsonify({'status': 'success', 'message': f'Пустая папка "{folder_name}" успешно удалена.', 'redirect_folder': redirect_to_folder_id })
+ flash(f'Пустая папка "{folder_name}" успешно удалена.')
except Exception as e:
- logging.error(f"Delete empty folder save error for {tg_id}: {e}", exc_info=True)
- return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных после удаления папки.'}), 500
+ flash('Ошибка сохранения данных после удаления папки.', 'error')
+ logging.error(f"Delete empty folder save error for {telegram_id}: {e}")
+ # Attempt to revert deletion in memory? Maybe too complex, log error is primary
else:
- return jsonify({'status': 'error', 'message': 'Не удалось удалить папку из базы данных.'}), 500
+ # This case should ideally not happen if folder_node was found
+ flash('Не удалось удалить папку из структуры данных.', 'error')
+ logging.error(f"Failed to remove folder node {folder_id} for user {telegram_id} despite finding it.")
+
+
+ # Redirect to the parent folder after deletion
+ return redirect(url_for('dashboard', folder_id=redirect_to_folder_id))
@app.route('/get_text_content/')
def get_text_content(file_id):
- tg_id_str = None
- is_admin_req = is_admin()
+ current_user_id = session.get('telegram_id')
+ admin_access = is_admin()
- if 'tg_id' in session:
- tg_id_str = str(session['tg_id'])
- elif not is_admin_req:
- return Response("Не авторизован", status=401)
+ if not current_user_id and not admin_access:
+ return Response("Не авторизован", status=401, mimetype='text/plain')
data = load_data()
file_node = None
- user_tg_id_of_file = None
-
- # Try finding file for logged-in user first
- if tg_id_str and tg_id_str in data.get('users', {}):
- user_data = data['users'][tg_id_str]
- file_node, _ = find_node_by_id(user_data.get('filesystem'), file_id)
- if file_node and file_node.get('type') == 'file' and file_node.get('file_type') == 'text':
- user_tg_id_of_file = tg_id_str
-
- # If not found for current user, admin can search all users
- if not file_node and is_admin_req:
- logging.info(f"Admin (TG ID: {session.get('tg_id')}) searching for text file ID {file_id} across all users.")
+ owner_id = None
+
+ # Find file (similar logic to download)
+ if current_user_id:
+ user_data = data['users'].get(current_user_id)
+ if user_data and 'filesystem' in user_data:
+ file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+ if file_node: owner_id = current_user_id
+
+ if not file_node and admin_access:
for user_id, udata in data.get('users', {}).items():
- if isinstance(udata, dict):
- node, _ = find_node_by_id(udata.get('filesystem'), file_id)
- if node and node.get('type') == 'file' and node.get('file_type') == 'text':
- file_node = node
- user_tg_id_of_file = user_id
- logging.info(f"Admin found text file ID {file_id} belonging to user TG ID {user_tg_id_of_file}")
- break
+ if 'filesystem' in udata:
+ node, _ = find_node_by_id(udata['filesystem'], file_id)
+ if node and node.get('type') == 'file' and node.get('file_type') == 'text':
+ file_node = node
+ owner_id = user_id
+ break
- if not file_node: # Already checked for type=file and file_type=text in the loops
- return Response("Текстовый файл не найден", status=404)
+ if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text':
+ return Response("Текстовый файл не найден", status=404, mimetype='text/plain')
hf_path = file_node.get('path')
if not hf_path:
- logging.error(f"Text file ID {file_id} (User: {user_tg_id_of_file}) has missing path.")
- return Response("Ошибка: путь к файлу отсутствует", status=500)
+ logging.error(f"Path missing for text file {file_id} (owner {owner_id})")
+ return Response("Ошибка: путь к файлу отсутствует", status=500, mimetype='text/plain')
+ # Construct URL carefully
file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
try:
@@ -1495,495 +1505,358 @@ def get_text_content(file_id):
if HF_TOKEN_READ:
headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
- response = requests.get(file_url, headers=headers, timeout=15) # Timeout for text files
- response.raise_for_status()
+ # Use a timeout for the request
+ response = requests.get(file_url, headers=headers, timeout=15)
+ response.raise_for_status() # Check for HTTP errors
+
+ # Limit preview size
+ MAX_PREVIEW_SIZE = 1 * 1024 * 1024 # 1 MB
+ if 'content-length' in response.headers:
+ try:
+ if int(response.headers['content-length']) > MAX_PREVIEW_SIZE:
+ return Response("Файл слишком большой для предпросмотра (> 1MB).", status=413, mimetype='text/plain')
+ except ValueError:
+ pass # Ignore if content-length is not a valid number
+
+ file_content = response.content
+ if len(file_content) > MAX_PREVIEW_SIZE:
+ # Check size again in case Content-Length was missing/wrong
+ return Response("Файл слишком большой для предпросмотра (> 1MB).", status=413, mimetype='text/plain')
- # Limit preview size (e.g., 1MB)
- max_preview_size = 1 * 1024 * 1024
- if len(response.content) > max_preview_size:
- # Provide truncated content instead of error?
- # text_content = response.content[:max_preview_size].decode('utf-8', errors='ignore') + "\n\n[Файл слишком большой, показана только часть]"
- return Response("Файл слишком большой для предпросмотра (>1MB).", status=413)
# Try decoding with UTF-8 first, then fallback
try:
- text_content = response.content.decode('utf-8')
+ text_content = file_content.decode('utf-8')
except UnicodeDecodeError:
try:
- # Common fallback for Windows-created files
- text_content = response.content.decode('cp1251')
- except Exception:
- # Last resort: latin-1 or ignore errors
- text_content = response.content.decode('latin-1', errors='ignore')
- logging.warning(f"Could not determine encoding for {hf_path}. Used latin-1 with errors ignored.")
-
+ # Try common fallback encodings
+ text_content = file_content.decode('cp1251') # Windows Cyrillic
+ except UnicodeDecodeError:
+ try:
+ text_content = file_content.decode('latin-1') # ISO-8859-1
+ except Exception:
+ logging.warning(f"Could not decode text file {file_id} (owner {owner_id}, path {hf_path}) with known encodings.")
+ return Response("Не удалось определить кодировку файла для предпросмотра.", status=500, mimetype='text/plain')
- return Response(text_content, mimetype='text/plain; charset=utf-8') # Specify UTF-8
+ # Return plain text content
+ return Response(text_content, mimetype='text/plain; charset=utf-8') # Specify charset
+ except requests.exceptions.Timeout:
+ logging.error(f"Timeout fetching text content from HF ({hf_path}) for file {file_id} (owner {owner_id})")
+ return Response("Ошибка загрузки содержимого: время ожидания истекло.", status=504, mimetype='text/plain')
except requests.exceptions.RequestException as e:
- logging.error(f"Error fetching text content from HF ({hf_path}): {e}")
+ logging.error(f"Error fetching text content from HF ({hf_path}) for file {file_id} (owner {owner_id}): {e}")
+ # Provide specific error if possible, e.g., 404
status_code = e.response.status_code if e.response is not None else 502
- return Response(f"Ошибка загрузки содержимого: {status_code}", status=status_code)
+ return Response(f"Ошибка загрузки содержимого: {e}", status=status_code, mimetype='text/plain')
except Exception as e:
- logging.error(f"Unexpected error fetching text content ({hf_path}): {e}", exc_info=True)
- return Response("Внутренняя ошибка сервера", status=500)
-
+ logging.error(f"Unexpected error fetching text content ({hf_path}) for file {file_id} (owner {owner_id}): {e}")
+ return Response("Внутренняя ошибка сервера при получении содержимого файла.", status=500, mimetype='text/plain')
-@app.route('/logout') # Kept for potential session clearing during testing
-def logout():
- session.clear()
- # In TMA context, redirecting to login doesn't make sense.
- # Maybe redirect to the main page which forces re-auth?
- return redirect(url_for('index'))
-
-# --- Admin Panel (Separate Access - Not directly part of TMA flow) ---
-
-def is_admin():
- # Check if the logged-in Telegram user's ID matches the ADMIN_TELEGRAM_ID
- return 'tg_id' in session and session['tg_id'] == ADMIN_TELEGRAM_ID
+# --- Admin Routes ---
@app.route('/admhosto')
def admin_panel():
- # Admin must access this URL in a browser AFTER authenticating via the TMA as the admin user
if not is_admin():
- # flash('Доступ запрещен (Admin).', 'error') # Flash not useful here
- # Redirect to main page or show an error
- return Response("Доступ запрещен. Только для администратора.", status=403)
-
+ # Maybe redirect to index or show a specific error page
+ flash('Доступ запрещен (Только для администраторов).', 'error')
+ return redirect(url_for('index'))
data = load_data()
- users = data.get('users', {})
+ users = data.get('users', {}) # Users keyed by integer telegram_id
user_details = []
- total_files_all_users = 0
- for tg_id_str, udata in users.items():
- if not isinstance(udata, dict): continue # Skip malformed data
-
+ for tid, udata in users.items():
+ if not isinstance(udata, dict): continue # Skip malformed user data
file_count = 0
- # Simple recursive counter (can be slow for very deep structures)
- def count_files_recursive(folder):
- count = 0
- if not isinstance(folder, dict) or not isinstance(folder.get('children'), list):
- return 0
- for item in folder.get('children', []):
- if isinstance(item, dict):
+ fs = udata.get('filesystem', {})
+ if fs and 'children' in fs:
+ q = [fs.get('children', [])]
+ while q:
+ current_level = q.pop(0)
+ for item in current_level:
+ if not isinstance(item, dict): continue # Skip invalid children
if item.get('type') == 'file':
- count += 1
- elif item.get('type') == 'folder':
- count += count_files_recursive(item)
- return count
+ file_count += 1
+ elif item.get('type') == 'folder' and 'children' in item:
+ q.append(item.get('children', []))
- file_count = count_files_recursive(udata.get('filesystem', {}))
- total_files_all_users += file_count
+ user_info = udata.get('user_info', {})
+ username = user_info.get('username', f"user_{tid}") # Fallback username
+ full_name = f"{user_info.get('first_name','')} {user_info.get('last_name','')}".strip()
+ display_name = full_name if full_name else username
user_details.append({
- 'tg_id': tg_id_str,
- 'display_name': udata.get('tg_first_name', f"ID: {tg_id_str}") + (f" (@{udata['tg_username']})" if udata.get('tg_username') else ""),
+ 'telegram_id': tid,
+ 'username': display_name, # Use combined name or username
'created_at': udata.get('created_at', 'N/A'),
'file_count': file_count
})
- user_details.sort(key=lambda x: x['display_name'].lower())
- admin_html = '''
-
-Админ-панель Cloud
-
-
Всего пользователей: {{ user_details|length }} | Всего файлов: {{ total_files_all_users }}
-{# Add flash message display area if needed for admin actions #}
-{% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
{{ message }}
- {% endfor %}
- {% endif %}
-{% endwith %}
+ # Sort users, e.g., by creation date or ID
+ user_details.sort(key=lambda x: x.get('telegram_id'))
+
+ return render_template_string(ADMIN_USERS_HTML, user_details=user_details)
-
{# Actions div #}
- Скачать
- {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %}
- {% if previewable %}
-
- {% endif %}
-
-
-
-{% else %}
У пользователя нет файлов.
{% endfor %}
-
-
-{# Modal for admin preview #}
-
-
- ×
-
-
-
-
-
-
- '''.format(style=BASE_STYLE) # Embed style
- template_context = {
- 'user_tg_id': user_tg_id,
- 'user_display_name': user_display_name,
- 'files': all_files,
- 'repo_id': REPO_ID,
- 'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}"
+ user_info_for_template = {
+ 'telegram_id': telegram_id,
+ 'username': display_name
}
- return render_template_string(files_html, **template_context)
-
-@app.route('/admhosto/delete_user/', methods=['POST'])
-def admin_delete_user(user_tg_id):
+ all_files = []
+ fs = user_data.get('filesystem', {})
+
+ def collect_files(folder_node, current_path_id='root'):
+ if not folder_node or not isinstance(folder_node, dict) or folder_node.get('type') != 'folder':
+ return
+
+ # Calculate path string for the parent folder
+ parent_path_str = get_node_path_string(fs, current_path_id) if current_path_id != 'root' else "Root"
+
+ for item in folder_node.get('children', []):
+ if not isinstance(item, dict): continue # Skip malformed
+ if item.get('type') == 'file':
+ # Add the calculated parent path string to the file item
+ item['parent_path_str'] = parent_path_str
+ all_files.append(item)
+ elif item.get('type') == 'folder':
+ # Recursive call for subfolder, passing its ID
+ collect_files(item, item.get('id'))
+
+ # Start collection from the root filesystem node
+ collect_files(fs, 'root')
+
+ all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
+
+ return render_template_string(
+ ADMIN_USER_FILES_HTML,
+ user_info=user_info_for_template,
+ files=all_files,
+ repo_id=REPO_ID,
+ hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
+ url_for=url_for # Pass url_for to template if needed inside complex logic not handled by Jinja directly
+ )
+
+
+@app.route('/admhosto/delete_user/', methods=['POST'])
+def admin_delete_user(telegram_id):
if not is_admin():
flash('Доступ запрещен.', 'error')
- return redirect(url_for('admin_panel')) # Redirect to admin panel
+ return redirect(url_for('index')) # Redirect non-admins away
+
if not HF_TOKEN_WRITE:
- flash('Удаление невозможно: токен для записи не настроен.', 'error')
+ flash('Удаление невозможно: токен Hugging Face для записи не настроен.', 'error')
return redirect(url_for('admin_panel'))
- user_tg_id_str = str(user_tg_id)
data = load_data()
- if user_tg_id_str not in data.get('users', {}):
- flash('Пользователь не найден!', 'error')
+ if telegram_id not in data.get('users', {}):
+ flash(f'Пользователь с ID {telegram_id} не найден!', 'error')
return redirect(url_for('admin_panel'))
- user_data = data['users'][user_tg_id_str]
- user_display_name = user_data.get('tg_first_name', f"ID: {user_tg_id_str}")
- logging.warning(f"ADMIN ACTION (User: {session.get('tg_id')}): Attempting to delete user {user_display_name} (ID: {user_tg_id_str}) and all their data.")
+ user_data = data['users'][telegram_id]
+ user_info = user_data.get('user_info', {})
+ username_display = f"{user_info.get('first_name','')} {user_info.get('last_name','')}".strip() or f"user_{telegram_id}"
+
+ logging.warning(f"ADMIN ACTION (User: {session.get('telegram_id')}): Attempting to delete user {username_display} (ID: {telegram_id}) and all their data.")
- # Step 1: Attempt to delete user's folder on Hugging Face
- hf_delete_success = False
+ # --- Step 1: Delete files from Hugging Face ---
+ hf_deletion_successful = False
try:
api = HfApi()
- # Use tg_id in the path for consistency
- user_folder_path_on_hf = f"cloud_files/{user_tg_id_str}"
-
- logging.info(f"ADMIN ACTION: Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {user_tg_id_str}")
- # Listing files might be too slow/complex. Attempt direct folder delete.
- # Note: delete_folder might fail if the folder isn't completely empty due to eventual consistency or large files.
- # Consider deleting individual files first if folder deletion is unreliable.
- # For simplicity, we try deleting the folder directly.
- try:
- api.delete_folder(
- folder_path=user_folder_path_on_hf,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"ADMIN ACTION: Deleted all files/folders for user {user_tg_id_str}"
- )
- logging.info(f"ADMIN ACTION: Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.")
- hf_delete_success = True # Assume success if no error raised (it might be async)
- except hf_utils.HfHubHTTPError as e:
- if e.response.status_code == 404:
- logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {user_tg_id_str}. Assuming already deleted or never existed.")
- hf_delete_success = True # Treat as success if not found
- else:
- raise # Re-raise other HTTP errors
+ # Use telegram_id (as string) in the path
+ user_folder_path_on_hf = f"cloud_files/{str(telegram_id)}"
+
+ logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {telegram_id}")
+ # Use delete_folder, assuming it handles non-empty folders
+ api.delete_folder(
+ folder_path=user_folder_path_on_hf,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"ADMIN ACTION: Deleted all files/folders for user {telegram_id}"
+ )
+ logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.")
+ hf_deletion_successful = True # Mark HF deletion as successful (or at least attempted without fatal error)
+
+ except hf_utils.HfHubHTTPError as e:
+ # Specifically check for 404 Not Found - means the folder doesn't exist, which is fine for deletion purpose
+ if e.response is not None and e.response.status_code == 404:
+ logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub for user {telegram_id}. Skipping HF deletion, proceeding with DB removal.")
+ hf_deletion_successful = True # Consider it successful as there's nothing to delete
+ else:
+ # Other HTTP errors are problematic
+ logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub for {telegram_id}: {e}")
+ flash(f'Ошибка при удалении файлов пользователя {username_display} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
+ return redirect(url_for('admin_panel')) # Stop the process if HF deletion fails unexpectedly
except Exception as e:
- logging.error(f"ADMIN ACTION: Error deleting user data from HF Hub for {user_tg_id_str}: {e}", exc_info=True)
- flash(f'Ошибка при удалении файлов пользователя {user_display_name} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
- return redirect(url_for('admin_panel'))
+ # Catch any other unexpected errors during HF deletion
+ logging.error(f"Unexpected error during HF Hub folder deletion for {telegram_id}: {e}")
+ flash(f'Неожиданная ошибка при удалении файлов {username_display} с сервера: {e}. Пользователь НЕ удален из базы.', 'error')
+ return redirect(url_for('admin_panel')) # Stop the process
- # Step 2: Delete user from local database ONLY IF HF deletion was successful or folder not found
- if hf_delete_success:
+ # --- Step 2: Delete user from database (only if HF deletion was successful or skipped due to 404) ---
+ if hf_deletion_successful:
try:
- del data['users'][user_tg_id_str]
- save_data(data)
- flash(f'Пользователь {user_display_name} (ID: {user_tg_id_str}) удален из базы данных (запрос на удаление файлов с сервера отправлен).')
- logging.info(f"ADMIN ACTION: Successfully deleted user {user_tg_id_str} from database.")
+ del data['users'][telegram_id] # Remove user by integer key
+ save_data(data) # Save the updated data
+ flash(f'Пользователь {username_display} (ID: {telegram_id}) и его файлы (запрос на удаление с сервера отправлен/папка не найдена) успешно удалены из базы данных!')
+ logging.info(f"ADMIN ACTION: Successfully deleted user {telegram_id} from database.")
+ except KeyError:
+ # Should not happen if check at the beginning passed, but handle defensively
+ flash(f'Ошибка: Пользователь {username_display} (ID: {telegram_id}) не найден в базе данных во время попытки удаления (возможно, удален параллельно?).', 'error')
+ logging.error(f"KeyError while trying to delete user {telegram_id} from DB, possibly already removed.")
except Exception as e:
- logging.error(f"ADMIN ACTION: Error saving data after deleting user {user_tg_id_str}: {e}", exc_info=True)
- # This is bad: user deleted from HF but not DB. Manual cleanup needed.
- flash(f'Файлы пользователя {user_display_name} удалены с сервера, но ПРОИЗОШЛА КРИТИЧЕСКАЯ ОШИБКА при удалении пользователя из базы данных: {e}. ТРЕБУЕТСЯ РУЧНОЕ ВМЕШАТЕЛЬСТВО.', 'error')
- else:
- # Should not happen if errors were handled correctly above, but as a safeguard:
- flash(f'Удаление файлов пользователя {user_display_name} с сервера не удалось. Пользователь НЕ удален из базы.', 'error')
-
+ # Critical error: HF files might be deleted, but DB entry remains
+ logging.error(f"CRITICAL: Error saving data after deleting user {telegram_id} from DB: {e}. HF folder deletion was likely attempted.")
+ flash(f'Файлы пользователя {username_display} удалены (или не найдены) с сервера, но произошла КРИТИЧЕСКАЯ ошибка при удалении пользователя из базы данных: {e}. Срочно проверьте консистентность!', 'error')
+ # Do NOT redirect immediately, admin needs to see this critical error message
+ # Redirect back to the admin panel after operations
return redirect(url_for('admin_panel'))
-@app.route('/admhosto/delete_file//', methods=['POST'])
-def admin_delete_file(user_tg_id, file_id):
+@app.route('/admhosto/delete_file//', methods=['POST'])
+def admin_delete_file(telegram_id, file_id):
+ # Ensure the action is performed by an admin
if not is_admin():
flash('Доступ запрещен.', 'error')
- # Redirect to admin panel for consistency
- return redirect(url_for('admin_panel'))
+ return redirect(url_for('index'))
+
+ # Check if write token is available
if not HF_TOKEN_WRITE:
- flash('Удаление невозможно: токен для записи не настроен.', 'error')
- return redirect(url_for('admin_user_files', user_tg_id=user_tg_id))
+ flash('Удаление файла невозможно: токен Hugging Face для записи не настроен.', 'error')
+ return redirect(url_for('admin_user_files', telegram_id=telegram_id)) # Redirect back to user's file list
- user_tg_id_str = str(user_tg_id)
data = load_data()
- user_data = data.get('users', {}).get(user_tg_id_str)
+ # Get the specific user's data using the provided telegram_id
+ user_data = data.get('users', {}).get(telegram_id)
+
if not user_data or not isinstance(user_data, dict):
- flash(f'Пользователь с ID {user_tg_id} не найден.', 'error')
- return redirect(url_for('admin_panel'))
+ flash(f'Пользователь с ID {telegram_id} не найден.', 'error')
+ return redirect(url_for('admin_panel')) # Redirect to main admin panel if user not found
- file_node, parent_node = find_node_by_id(user_data.get('filesystem'), file_id)
+ # Find the file within this specific user's filesystem
+ file_node, parent_node = find_node_by_id(user_data.get('filesystem',{}), file_id)
if not file_node or file_node.get('type') != 'file' or not parent_node:
- flash('Файл не найден в структуре пользователя.', 'error')
- return redirect(url_for('admin_user_files', user_tg_id=user_tg_id))
+ flash(f'Файл с ID {file_id} не найден в структуре пользователя {telegram_id}.', 'error')
+ return redirect(url_for('admin_user_files', telegram_id=telegram_id)) # Redirect back to the user's file list
hf_path = file_node.get('path')
original_filename = file_node.get('original_filename', 'файл')
- user_display_name = user_data.get('tg_first_name', f"ID: {user_tg_id_str}")
- save_needed = False
- error_occurred = False
- admin_tg_id = session.get('tg_id') # For logging
- # Log admin action
- logging.warning(f"ADMIN ACTION (User: {admin_tg_id}): Attempting to delete file ID {file_id} ({original_filename}) for user {user_display_name} (ID: {user_tg_id_str}).")
+ user_info = user_data.get('user_info', {})
+ username_display = f"{user_info.get('first_name','')} {user_info.get('last_name','')}".strip() or f"user_{telegram_id}"
+ # Handle deletion if HF path is missing in metadata
if not hf_path:
- flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error')
- logging.warning(f"ADMIN ACTION: Path missing for file {file_id}. Deleting metadata only.")
+ logging.warning(f"ADMIN ACTION (User: {session.get('telegram_id')}): HF path missing for file {file_id} ({original_filename}) user {telegram_id}. Removing from DB only.")
+ flash(f'Предупреждение: Путь к файлу {original_filename} отсутствует в метаданных. Удаление только из базы.', 'warning')
if remove_node(user_data['filesystem'], file_id):
- save_needed = True
- else:
- error_occurred = True
- flash('Не удалось удалить метаданные файла (путь отсутствовал).')
- else:
- try:
- api = HfApi()
- api.delete_file(
- path_in_repo=hf_path,
- repo_id=REPO_ID,
- repo_type="dataset",
- token=HF_TOKEN_WRITE,
- commit_message=f"ADMIN ACTION (by {admin_tg_id}): Deleted file {original_filename} (ID: {file_id}) for user {user_tg_id_str}"
- )
- logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {user_tg_id_str}")
-
- if remove_node(user_data['filesystem'], file_id):
- save_needed = True
- else:
- flash('Файл удален с сервера, но не найден в базе для удаления метаданных.', 'error')
- error_occurred = True # Metadata mismatch
-
- except hf_utils.EntryNotFoundError:
- logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub during delete for user {user_tg_id_str}. Removing from DB.")
- flash(f'Файл {original_filename} не найден на сервере.')
- if remove_node(user_data['filesystem'], file_id):
- save_needed = True
- flash('Удален из базы.')
- else:
- flash('Не найден ни на сервере, ни в базе данных.', 'error')
- error_occurred = True
- except Exception as e:
- logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {user_tg_id_str}: {e}", exc_info=True)
- flash(f'Ошибка удаления файла {original_filename} с сервера: {e}', 'error')
- error_occurred = True # Do not save if HF delete failed
+ try:
+ save_data(data)
+ flash(f'Метаданные файла {original_filename} (пользователь {username_display}) удалены (путь отсутствовал).')
+ except Exception as e:
+ flash(f'Ошибка сохранения данных после удаления метаданных файла {original_filename} (путь отсутствовал).', 'error')
+ logging.error(f"Admin delete file metadata save error (no path) for user {telegram_id}, file {file_id}: {e}")
+ return redirect(url_for('admin_user_files', telegram_id=telegram_id))
- if save_needed:
- try:
- save_data(data)
- flash(f'Файл {original_filename} успешно удален (база обновлена).')
- except Exception as e:
- logging.error(f"ADMIN ACTION: Delete file DB update error for {user_tg_id_str}: {e}", exc_info=True)
- flash('Файл удален, но произошла ошибка обновления базы данных.', 'error')
- error_occurred = True
- # Redirect back to the user's file list
- return redirect(url_for('admin_user_files', user_tg_id=user_tg_id))
+ # --- Step 1: Delete file from Hugging Face ---
+ hf_deleted = False
+ try:
+ api = HfApi()
+ api.delete_file(
+ path_in_repo=hf_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"ADMIN ACTION: Deleted file {original_filename} (ID: {file_id}) for user {telegram_id}"
+ )
+ logging.info(f"ADMIN ACTION (User: {session.get('telegram_id')}): Deleted file {hf_path} from HF Hub for user {telegram_id}")
+ hf_deleted = True # Mark HF deletion successful
+
+ except hf_utils.EntryNotFoundError:
+ logging.warning(f"ADMIN ACTION (User: {session.get('telegram_id')}): File {hf_path} not found on HF Hub during delete for user {telegram_id}. Removing from DB.")
+ flash(f'Файл {original_filename} (пользователь {username_display}) не найден на сервере Hugging Face. Удаляется из базы.', 'warning')
+ hf_deleted = True # Consider successful as the file is gone from HF
+
+ except Exception as e:
+ logging.error(f"ADMIN ACTION (User: {session.get('telegram_id')}): Error deleting file {hf_path} from HF for {telegram_id}: {e}")
+ flash(f'Ошибка удаления файла {original_filename} (пользователь {username_display}) с сервера: {e}. Файл НЕ удален из базы.', 'error')
+ # Do not proceed with DB deletion if HF deletion failed unexpectedly
+ return redirect(url_for('admin_user_files', telegram_id=telegram_id))
+
+ # --- Step 2: Delete file from database (if HF deletion was successful or skipped due to 404) ---
+ if hf_deleted:
+ if remove_node(user_data['filesystem'], file_id):
+ try:
+ save_data(data)
+ flash(f'Файл {original_filename} (пользователь {username_display}) успешно удален!')
+ logging.info(f"Successfully removed file {file_id} from DB for user {telegram_id}")
+ except Exception as e:
+ # Critical inconsistency
+ flash(f'Файл {original_filename} удален (или не найден) с сервера, но произошла КРИТИЧЕСКАЯ ошибка обновления базы данных для пользователя {username_display}.', 'error')
+ logging.error(f"CRITICAL: Admin delete file DB update error after HF action for user {telegram_id}, file {file_id}: {e}")
+ else:
+ # This indicates an inconsistency if file_node was found initially
+ flash(f'Файл {original_filename} удален (или не найден) с сервера, но НЕ НАЙДЕН в базе данных для удаления метаданных (пользователь {username_display}). Проверьте консистентность.', 'error')
+ logging.error(f"Inconsistency: File {file_id} deleted from HF (or not found) but failed to remove from DB structure for user {telegram_id}")
+
+ # Redirect back to the user's file list in the admin panel
+ return redirect(url_for('admin_user_files', telegram_id=telegram_id))
# --- App Initialization ---
if __name__ == '__main__':
- print(f"--- Configuration ---")
- print(f"Repo ID: {REPO_ID}")
- print(f"Write Token Set: {'Yes' if HF_TOKEN_WRITE else 'No'}")
- print(f"Read Token Set: {'Yes' if HF_TOKEN_READ else 'No (using Write Token if set)'}")
- print(f"Telegram Bot Token Set: {'Yes' if TELEGRAM_BOT_TOKEN else 'No - TELEGRAM AUTH WILL FAIL'}")
- print(f"Admin TG ID: {ADMIN_TELEGRAM_ID if ADMIN_TELEGRAM_ID else 'Not Set - ADMIN PANEL DISABLED'}")
- print(f"Data File: {DATA_FILE}")
- print(f"Upload Folder: {UPLOAD_FOLDER}")
- print(f"---------------------")
-
-
- if not TELEGRAM_BOT_TOKEN:
- logging.error("CRITICAL: TELEGRAM_BOT_TOKEN is not set. Telegram authentication will fail.")
- if not ADMIN_TELEGRAM_ID:
- logging.warning("ADMIN_TELEGRAM_ID is not set. Admin panel functionality will be unavailable.")
-
- # Initial DB download/check
+ app.config['SESSION_PERMANENT'] = True
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30) # Example: 30 days session lifetime
+
+ if not BOT_TOKEN:
+ logging.critical("BOT_TOKEN is not set. Telegram authentication will fail.")
+ if not HF_TOKEN_WRITE:
+ logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.")
+ if not HF_TOKEN_READ:
+ logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN. File downloads/previews might fail for private repos if HF_TOKEN is also not set.")
+ if not ADMIN_TELEGRAM_IDS:
+ logging.warning("ADMIN_TELEGRAM_IDS is not set. Admin panel access will be blocked.")
+ else:
+ logging.info(f"Admin Telegram IDs loaded: {ADMIN_TELEGRAM_IDS}")
+
+
+ # Perform initial DB download/check
if HF_TOKEN_READ or HF_TOKEN_WRITE:
- logging.info("Performing initial database download/check before starting.")
+ logging.info("Performing initial database download/check...")
download_db_from_hf()
else:
- logging.warning("No HF read/write token. Database operations with Hugging Face Hub are disabled.")
- # Ensure local file exists if HF is disabled
- if not os.path.exists(DATA_FILE):
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
- json.dump({'users': {}}, f)
- logging.info(f"Created empty local database file: {DATA_FILE}")
+ logging.warning("No read or write token. Database operations with Hugging Face Hub are disabled.")
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump({'users': {}}, f)
+ logging.info(f"Created empty local database file: {DATA_FILE}")
- # Start periodic backup thread if write token exists
+ # Start periodic backup thread only if write token is available
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
@@ -1991,12 +1864,8 @@ if __name__ == '__main__':
else:
logging.warning("Periodic backup disabled because HF_TOKEN (write access) is not set.")
- # Set session lifetime (e.g., 30 days)
- app.permanent_session_lifetime = timedelta(days=30)
-
# Run the Flask app
- # Use debug=False for production/TMA context
- # Host 0.0.0.0 makes it accessible externally (needed for TMA)
- app.run(debug=False, host='0.0.0.0', port=7860)
-
-# --- END OF FILE app.py ---
\ No newline at end of file
+ # Use Gunicorn or another production server instead of app.run for deployment
+ # Example for local testing:
+ # from datetime import timedelta
+ app.run(debug=False, host='0.0.0.0', port=7861) # Use a different port, e.g., 7861
\ No newline at end of file