Для быстрого доступа к Zeus Cloud, вы можете добавить это приложение на главный экран вашего телефона:
+
+
+
Android (Chrome):
+
+
Нажмите меню (три точки).
+
Выберите "Установить приложение" или "Добавить на главный экран".
+
Подтвердите добавление.
+
+
+
+
iOS (Safari):
+
+
Нажмите кнопку "Поделиться" .
+
Прокрутите вниз и выберите "На экран «Домой»".
+
Нажмите "Добавить".
+
+
+
+
-
-
-
-
-
Просмотр файла
-
-
-
-
-
-
-
+
+ ×
+
'''
+ hf_repo_url_base = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/"
+ return render_template_string(
+ html,
+ style=BASE_STYLE,
+ username=username,
+ current_path=current_path,
+ current_files=current_files,
+ current_folders=current_folders,
+ breadcrumbs=breadcrumbs,
+ repo_id=REPO_ID,
+ hf_repo_url_base=hf_repo_url_base,
+ folder_icon=get_folder_icon(),
+ file_icon=get_file_icon('other'), # Pass a generic icon
+ mimetypes=mimetypes # Pass mimetypes module to template
+ )
+
+@app.route('/create_folder', methods=['POST'])
+def create_folder():
+ if 'username' not in session:
+ return redirect(url_for('login'))
+
+ username = session['username']
+ parent_path = request.form.get('parent_path', '/')
+ folder_name = request.form.get('folder_name', '').strip()
+
+ if not folder_name or not secure_filename(folder_name) == folder_name: # Basic validation
+ flash('Недопустимое имя папки. Используйте буквы, цифры, _ или -.', 'error')
+ return redirect(url_for('dashboard', path=parent_path))
+
+ if not parent_path.startswith('/'): parent_path = '/' + parent_path
+ if not parent_path.endswith('/'): parent_path += '/'
+
+ new_folder_path = os.path.join(parent_path, folder_name).replace('\\', '/') + '/' # Ensure unix paths and trailing slash
+
+ data = load_data()
+ user_data = data['users'][username]
+ user_data.setdefault('folders', [])
+
+ if new_folder_path in user_data['folders']:
+ flash(f'Папка "{folder_name}" уже существует в {parent_path}.', 'warning')
+ else:
+ user_data['folders'].append(new_folder_path)
+ try:
+ save_data(data)
+ flash(f'Папка "{folder_name}" успешно создана.', 'success')
+ except Exception as e:
+ flash('Ошибка сохранения данных при создании папки.', 'error')
+ logging.error(f"Folder creation save error: {e}")
+
+ return redirect(url_for('dashboard', path=parent_path)) # Redirect back to parent
+
+@app.route('/delete_folder', methods=['POST'])
+def delete_folder():
+ if 'username' not in session:
+ flash('Пожалуйста, войдите в систему!', 'info')
+ return redirect(url_for('login'))
+
+ username = session['username']
+ folder_path_to_delete = request.form.get('folder_path')
+
+ if not folder_path_to_delete or not folder_path_to_delete.startswith('/') or not folder_path_to_delete.endswith('/'):
+ flash('Некорректный путь к папке.', 'error')
+ return redirect(request.referrer or url_for('dashboard'))
+
+ data = load_data()
+ user_data = data['users'][username]
+ user_data.setdefault('files', [])
+ user_data.setdefault('folders', [])
+
+ if folder_path_to_delete not in user_data['folders'] and folder_path_to_delete != '/':
+ # Check if it's an implicitly created folder (contains files but not in 'folders' list)
+ has_files = any(f.get('path_prefix', '').startswith(folder_path_to_delete) for f in user_data['files'])
+ if not has_files:
+ flash('Папка не найдена или уже удалена.', 'warning')
+ # Determine redirect path (parent folder)
+ parent_path = '/'.join(folder_path_to_delete.strip('/').split('/')[:-1])
+ parent_path = '/' + parent_path + '/' if parent_path else '/'
+ return redirect(url_for('dashboard', path=parent_path))
+ # If it has files, proceed with deletion logic even if not in folders list
+
+ # Find all files and subfolders within the target folder
+ files_in_folder = [f for f in user_data['files'] if f.get('path_prefix', '').startswith(folder_path_to_delete)]
+ subfolders_in_folder = [f for f in user_data['folders'] if f.startswith(folder_path_to_delete) and f != folder_path_to_delete]
+
+ hf_paths_to_delete = [f['hf_path'] for f in files_in_folder]
+ # Note: We don't explicitly delete folders on HF Hub, just the files within.
+
+ try:
+ api = HfApi(token=HF_TOKEN_WRITE)
+ if hf_paths_to_delete:
+ # HF API might have limitations on bulk delete, delete one by one or batch if possible
+ logging.info(f"Attempting to delete {len(hf_paths_to_delete)} files in folder {folder_path_to_delete} for {username}")
+ deleted_count = 0
+ for hf_path in hf_paths_to_delete:
+ try:
+ api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset")
+ deleted_count += 1
+ except Exception as file_delete_error:
+ # Log error but continue trying to delete others
+ logging.warning(f"Failed to delete file {hf_path} during folder delete: {file_delete_error}")
+ logging.info(f"Successfully deleted {deleted_count} files from HF.")
+
+
+ # Remove files and folders from user data
+ user_data['files'] = [f for f in user_data['files'] if not f.get('path_prefix', '').startswith(folder_path_to_delete)]
+ user_data['folders'] = [f for f in user_data['folders'] if not f.startswith(folder_path_to_delete)] # Removes target and all subfolders
+
+ save_data(data)
+ flash(f'Папка "{folder_path_to_delete.strip("/").split("/")[-1]}" и её содержимое удалены.', 'success')
+
+ except Exception as e:
+ logging.error(f"Error deleting folder {folder_path_to_delete} for {username}: {e}")
+ flash(f'Ошибка при удалении папки: {e}', 'error')
+
+ # Determine redirect path (parent folder)
+ parent_path = '/'.join(folder_path_to_delete.strip('/').split('/')[:-1])
+ parent_path = '/' + parent_path + '/' if parent_path else '/'
+ return redirect(url_for('dashboard', path=parent_path))
+
+
+# Proxy route for serving files with auth token if needed
+@app.route('/file_proxy/')
+def file_proxy(file_path):
+ if 'username' not in session:
+ # Allow access from admin panel view even if user isn't logged in
+ # A better check would involve admin session/role
+ is_admin_referrer = request.referrer and 'admhosto' in request.referrer
+ if not is_admin_referrer:
+ return "Unauthorized", 401
+
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{file_path}"
+ headers = {}
+ if HF_TOKEN_READ:
+ headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
+
+ try:
+ response = requests.get(file_url, headers=headers, stream=True, timeout=30) # Add timeout
+ response.raise_for_status()
+
+ # Determine content type
+ content_type = response.headers.get('Content-Type', mimetypes.guess_type(file_path)[0] or 'application/octet-stream')
+
+ # Create a Flask response to stream the content
+ def generate():
+ for chunk in response.iter_content(chunk_size=8192):
+ yield chunk
+
+ # For PDF, force inline display if possible
+ headers_out = {'Content-Type': content_type}
+ if content_type == 'application/pdf':
+ # Extract filename for Content-Disposition
+ filename = os.path.basename(file_path)
+ headers_out['Content-Disposition'] = f'inline; filename="{filename}"'
+
+ # For video/audio, enable range requests (partial content)
+ if content_type.startswith('video/') or content_type.startswith('audio/'):
+ headers_out['Accept-Ranges'] = 'bytes'
+ # Basic handling for Range requests (can be improved)
+ range_header = request.headers.get('Range', None)
+ if range_header:
+ try:
+ size = int(response.headers.get('Content-Length', 0))
+ byte1, byte2 = 0, None
+ m = re.match(r'bytes=(\d+)-(\d*)', range_header)
+ if m:
+ byte1 = int(m.group(1))
+ if m.group(2):
+ byte2 = int(m.group(2))
+ if size > 0 and byte1 < size:
+ # Adjust byte2 if necessary
+ if byte2 is None or byte2 >= size:
+ byte2 = size - 1
+ length = byte2 - byte1 + 1
+ # Re-fetch the specific range - more complex with requests stream
+ # Simpler approach: return 206 but stream the whole thing (less efficient but works for seeking)
+ # A proper implementation requires seeking in the stream or making a new ranged request.
+ # For now, let's return 206 status but might not perfectly handle all range scenarios.
+ status_code = 206
+ headers_out['Content-Range'] = f'bytes {byte1}-{byte2}/{size}'
+ headers_out['Content-Length'] = str(length)
+ # This requires adjusting the generator to yield only the requested range.
+ # Simplified: Return 200 OK and let browser handle seeking if possible.
+ status_code = 200 # Revert to 200 for simplicity here
+ del headers_out['Content-Range'] # Remove incorrect header if sending full content
+ del headers_out['Content-Length'] # Let Flask handle length for full stream
+ except (ValueError, IndexError, TypeError, AttributeError) as range_error:
+ logging.warning(f"Range header parsing error: {range_error}")
+ status_code = 200 # Fallback to 200
+ else:
+ status_code = 200
+ else:
+ status_code = 200
+
+
+ return app.response_class(generate(), headers=headers_out, mimetype=content_type, status=status_code)
+
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Error proxying file {file_path}: {e}")
+ # Return a placeholder or error image/message
+ return f"Error fetching file: {e}", 502 # Bad Gateway
+ except Exception as e:
+ logging.error(f"Unexpected error in file proxy for {file_path}: {e}")
+ return "Internal server error", 500
+
+
+@app.route('/download/')
+def download_file(file_path):
+ is_admin_referrer = request.referrer and 'admhosto' in request.referrer
+ if 'username' not in session and not is_admin_referrer:
+ flash('Пожалуйста, войдите в систему!', 'info')
+ return redirect(url_for('login'))
+
+ # Determine original filename for download prompt
+ # This requires looking up the file_path in the database
+ data = load_data()
+ original_filename = os.path.basename(file_path) # Fallback
+ found = False
+ requesting_user = session.get('username')
+
+ user_to_check = None
+ if is_admin_referrer:
+ # Try to extract username from path for admin downloads
+ try:
+ parts = file_path.split('/')
+ if len(parts) > 2 and parts[0] == 'cloud_files':
+ user_to_check = parts[1]
+ except IndexError:
+ pass
+ elif requesting_user:
+ user_to_check = requesting_user
+
+
+ if user_to_check and user_to_check in data['users']:
+ user_files = data['users'][user_to_check].get('files', [])
+ for f in user_files:
+ if f.get('hf_path') == file_path:
+ original_filename = f.get('original_filename', original_filename)
+ # Basic permission check: User must own the file OR it's an admin downloading
+ if requesting_user == user_to_check or is_admin_referrer:
+ found = True
+ break
+ else:
+ flash('У вас нет доступа к этому файлу!', 'error')
+ return redirect(url_for('dashboard'))
+
+ if not found and not is_admin_referrer: # Allow admin download even if file metadata missing
+ flash('Файл не найден или у вас нет доступа.', 'error')
+ # Redirect back to appropriate dashboard/admin page
+ if is_admin_referrer and user_to_check:
+ return redirect(url_for('admin_user_files', username=user_to_check))
+ elif requesting_user:
+ return redirect(url_for('dashboard'))
+ else:
+ return redirect(url_for('login'))
+
+
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{file_path}?download=true"
+ headers = {}
+ if HF_TOKEN_READ:
+ headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
+
+ try:
+ response = requests.get(file_url, headers=headers, stream=True, timeout=60)
+ response.raise_for_status()
+
+ file_content = BytesIO(response.content)
+ return send_file(
+ file_content,
+ as_attachment=True,
+ download_name=original_filename, # Use original name for download prompt
+ mimetype=response.headers.get('Content-Type', 'application/octet-stream')
+ )
+ except requests.exceptions.RequestException as e:
+ logging.error(f"Error downloading file from HF ({file_path}): {e}")
+ flash(f'Ошибка скачивания файла: {e}', 'error')
+ except Exception as e:
+ logging.error(f"Unexpected error during download ({file_path}): {e}")
+ flash('Произошла непредвиденная ошибка при скачивании файла.', 'error')
+
+ # Redirect back if download fails
+ referer = request.referrer
+ if referer:
+ return redirect(referer)
+ elif is_admin_referrer and user_to_check:
+ return redirect(url_for('admin_user_files', username=user_to_check))
+ elif requesting_user:
+ return redirect(url_for('dashboard'))
+ else:
+ return redirect(url_for('login'))
+
+
+@app.route('/delete_file/', methods=['POST'])
+def delete_file(file_path):
+ if 'username' not in session:
+ flash('Пожалуйста, войдите в систему!', 'info')
+ return redirect(url_for('login'))
+
+ username = session['username']
+ current_path_redirect = request.form.get('current_path', '/') # Get path for redirect
+
+ data = load_data()
+ if username not in data['users']:
+ session.pop('username', None)
+ flash('Пользователь не найден!', 'error')
+ return redirect(url_for('login'))
+
+ user_files = data['users'][username].get('files', [])
+ original_file_list = list(user_files) # Copy for modification
+ file_to_delete_index = -1
+
+ for i, file in enumerate(original_file_list):
+ if file.get('hf_path') == file_path:
+ file_to_delete_index = i
+ break
+
+ if file_to_delete_index == -1:
+ flash('Файл не найден в вашей базе данных!', 'warning')
+ # Attempt deletion from HF anyway in case of desync? Or just redirect?
+ # Let's try deleting from HF anyway
+ # return redirect(url_for('dashboard', path=current_path_redirect))
+
+ try:
+ api = HfApi(token=HF_TOKEN_WRITE)
+ api.delete_file(
+ path_in_repo=file_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ commit_message=f"User delete: {file_path} for {username}"
+ )
+ logging.info(f"Successfully deleted file {file_path} from HF for user {username}")
+
+ # Remove from data only if found
+ if file_to_delete_index != -1:
+ del original_file_list[file_to_delete_index]
+ data['users'][username]['files'] = original_file_list
+ save_data(data)
+ flash('Файл успешно удален!', 'success')
+ else:
+ flash('Файл удален с хранилища (не найден в базе).', 'info')
+
+
+ except Exception as e:
+ # Handle case where file might already be deleted on HF (e.g., HTTP 404 or specific API error)
+ # The exact error depends on the huggingface_hub library version and API response.
+ # For now, log the error and inform the user.
+ logging.error(f"Error deleting file {file_path} for {username}: {e}")
+ # If file was found in DB but failed to delete on HF, keep it in DB?
+ # Or remove from DB anyway if deletion likely succeeded despite error? Risky.
+ # Let's keep it in DB if HF deletion fails explicitly.
+ flash(f'Ошибка удаления файла: {e}', 'error')
+
+
+ return redirect(url_for('dashboard', path=current_path_redirect))
+
+@app.route('/logout')
+def logout():
+ session.pop('username', None)
+ flash('Вы успешно вышли из системы.', 'success')
+ # Client-side JS should handle localStorage removal on login page load or logout click
+ return redirect(url_for('login'))
+
+
+# --- Admin Routes ---
+# WARNING: These routes lack proper admin authentication/authorization.
+# Implement robust checks (e.g., separate admin login, role check) in production.
+
+@app.route('/admhosto')
+def admin_panel():
+ # TODO: Add admin authentication check here
+ data = load_data()
+ users = data.get('users', {})
+ sorted_users = dict(sorted(users.items()))
-ADMIN_PANEL_TEMPLATE = '''
+ html = '''
Админ-панель - Zeus Cloud
-
-
-
+
+
-
Админ-панель Zeus Cloud
-
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
{{ message }}
- {% endfor %}
- {% endif %}
+
Админ-панель Zeus Cloud
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
+ {% if not current_files and not current_folders %}
+
Папка пуста.
+ {% endif %}
-
-
-
-
-
-
Просмотр файла
-
-
-
-
-
-
+
+ ×
+
-
'''
+ return render_template_string(
+ html,
+ style=BASE_STYLE,
+ username=username,
+ current_path=current_path,
+ current_files=current_files,
+ current_folders=current_folders,
+ breadcrumbs=breadcrumbs,
+ repo_id=REPO_ID,
+ folder_icon=get_folder_icon(),
+ file_icon=get_file_icon('other'),
+ mimetypes=mimetypes
+ )
-# --- PWA Files ---
-# Create these files in your project directory
-
-# manifest.json (place in root or static)
-MANIFEST_JSON_CONTENT = """
-{
- "name": "Zeus Cloud",
- "short_name": "ZeusCloud",
- "description": "Personal Cloud Storage powered by Zeus",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#1E1E2E",
- "theme_color": "#4A90E2",
- "icons": [
- {
- "src": "/static/icon-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/static/icon-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ]
-}
-"""
-
-# service-worker.js (place in root)
-SERVICE_WORKER_JS_CONTENT = """
-const CACHE_NAME = 'zeus-cloud-cache-v1';
-const urlsToCache = [
- '/',
- '/dashboard', // Cache main dashboard route if possible (might be dynamic)
- // Add paths to your BASE_STYLE if served separately, or other static assets
- // '/static/style.css',
- // '/static/icon-192x192.png',
- // '/static/icon-512x512.png',
- 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css', // Cache external CSS
- 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap' // Cache external font CSS
-];
-
-self.addEventListener('install', event => {
- // Perform install steps
- event.waitUntil(
- caches.open(CACHE_NAME)
- .then(cache => {
- console.log('Opened cache');
- // AddAll can fail if one request fails. Consider adding individually.
- return cache.addAll(urlsToCache.map(url => new Request(url, { mode: 'no-cors' }))) // Use no-cors for external resources if needed, might prevent caching them properly
- .catch(error => console.error('Failed to cache initial resources:', error));
- })
- );
-});
-
-self.addEventListener('fetch', event => {
- event.respondWith(
- caches.match(event.request)
- .then(response => {
- // Cache hit - return response
- if (response) {
- return response;
- }
+@app.route('/admhosto/delete_user/', methods=['POST'])
+def admin_delete_user(username):
+ # TODO: Add admin authentication check here
+ data = load_data()
+ if username not in data.get('users', {}):
+ flash(f'Пользователь "{username}" не найден!', 'error')
+ return redirect(url_for('admin_panel'))
- // IMPORTANT: Clone the request. A request is a stream and
- // can only be consumed once. Since we are consuming this
- // once by cache and once by the browser for fetch, we need
- // to clone the response.
- const fetchRequest = event.request.clone();
-
- return fetch(fetchRequest).then(
- response => {
- // Check if we received a valid response
- // Don't cache non-GET requests or opaque responses (no-cors) unless necessary
- if(!response || response.status !== 200 || response.type !== 'basic' || event.request.method !== 'GET') {
- return response;
- }
+ user_data = data['users'][username]
+ files_to_delete = user_data.get('files', [])
+ hf_paths_to_delete = [f['hf_path'] for f in files_to_delete]
- // IMPORTANT: Clone the response. A response is a stream
- // and because we want the browser to consume the response
- // as well as the cache consuming the response, we need
- // to clone it so we have two streams.
- const responseToCache = response.clone();
-
- caches.open(CACHE_NAME)
- .then(cache => {
- cache.put(event.request, responseToCache);
- });
-
- return response;
- }
- ).catch(error => {
- console.error("Fetch failed; returning offline page instead.", error);
- // Optional: Return an offline fallback page
- // return caches.match('/offline.html');
- });
- })
- );
-});
-
-// Cache cleanup
-self.addEventListener('activate', event => {
- const cacheWhitelist = [CACHE_NAME]; // Only keep the current cache version
- event.waitUntil(
- caches.keys().then(cacheNames => {
- return Promise.all(
- cacheNames.map(cacheName => {
- if (cacheWhitelist.indexOf(cacheName) === -1) {
- console.log('Deleting old cache:', cacheName);
- return caches.delete(cacheName);
- }
- })
- );
- })
- );
-});
-
-"""
-
-# Route to serve manifest.json
-@app.route('/manifest.json')
-def serve_manifest():
- return Response(MANIFEST_JSON_CONTENT, mimetype='application/manifest+json')
-
-# Route to serve service-worker.js
-@app.route('/service-worker.js')
-def serve_sw():
- return Response(SERVICE_WORKER_JS_CONTENT, mimetype='application/javascript')
-
-# Route for static files (like icons) - ensure you have a 'static' folder
-@app.route('/static/')
-def serve_static(filename):
- # Make sure the static folder exists
- static_folder = os.path.join(app.root_path, 'static')
- return send_from_directory(static_folder, filename)
-
-# --- Main Execution ---
-from datetime import timedelta
-from flask import send_from_directory
+ # It's safer and often sufficient to delete individual files listed in the DB.
+ # Deleting the entire folder might remove files not tracked in the DB if inconsistencies exist.
+ try:
+ api = HfApi(token=HF_TOKEN_WRITE)
+ deleted_count = 0
+ logging.info(f"Admin attempting to delete {len(hf_paths_to_delete)} files for user {username}")
+ if hf_paths_to_delete:
+ for hf_path in hf_paths_to_delete:
+ try:
+ api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset")
+ deleted_count += 1
+ except Exception as file_delete_error:
+ logging.warning(f"Admin failed to delete file {hf_path} during user delete for {username}: {file_delete_error}")
+
+ logging.info(f"Admin deleted {deleted_count} files from HF for user {username}.")
+
+ # Delete user from database
+ del data['users'][username]
+ save_data(data)
+ flash(f'Пользователь {username} и его файлы ({deleted_count}) успешно удалены!', 'success')
+ logging.info(f"Admin deleted user {username} and their file records.")
+
+ except Exception as e:
+ logging.error(f"Error deleting user {username} by admin: {e}")
+ flash(f'Ошибка при удалении пользователя {username}: {e}', 'error')
+
+ return redirect(url_for('admin_panel'))
+
+@app.route('/admhosto/delete_folder//', methods=['POST'])
+def admin_delete_folder(username, folder_path):
+ # TODO: Add admin authentication check here
+ if not folder_path.startswith('/'): folder_path = '/' + folder_path
+ if not folder_path.endswith('/'): folder_path += '/'
+
+ data = load_data()
+ if username not in data.get('users', {}):
+ flash(f'Пользователь "{username}" не найден!', 'error')
+ return redirect(url_for('admin_panel'))
+
+ user_data = data['users'][username]
+ user_data.setdefault('files', [])
+ user_data.setdefault('folders', [])
+
+ # Find all files and subfolders within the target folder for this specific user
+ files_in_folder = [f for f in user_data['files'] if f.get('path_prefix', '').startswith(folder_path)]
+ subfolders_in_folder = [f for f in user_data['folders'] if f.startswith(folder_path) and f != folder_path] # Exclude self
+ hf_paths_to_delete = [f['hf_path'] for f in files_in_folder]
+
+ try:
+ api = HfApi(token=HF_TOKEN_WRITE)
+ deleted_count = 0
+ logging.info(f"Admin attempting to delete {len(hf_paths_to_delete)} files in folder {folder_path} for user {username}")
+ if hf_paths_to_delete:
+ for hf_path in hf_paths_to_delete:
+ try:
+ api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset")
+ deleted_count += 1
+ except Exception as file_delete_error:
+ logging.warning(f"Admin failed to delete file {hf_path} during folder delete: {file_delete_error}")
+ logging.info(f"Admin deleted {deleted_count} files from HF in folder {folder_path} for {username}.")
+
+ # Remove files and folders from user data
+ user_data['files'] = [f for f in user_data['files'] if not f.get('path_prefix', '').startswith(folder_path)]
+ user_data['folders'] = [f for f in user_data['folders'] if not f.startswith(folder_path)] # Removes target and subfolders
+
+ save_data(data)
+ folder_name = folder_path.strip('/').split('/')[-1]
+ flash(f'Папка "{folder_name}" и её содержимое ({deleted_count} файлов) удалены для пользователя {username}.', 'success')
+
+ except Exception as e:
+ logging.error(f"Admin error deleting folder {folder_path} for {username}: {e}")
+ flash(f'Ошибка при удалении папки: {e}', 'error')
+
+ # Determine redirect path (parent folder within admin view)
+ parent_path = '/'.join(folder_path.strip('/').split('/')[:-1])
+ parent_path = '/' + parent_path + '/' if parent_path else '/'
+ return redirect(url_for('admin_user_files', username=username, path=parent_path))
+
+
+@app.route('/admhosto/delete_file//', methods=['POST'])
+def admin_delete_file(username, file_path):
+ # TODO: Add admin authentication check here
+ current_path_redirect = request.form.get('current_path', '/') # Get path for redirect
+
+ data = load_data()
+ if username not in data.get('users', {}):
+ flash(f'Пользователь "{username}" не найден!', 'error')
+ return redirect(url_for('admin_panel'))
+
+ user_files = data['users'][username].get('files', [])
+ original_file_list = list(user_files)
+ file_to_delete_index = -1
+ original_filename = os.path.basename(file_path) # Fallback name
+
+ for i, file in enumerate(original_file_list):
+ if file.get('hf_path') == file_path:
+ file_to_delete_index = i
+ original_filename = file.get('original_filename', original_filename)
+ break
+
+ # We proceed with deletion even if not found in DB, assuming admin wants it gone from HF
+ if file_to_delete_index == -1:
+ flash(f'Файл "{original_filename}" не найден в базе данных пользователя, но будет предпринята попытка удаления из хранилища.', 'warning')
+
+ try:
+ api = HfApi(token=HF_TOKEN_WRITE)
+ api.delete_file(
+ path_in_repo=file_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ commit_message=f"Admin delete: {file_path} for {username}"
+ )
+ logging.info(f"Admin successfully deleted file {file_path} from HF for user {username}")
+
+ if file_to_delete_index != -1:
+ del original_file_list[file_to_delete_index]
+ data['users'][username]['files'] = original_file_list
+ save_data(data)
+ flash(f'Файл "{original_filename}" успешно удален!', 'success')
+ else:
+ # If it wasn't in the DB, no save_data needed, just confirm HF deletion attempt
+ flash(f'Файл "{original_filename}" удален из хранилища (или уже отсутствовал).', 'info')
+
+ except Exception as e:
+ logging.error(f"Admin error deleting file {file_path} for {username}: {e}")
+ flash(f'Ошибка удаления файла "{original_filename}": {e}', 'error')
+
+ return redirect(url_for('admin_user_files', username=username, path=current_path_redirect))
if __name__ == '__main__':
- # Ensure static directory and PWA files exist before starting
- static_dir = os.path.join(app.root_path, 'static')
- os.makedirs(static_dir, exist_ok=True)
- # Create placeholder icons if they don't exist (replace with real icons)
- icon192_path = os.path.join(static_dir, 'icon-192x192.png')
- icon512_path = os.path.join(static_dir, 'icon-512x512.png')
- if not os.path.exists(icon192_path):
- # Create a simple placeholder PNG (requires Pillow)
- try:
- from PIL import Image, ImageDraw
- img = Image.new('RGB', (192, 192), color = (74, 144, 226)) # Blue
- d = ImageDraw.Draw(img)
- d.text((10,10), "ZC 192", fill=(255,255,255))
- img.save(icon192_path)
- except ImportError:
- logging.warning("Pillow not installed, cannot create placeholder icon-192x192.png")
-
- if not os.path.exists(icon512_path):
- try:
- from PIL import Image, ImageDraw
- img = Image.new('RGB', (512, 512), color = (74, 144, 226)) # Blue
- d = ImageDraw.Draw(img)
- d.text((20,20), "Zeus Cloud 512", fill=(255,255,255))
- img.save(icon512_path)
- except ImportError:
- logging.warning("Pillow not installed, cannot create placeholder icon-512x512.png")
-
- # Check tokens
if not HF_TOKEN_WRITE:
- logging.warning("HF_TOKEN (write access) is not set. File/folder uploads and deletions will fail.")
+ logging.warning("!!! HF_TOKEN (write access) is NOT set. File/folder uploads, deletions, and DB backups will FAIL. !!!")
if not HF_TOKEN_READ:
- logging.warning("HF_TOKEN_READ is not set. Falling back to HF_TOKEN (if set). File downloads/previews might fail for private repos.")
+ 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 HF_TOKEN_WRITE:
+ threading.Thread(target=periodic_backup, daemon=True).start()
+ else:
+ logging.warning("Periodic database backup disabled because HF_TOKEN (write access) is not set.")
# Initial data load/download
load_data()
- # Background backup thread (removed as frequent saves handle this)
- # If you prefer periodic backup *in addition* to save-on-change:
- # if HF_TOKEN_WRITE:
- # threading.Thread(target=periodic_backup, daemon=True).start() # Define periodic_backup function
-
- app.run(debug=False, host='0.0.0.0', port=7860) # Disable debug for production
\ No newline at end of file
+ # Use Gunicorn or Waitress in production instead of app.run(debug=True)
+ # Example: gunicorn --bind 0.0.0.0:7860 app:app
+ app.run(host='0.0.0.0', port=7860, debug=False) # Set debug=False for production
\ No newline at end of file