hesabixArc/hesabixAPI/adapters/api/v1/admin/file_storage.py

726 lines
26 KiB
Python
Raw Normal View History

2025-09-21 19:53:21 +03:30
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Request
from sqlalchemy.orm import Session
2025-09-21 21:40:10 +03:30
from sqlalchemy import and_
2025-09-21 19:53:21 +03:30
from adapters.db.session import get_db
2025-09-21 21:40:10 +03:30
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_app_permission
2025-09-21 19:53:21 +03:30
from app.core.responses import success_response
2025-09-21 20:31:52 +03:30
from app.core.responses import ApiError
2025-09-21 19:53:21 +03:30
from app.core.i18n import locale_dependency
from app.services.file_storage_service import FileStorageService
from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository
from adapters.db.models.user import User
from adapters.db.models.file_storage import StorageConfig, FileStorage
2025-09-21 20:31:52 +03:30
from adapters.api.v1.schema_models.file_storage import (
2025-09-21 19:53:21 +03:30
StorageConfigCreateRequest,
StorageConfigUpdateRequest,
FileUploadRequest,
FileVerificationRequest,
FileInfo,
FileUploadResponse,
StorageConfigResponse,
FileStatisticsResponse,
CleanupResponse
)
router = APIRouter(prefix="/admin/files", tags=["Admin File Management"])
@router.get("/", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def list_all_files(
request: Request,
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=100),
module_context: Optional[str] = Query(None),
is_temporary: Optional[bool] = Query(None),
is_verified: Optional[bool] = Query(None),
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""لیست تمام فایل‌ها با فیلتر"""
try:
2025-09-21 21:40:10 +03:30
file_repo = FileStorageRepository(db)
# محاسبه offset برای pagination
offset = (page - 1) * size
# ساخت فیلترها
filters = []
if module_context:
filters.append(FileStorage.module_context == module_context)
if is_temporary is not None:
filters.append(FileStorage.is_temporary == is_temporary)
if is_verified is not None:
filters.append(FileStorage.is_verified == is_verified)
# اضافه کردن فیلتر حذف نشده
filters.append(FileStorage.deleted_at.is_(None))
# دریافت فایل‌ها با فیلتر و pagination
files_query = db.query(FileStorage).filter(and_(*filters))
total_count = files_query.count()
files = files_query.order_by(FileStorage.created_at.desc()).offset(offset).limit(size).all()
# تبدیل به فرمت مناسب
files_data = []
for file in files:
files_data.append({
"id": str(file.id),
"original_name": file.original_name,
"stored_name": file.stored_name,
"file_size": file.file_size,
"mime_type": file.mime_type,
"storage_type": file.storage_type,
"module_context": file.module_context,
"context_id": str(file.context_id) if file.context_id else None,
"is_temporary": file.is_temporary,
"is_verified": file.is_verified,
"is_active": file.is_active,
"created_at": file.created_at.isoformat(),
"updated_at": file.updated_at.isoformat(),
"expires_at": file.expires_at.isoformat() if file.expires_at else None,
"uploaded_by": file.uploaded_by,
"checksum": file.checksum
})
# محاسبه pagination info
total_pages = (total_count + size - 1) // size
has_next = page < total_pages
has_prev = page > 1
2025-09-21 19:53:21 +03:30
data = {
2025-09-21 21:40:10 +03:30
"files": files_data,
"pagination": {
"page": page,
"size": size,
"total_count": total_count,
"total_pages": total_pages,
"has_next": has_next,
"has_prev": has_prev
},
"filters": {
"module_context": module_context,
"is_temporary": is_temporary,
"is_verified": is_verified
}
2025-09-21 19:53:21 +03:30
}
return success_response(data, request)
except Exception as e:
raise ApiError(
code="FILE_LIST_ERROR",
message=translator.t("FILE_LIST_ERROR", f"خطا در دریافت لیست فایل‌ها: {str(e)}"),
http_status=500,
translator=translator
)
@router.get("/unverified", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def get_unverified_files(
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""فایل‌های تایید نشده"""
try:
file_service = FileStorageService(db)
unverified_files = await file_service.file_repo.get_unverified_temporary_files()
data = {
"unverified_files": [
{
"file_id": str(file.id),
"original_name": file.original_name,
"file_size": file.file_size,
"module_context": file.module_context,
"created_at": file.created_at.isoformat(),
"expires_at": file.expires_at.isoformat() if file.expires_at else None
}
for file in unverified_files
],
"count": len(unverified_files)
}
return success_response(data, request)
except Exception as e:
raise ApiError(
code="UNVERIFIED_FILES_ERROR",
message=translator.t("UNVERIFIED_FILES_ERROR", f"خطا در دریافت فایل‌های تایید نشده: {str(e)}"),
http_status=500,
translator=translator
)
@router.post("/cleanup-temporary", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def cleanup_temporary_files(
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""پاکسازی فایل‌های موقت"""
try:
file_service = FileStorageService(db)
cleanup_result = await file_service.cleanup_unverified_files()
data = {
"message": translator.t("CLEANUP_COMPLETED", "Temporary files cleanup completed"),
"result": cleanup_result
}
return success_response(data, request)
except Exception as e:
raise ApiError(
code="CLEANUP_ERROR",
message=translator.t("CLEANUP_ERROR", f"خطا در پاکسازی فایل‌های موقت: {str(e)}"),
http_status=500,
translator=translator
)
@router.delete("/{file_id}", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def force_delete_file(
file_id: UUID,
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""حذف اجباری فایل"""
try:
file_service = FileStorageService(db)
success = await file_service.delete_file(file_id)
if not success:
raise ApiError(
code="FILE_NOT_FOUND",
message=translator.t("FILE_NOT_FOUND", "فایل یافت نشد"),
http_status=404,
translator=translator
)
data = {"message": translator.t("FILE_DELETED_SUCCESS", "File deleted successfully")}
return success_response(data, request)
except ApiError:
raise
except Exception as e:
raise ApiError(
code="DELETE_FILE_ERROR",
message=translator.t("DELETE_FILE_ERROR", f"خطا در حذف فایل: {str(e)}"),
http_status=500,
translator=translator
)
@router.put("/{file_id}/restore", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def restore_file(
file_id: UUID,
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""بازیابی فایل حذف شده"""
try:
file_repo = FileStorageRepository(db)
success = await file_repo.restore_file(file_id)
if not success:
raise ApiError(
code="FILE_NOT_FOUND",
message=translator.t("FILE_NOT_FOUND", "فایل یافت نشد"),
http_status=404,
translator=translator
)
data = {"message": translator.t("FILE_RESTORED_SUCCESS", "File restored successfully")}
return success_response(data, request)
except ApiError:
raise
except Exception as e:
raise ApiError(
code="RESTORE_FILE_ERROR",
message=translator.t("RESTORE_FILE_ERROR", f"خطا در بازیابی فایل: {str(e)}"),
http_status=500,
translator=translator
)
@router.get("/statistics", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def get_file_statistics(
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""آمار استفاده از فضای ذخیره‌سازی"""
try:
file_service = FileStorageService(db)
statistics = await file_service.get_storage_statistics()
return success_response(statistics, request)
except Exception as e:
raise ApiError(
code="STATISTICS_ERROR",
message=translator.t("STATISTICS_ERROR", f"خطا در دریافت آمار: {str(e)}"),
http_status=500,
translator=translator
)
# Storage Configuration Management
@router.get("/storage-configs/", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def get_storage_configs(
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""لیست تنظیمات ذخیره‌سازی"""
try:
config_repo = StorageConfigRepository(db)
2025-09-21 21:40:10 +03:30
configs = config_repo.get_all_configs()
2025-09-21 19:53:21 +03:30
data = {
"configs": [
{
"id": str(config.id),
"name": config.name,
"storage_type": config.storage_type,
"is_default": config.is_default,
"is_active": config.is_active,
2025-09-21 21:40:10 +03:30
"config_data": config.config_data,
2025-09-21 19:53:21 +03:30
"created_at": config.created_at.isoformat()
}
for config in configs
]
}
return success_response(data, request)
except Exception as e:
raise ApiError(
code="STORAGE_CONFIGS_ERROR",
message=translator.t("STORAGE_CONFIGS_ERROR", f"خطا در دریافت تنظیمات ذخیره‌سازی: {str(e)}"),
http_status=500,
translator=translator
)
@router.post("/storage-configs/", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def create_storage_config(
request: Request,
config_request: StorageConfigCreateRequest,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""ایجاد تنظیمات ذخیره‌سازی جدید"""
try:
config_repo = StorageConfigRepository(db)
config = await config_repo.create_config(
name=config_request.name,
storage_type=config_request.storage_type,
config_data=config_request.config_data,
2025-09-21 21:40:10 +03:30
created_by=current_user.get_user_id(),
is_default=config_request.is_default,
is_active=config_request.is_active
2025-09-21 19:53:21 +03:30
)
data = {
"message": translator.t("STORAGE_CONFIG_CREATED", "Storage configuration created successfully"),
"config_id": str(config.id)
}
return success_response(data, request)
except Exception as e:
raise ApiError(
code="CREATE_STORAGE_CONFIG_ERROR",
message=translator.t("CREATE_STORAGE_CONFIG_ERROR", f"خطا در ایجاد تنظیمات ذخیره‌سازی: {str(e)}"),
http_status=400,
translator=translator
)
@router.put("/storage-configs/{config_id}", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def update_storage_config(
config_id: UUID,
request: Request,
config_request: StorageConfigUpdateRequest,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""بروزرسانی تنظیمات ذخیره‌سازی"""
try:
config_repo = StorageConfigRepository(db)
# TODO: پیاده‌سازی بروزرسانی
data = {"message": translator.t("STORAGE_CONFIG_UPDATE_NOT_IMPLEMENTED", "Storage configuration update - to be implemented")}
return success_response(data, request)
except Exception as e:
raise ApiError(
code="UPDATE_STORAGE_CONFIG_ERROR",
message=translator.t("UPDATE_STORAGE_CONFIG_ERROR", f"خطا در بروزرسانی تنظیمات ذخیره‌سازی: {str(e)}"),
http_status=500,
translator=translator
)
@router.put("/storage-configs/{config_id}/set-default", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def set_default_storage_config(
config_id: UUID,
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""تنظیم به عنوان پیش‌فرض"""
try:
config_repo = StorageConfigRepository(db)
success = await config_repo.set_default_config(config_id)
if not success:
raise ApiError(
code="STORAGE_CONFIG_NOT_FOUND",
message=translator.t("STORAGE_CONFIG_NOT_FOUND", "تنظیمات ذخیره‌سازی یافت نشد"),
http_status=404,
translator=translator
)
data = {"message": translator.t("DEFAULT_STORAGE_CONFIG_UPDATED", "Default storage configuration updated successfully")}
return success_response(data, request)
except ApiError:
raise
except Exception as e:
raise ApiError(
code="SET_DEFAULT_STORAGE_CONFIG_ERROR",
message=translator.t("SET_DEFAULT_STORAGE_CONFIG_ERROR", f"خطا در تنظیم پیش‌فرض: {str(e)}"),
http_status=500,
translator=translator
)
@router.delete("/storage-configs/{config_id}", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def delete_storage_config(
2025-09-22 11:00:18 +03:30
config_id: str,
2025-09-21 19:53:21 +03:30
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""حذف تنظیمات ذخیره‌سازی"""
try:
config_repo = StorageConfigRepository(db)
2025-09-22 11:00:18 +03:30
# بررسی وجود فایل‌ها قبل از حذف
file_count = config_repo.count_files_by_storage_config(config_id)
if file_count > 0:
raise ApiError(
code="STORAGE_CONFIG_HAS_FILES",
message=translator.t("STORAGE_CONFIG_HAS_FILES", f"این تنظیمات ذخیره‌سازی دارای {file_count} فایل است و قابل حذف نیست"),
http_status=400,
translator=translator
)
success = config_repo.delete_config(config_id)
2025-09-21 19:53:21 +03:30
if not success:
raise ApiError(
code="STORAGE_CONFIG_NOT_FOUND",
message=translator.t("STORAGE_CONFIG_NOT_FOUND", "تنظیمات ذخیره‌سازی یافت نشد"),
http_status=404,
translator=translator
)
data = {"message": translator.t("STORAGE_CONFIG_DELETED", "Storage configuration deleted successfully")}
return success_response(data, request)
except ApiError:
raise
except Exception as e:
raise ApiError(
code="DELETE_STORAGE_CONFIG_ERROR",
message=translator.t("DELETE_STORAGE_CONFIG_ERROR", f"خطا در حذف تنظیمات ذخیره‌سازی: {str(e)}"),
http_status=500,
translator=translator
)
@router.post("/storage-configs/{config_id}/test", response_model=dict)
@require_app_permission("superadmin")
2025-09-21 19:53:21 +03:30
async def test_storage_config(
2025-09-21 21:40:10 +03:30
config_id: str,
2025-09-21 19:53:21 +03:30
request: Request,
db: Session = Depends(get_db),
2025-09-21 21:40:10 +03:30
current_user: AuthContext = Depends(get_current_user),
2025-09-21 19:53:21 +03:30
translator = Depends(locale_dependency)
):
"""تست اتصال به storage"""
try:
2025-09-21 21:40:10 +03:30
config_repo = StorageConfigRepository(db)
config = db.query(StorageConfig).filter(StorageConfig.id == config_id).first()
if not config:
raise ApiError(
code="STORAGE_CONFIG_NOT_FOUND",
message=translator.t("STORAGE_CONFIG_NOT_FOUND", "تنظیمات ذخیره‌سازی یافت نشد"),
http_status=404,
translator=translator
)
# تست اتصال بر اساس نوع storage
test_result = await _test_storage_connection(config)
if test_result["success"]:
data = {
"message": translator.t("STORAGE_CONNECTION_SUCCESS", "اتصال به storage موفقیت‌آمیز بود"),
"test_result": test_result
}
else:
data = {
"message": translator.t("STORAGE_CONNECTION_FAILED", "اتصال به storage ناموفق بود"),
"test_result": test_result
}
2025-09-21 19:53:21 +03:30
return success_response(data, request)
2025-09-21 21:40:10 +03:30
except ApiError:
raise
2025-09-21 19:53:21 +03:30
except Exception as e:
raise ApiError(
code="TEST_STORAGE_CONFIG_ERROR",
message=translator.t("TEST_STORAGE_CONFIG_ERROR", f"خطا در تست اتصال: {str(e)}"),
http_status=500,
translator=translator
)
2025-09-21 21:40:10 +03:30
# Helper function for testing storage connections
async def _test_storage_connection(config: StorageConfig) -> dict:
"""تست اتصال به storage بر اساس نوع آن"""
import os
import tempfile
from datetime import datetime
try:
if config.storage_type == "local":
return await _test_local_storage(config)
elif config.storage_type == "ftp":
return await _test_ftp_storage(config)
else:
return {
"success": False,
"error": f"نوع storage پشتیبانی نشده: {config.storage_type}",
"tested_at": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"success": False,
"error": str(e),
"tested_at": datetime.utcnow().isoformat()
}
async def _test_local_storage(config: StorageConfig) -> dict:
"""تست اتصال به local storage"""
import os
from datetime import datetime
try:
base_path = config.config_data.get("base_path", "/tmp/hesabix_files")
# بررسی وجود مسیر
if not os.path.exists(base_path):
# تلاش برای ایجاد مسیر
os.makedirs(base_path, exist_ok=True)
# بررسی دسترسی نوشتن
test_file_path = os.path.join(base_path, f"test_connection_{datetime.utcnow().timestamp()}.txt")
# نوشتن فایل تست
with open(test_file_path, "w") as f:
f.write("Test connection file")
# خواندن فایل تست
with open(test_file_path, "r") as f:
content = f.read()
# حذف فایل تست
os.remove(test_file_path)
if content == "Test connection file":
return {
"success": True,
"message": "اتصال به local storage موفقیت‌آمیز بود",
"storage_type": "local",
"base_path": base_path,
"tested_at": datetime.utcnow().isoformat()
}
else:
return {
"success": False,
"error": "خطا در خواندن فایل تست",
"tested_at": datetime.utcnow().isoformat()
}
except PermissionError:
return {
"success": False,
"error": "دسترسی به مسیر ذخیره‌سازی وجود ندارد",
"tested_at": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"success": False,
"error": f"خطا در تست local storage: {str(e)}",
"tested_at": datetime.utcnow().isoformat()
}
async def _test_ftp_storage(config: StorageConfig) -> dict:
"""تست اتصال به FTP storage"""
2025-09-22 11:00:18 +03:30
import ftplib
import tempfile
import os
2025-09-21 21:40:10 +03:30
from datetime import datetime
try:
2025-09-22 11:00:18 +03:30
# دریافت تنظیمات FTP
config_data = config.config_data
host = config_data.get("host")
port = int(config_data.get("port", 21))
username = config_data.get("username")
password = config_data.get("password")
directory = config_data.get("directory", "/")
use_tls = config_data.get("use_tls", False)
# بررسی وجود پارامترهای ضروری
if not all([host, username, password]):
return {
"success": False,
"error": "پارامترهای ضروری FTP (host, username, password) موجود نیست",
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat()
}
# اتصال به FTP
if use_tls:
ftp = ftplib.FTP_TLS()
else:
ftp = ftplib.FTP()
# تنظیم timeout
ftp.connect(host, port, timeout=10)
ftp.login(username, password)
# تغییر به دایرکتوری مورد نظر
if directory and directory != "/":
try:
ftp.cwd(directory)
except ftplib.error_perm:
return {
"success": False,
"error": f"دسترسی به دایرکتوری {directory} وجود ندارد",
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat()
}
# تست نوشتن فایل
test_filename = f"test_connection_{datetime.utcnow().timestamp()}.txt"
test_content = "Test FTP connection file"
# ایجاد فایل موقت
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as temp_file:
temp_file.write(test_content)
temp_file_path = temp_file.name
try:
# آپلود فایل
with open(temp_file_path, 'rb') as file:
ftp.storbinary(f'STOR {test_filename}', file)
# بررسی وجود فایل
file_list = []
ftp.retrlines('LIST', file_list.append)
file_exists = any(test_filename in line for line in file_list)
if not file_exists:
return {
"success": False,
"error": "فایل تست آپلود نشد",
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat()
}
# حذف فایل تست
try:
ftp.delete(test_filename)
except ftplib.error_perm:
pass # اگر نتوانست حذف کند، مهم نیست
# بستن اتصال
ftp.quit()
return {
"success": True,
"message": "اتصال به FTP server موفقیت‌آمیز بود",
"storage_type": "ftp",
"host": host,
"port": port,
"directory": directory,
"use_tls": use_tls,
"tested_at": datetime.utcnow().isoformat()
}
finally:
# حذف فایل موقت
try:
os.unlink(temp_file_path)
except:
pass
except ftplib.error_perm as e:
2025-09-21 21:40:10 +03:30
return {
"success": False,
2025-09-22 11:00:18 +03:30
"error": f"خطا در احراز هویت FTP: {str(e)}",
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat()
}
except ftplib.error_temp as e:
return {
"success": False,
"error": f"خطای موقت FTP: {str(e)}",
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat()
}
except ConnectionRefusedError:
return {
"success": False,
"error": "اتصال به سرور FTP رد شد. بررسی کنید که سرور در حال اجرا باشد",
2025-09-21 21:40:10 +03:30
"storage_type": "ftp",
"tested_at": datetime.utcnow().isoformat()
}
except Exception as e:
return {
"success": False,
"error": f"خطا در تست FTP storage: {str(e)}",
2025-09-22 11:00:18 +03:30
"storage_type": "ftp",
2025-09-21 21:40:10 +03:30
"tested_at": datetime.utcnow().isoformat()
}