progress in file storage
This commit is contained in:
parent
754d61e622
commit
bee18daf4a
|
|
@ -2,9 +2,10 @@ from typing import List, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Request
|
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
from adapters.db.session import get_db
|
from adapters.db.session import get_db
|
||||||
from app.core.auth_dependency import get_current_user
|
from app.core.auth_dependency import get_current_user, AuthContext
|
||||||
from app.core.permissions import require_permission
|
from app.core.permissions import require_permission
|
||||||
from app.core.responses import success_response
|
from app.core.responses import success_response
|
||||||
from app.core.responses import ApiError
|
from app.core.responses import ApiError
|
||||||
|
|
@ -12,6 +13,7 @@ from app.core.i18n import locale_dependency
|
||||||
from app.services.file_storage_service import FileStorageService
|
from app.services.file_storage_service import FileStorageService
|
||||||
from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository
|
from adapters.db.repositories.file_storage_repository import StorageConfigRepository, FileStorageRepository
|
||||||
from adapters.db.models.user import User
|
from adapters.db.models.user import User
|
||||||
|
from adapters.db.models.file_storage import StorageConfig
|
||||||
from adapters.api.v1.schema_models.file_storage import (
|
from adapters.api.v1.schema_models.file_storage import (
|
||||||
StorageConfigCreateRequest,
|
StorageConfigCreateRequest,
|
||||||
StorageConfigUpdateRequest,
|
StorageConfigUpdateRequest,
|
||||||
|
|
@ -36,19 +38,85 @@ async def list_all_files(
|
||||||
is_temporary: Optional[bool] = Query(None),
|
is_temporary: Optional[bool] = Query(None),
|
||||||
is_verified: Optional[bool] = Query(None),
|
is_verified: Optional[bool] = Query(None),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.file.view")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""لیست تمام فایلها با فیلتر"""
|
"""لیست تمام فایلها با فیلتر"""
|
||||||
try:
|
try:
|
||||||
file_service = FileStorageService(db)
|
# Check permission
|
||||||
|
if not current_user.has_app_permission("admin.file.view"):
|
||||||
|
raise ApiError(
|
||||||
|
code="FORBIDDEN",
|
||||||
|
message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
|
||||||
|
http_status=403,
|
||||||
|
translator=translator
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: پیادهسازی pagination و فیلترها
|
file_repo = FileStorageRepository(db)
|
||||||
statistics = await file_service.get_storage_statistics()
|
|
||||||
|
# محاسبه 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
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"statistics": statistics,
|
"files": files_data,
|
||||||
"message": translator.t("FILE_LIST_NOT_IMPLEMENTED", "File list endpoint - to be implemented")
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return success_response(data, request)
|
return success_response(data, request)
|
||||||
|
|
@ -65,7 +133,7 @@ async def list_all_files(
|
||||||
async def get_unverified_files(
|
async def get_unverified_files(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.file.view")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""فایلهای تایید نشده"""
|
"""فایلهای تایید نشده"""
|
||||||
|
|
@ -102,7 +170,7 @@ async def get_unverified_files(
|
||||||
async def cleanup_temporary_files(
|
async def cleanup_temporary_files(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.file.cleanup")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""پاکسازی فایلهای موقت"""
|
"""پاکسازی فایلهای موقت"""
|
||||||
|
|
@ -130,7 +198,7 @@ async def force_delete_file(
|
||||||
file_id: UUID,
|
file_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.file.delete")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""حذف اجباری فایل"""
|
"""حذف اجباری فایل"""
|
||||||
|
|
@ -164,7 +232,7 @@ async def restore_file(
|
||||||
file_id: UUID,
|
file_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.file.restore")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""بازیابی فایل حذف شده"""
|
"""بازیابی فایل حذف شده"""
|
||||||
|
|
@ -197,7 +265,7 @@ async def restore_file(
|
||||||
async def get_file_statistics(
|
async def get_file_statistics(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.file.view")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""آمار استفاده از فضای ذخیرهسازی"""
|
"""آمار استفاده از فضای ذخیرهسازی"""
|
||||||
|
|
@ -220,13 +288,22 @@ async def get_file_statistics(
|
||||||
async def get_storage_configs(
|
async def get_storage_configs(
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.storage.view")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""لیست تنظیمات ذخیرهسازی"""
|
"""لیست تنظیمات ذخیرهسازی"""
|
||||||
try:
|
try:
|
||||||
|
# Check permission
|
||||||
|
if not current_user.has_app_permission("admin.storage.view"):
|
||||||
|
raise ApiError(
|
||||||
|
code="FORBIDDEN",
|
||||||
|
message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
|
||||||
|
http_status=403,
|
||||||
|
translator=translator
|
||||||
|
)
|
||||||
|
|
||||||
config_repo = StorageConfigRepository(db)
|
config_repo = StorageConfigRepository(db)
|
||||||
configs = await config_repo.get_all_configs()
|
configs = config_repo.get_all_configs()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"configs": [
|
"configs": [
|
||||||
|
|
@ -236,6 +313,7 @@ async def get_storage_configs(
|
||||||
"storage_type": config.storage_type,
|
"storage_type": config.storage_type,
|
||||||
"is_default": config.is_default,
|
"is_default": config.is_default,
|
||||||
"is_active": config.is_active,
|
"is_active": config.is_active,
|
||||||
|
"config_data": config.config_data,
|
||||||
"created_at": config.created_at.isoformat()
|
"created_at": config.created_at.isoformat()
|
||||||
}
|
}
|
||||||
for config in configs
|
for config in configs
|
||||||
|
|
@ -257,7 +335,7 @@ async def create_storage_config(
|
||||||
request: Request,
|
request: Request,
|
||||||
config_request: StorageConfigCreateRequest,
|
config_request: StorageConfigCreateRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.storage.create")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""ایجاد تنظیمات ذخیرهسازی جدید"""
|
"""ایجاد تنظیمات ذخیرهسازی جدید"""
|
||||||
|
|
@ -268,8 +346,9 @@ async def create_storage_config(
|
||||||
name=config_request.name,
|
name=config_request.name,
|
||||||
storage_type=config_request.storage_type,
|
storage_type=config_request.storage_type,
|
||||||
config_data=config_request.config_data,
|
config_data=config_request.config_data,
|
||||||
created_by=current_user.id,
|
created_by=current_user.get_user_id(),
|
||||||
is_default=config_request.is_default
|
is_default=config_request.is_default,
|
||||||
|
is_active=config_request.is_active
|
||||||
)
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -293,7 +372,7 @@ async def update_storage_config(
|
||||||
request: Request,
|
request: Request,
|
||||||
config_request: StorageConfigUpdateRequest,
|
config_request: StorageConfigUpdateRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.storage.update")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""بروزرسانی تنظیمات ذخیرهسازی"""
|
"""بروزرسانی تنظیمات ذخیرهسازی"""
|
||||||
|
|
@ -317,7 +396,7 @@ async def set_default_storage_config(
|
||||||
config_id: UUID,
|
config_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.storage.update")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""تنظیم به عنوان پیشفرض"""
|
"""تنظیم به عنوان پیشفرض"""
|
||||||
|
|
@ -351,7 +430,7 @@ async def delete_storage_config(
|
||||||
config_id: UUID,
|
config_id: UUID,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.storage.delete")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""حذف تنظیمات ذخیرهسازی"""
|
"""حذف تنظیمات ذخیرهسازی"""
|
||||||
|
|
@ -382,17 +461,42 @@ async def delete_storage_config(
|
||||||
|
|
||||||
@router.post("/storage-configs/{config_id}/test", response_model=dict)
|
@router.post("/storage-configs/{config_id}/test", response_model=dict)
|
||||||
async def test_storage_config(
|
async def test_storage_config(
|
||||||
config_id: UUID,
|
config_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(require_permission("admin.storage.test")),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
translator = Depends(locale_dependency)
|
translator = Depends(locale_dependency)
|
||||||
):
|
):
|
||||||
"""تست اتصال به storage"""
|
"""تست اتصال به storage"""
|
||||||
try:
|
try:
|
||||||
# TODO: پیادهسازی تست اتصال
|
config_repo = StorageConfigRepository(db)
|
||||||
data = {"message": translator.t("STORAGE_CONNECTION_TEST_NOT_IMPLEMENTED", "Storage connection test - to be implemented")}
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return success_response(data, request)
|
return success_response(data, request)
|
||||||
|
except ApiError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
code="TEST_STORAGE_CONFIG_ERROR",
|
code="TEST_STORAGE_CONFIG_ERROR",
|
||||||
|
|
@ -400,3 +504,107 @@ async def test_storage_config(
|
||||||
http_status=500,
|
http_status=500,
|
||||||
translator=translator
|
translator=translator
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 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"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
try:
|
||||||
|
# TODO: پیادهسازی تست FTP
|
||||||
|
# فعلاً فقط ساختار کلی را برمیگردانیم
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "تست FTP هنوز پیادهسازی نشده است",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"خطا در تست FTP storage: {str(e)}",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class StorageConfigCreateRequest(BaseModel):
|
||||||
storage_type: str = Field(..., description="نوع ذخیرهسازی")
|
storage_type: str = Field(..., description="نوع ذخیرهسازی")
|
||||||
config_data: Dict[str, Any] = Field(..., description="دادههای پیکربندی")
|
config_data: Dict[str, Any] = Field(..., description="دادههای پیکربندی")
|
||||||
is_default: bool = Field(default=False, description="آیا پیشفرض است")
|
is_default: bool = Field(default=False, description="آیا پیشفرض است")
|
||||||
|
is_active: bool = Field(default=True, description="آیا فعال است")
|
||||||
|
|
||||||
|
|
||||||
class StorageConfigUpdateRequest(BaseModel):
|
class StorageConfigUpdateRequest(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from adapters.db.repositories.base_repo import BaseRepository
|
||||||
|
|
||||||
class FileStorageRepository(BaseRepository[FileStorage]):
|
class FileStorageRepository(BaseRepository[FileStorage]):
|
||||||
def __init__(self, db: Session):
|
def __init__(self, db: Session):
|
||||||
super().__init__(FileStorage, db)
|
super().__init__(db, FileStorage)
|
||||||
|
|
||||||
async def create_file(
|
async def create_file(
|
||||||
self,
|
self,
|
||||||
|
|
@ -177,15 +177,16 @@ class FileStorageRepository(BaseRepository[FileStorage]):
|
||||||
|
|
||||||
class StorageConfigRepository(BaseRepository[StorageConfig]):
|
class StorageConfigRepository(BaseRepository[StorageConfig]):
|
||||||
def __init__(self, db: Session):
|
def __init__(self, db: Session):
|
||||||
super().__init__(StorageConfig, db)
|
super().__init__(db, StorageConfig)
|
||||||
|
|
||||||
async def create_config(
|
async def create_config(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
storage_type: str,
|
storage_type: str,
|
||||||
config_data: Dict,
|
config_data: Dict,
|
||||||
created_by: UUID,
|
created_by: int,
|
||||||
is_default: bool = False
|
is_default: bool = False,
|
||||||
|
is_active: bool = True
|
||||||
) -> StorageConfig:
|
) -> StorageConfig:
|
||||||
# اگر این config به عنوان پیشفرض تنظیم میشود، بقیه را غیرفعال کن
|
# اگر این config به عنوان پیشفرض تنظیم میشود، بقیه را غیرفعال کن
|
||||||
if is_default:
|
if is_default:
|
||||||
|
|
@ -196,7 +197,8 @@ class StorageConfigRepository(BaseRepository[StorageConfig]):
|
||||||
storage_type=storage_type,
|
storage_type=storage_type,
|
||||||
config_data=config_data,
|
config_data=config_data,
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
is_default=is_default
|
is_default=is_default,
|
||||||
|
is_active=is_active
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.add(storage_config)
|
self.db.add(storage_config)
|
||||||
|
|
@ -212,7 +214,7 @@ class StorageConfigRepository(BaseRepository[StorageConfig]):
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
async def get_all_configs(self) -> List[StorageConfig]:
|
def get_all_configs(self) -> List[StorageConfig]:
|
||||||
return self.db.query(StorageConfig).filter(
|
return self.db.query(StorageConfig).filter(
|
||||||
StorageConfig.is_active == True
|
StorageConfig.is_active == True
|
||||||
).order_by(desc(StorageConfig.created_at)).all()
|
).order_by(desc(StorageConfig.created_at)).all()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import '../../../core/api_client.dart';
|
||||||
|
|
||||||
class FileManagementWidget extends StatefulWidget {
|
class FileManagementWidget extends StatefulWidget {
|
||||||
const FileManagementWidget({super.key});
|
const FileManagementWidget({super.key});
|
||||||
|
|
@ -36,49 +37,26 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to load files
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
|
||||||
|
|
||||||
setState(() {
|
// Call API to load files
|
||||||
_allFiles = [
|
final response = await api.get('/api/v1/admin/files/');
|
||||||
{
|
final unverifiedResponse = await api.get('/api/v1/admin/files/unverified');
|
||||||
'id': '1',
|
|
||||||
'original_name': 'document.pdf',
|
if (response.data != null && response.data['success'] == true) {
|
||||||
'file_size': 1024000,
|
final files = response.data['data']['files'] as List<dynamic>;
|
||||||
'mime_type': 'application/pdf',
|
final unverifiedFiles = unverifiedResponse.data != null && unverifiedResponse.data['success'] == true
|
||||||
'module_context': 'tickets',
|
? unverifiedResponse.data['data']['unverified_files'] as List<dynamic>
|
||||||
'created_at': '2024-01-01T10:00:00Z',
|
: <dynamic>[];
|
||||||
'expires_at': null,
|
|
||||||
'is_temporary': false,
|
|
||||||
'is_verified': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'original_name': 'image.jpg',
|
|
||||||
'file_size': 512000,
|
|
||||||
'mime_type': 'image/jpeg',
|
|
||||||
'module_context': 'accounting',
|
|
||||||
'created_at': '2024-01-02T11:00:00Z',
|
|
||||||
'expires_at': '2024-01-09T11:00:00Z',
|
|
||||||
'is_temporary': true,
|
|
||||||
'is_verified': false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'original_name': 'spreadsheet.xlsx',
|
|
||||||
'file_size': 256000,
|
|
||||||
'mime_type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
'module_context': 'reports',
|
|
||||||
'created_at': '2024-01-03T12:00:00Z',
|
|
||||||
'expires_at': null,
|
|
||||||
'is_temporary': false,
|
|
||||||
'is_verified': true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
_unverifiedFiles = _allFiles.where((file) => file['is_verified'] == false).toList();
|
setState(() {
|
||||||
_isLoading = false;
|
_allFiles = files.cast<Map<String, dynamic>>();
|
||||||
});
|
_unverifiedFiles = unverifiedFiles.cast<Map<String, dynamic>>();
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data?['message'] ?? 'خطا در دریافت فایلها');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
|
|
@ -87,8 +65,9 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _forceDeleteFile(String fileId) async {
|
Future<void> _forceDeleteFile(String fileId) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
|
|
@ -112,17 +91,21 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to force delete file
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
final response = await api.delete('/api/v1/admin/files/$fileId');
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (response.data != null && response.data['success'] == true) {
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(l10n.fileDeleted),
|
SnackBar(
|
||||||
backgroundColor: Colors.green,
|
content: Text(l10n.fileDeleted),
|
||||||
),
|
backgroundColor: Colors.green,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
_loadFiles();
|
|
||||||
|
_loadFiles();
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data?['message'] ?? 'خطا در حذف فایل');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
@ -135,7 +118,7 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _restoreFile(String fileId) async {
|
Future<void> _restoreFile(String fileId) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
|
|
@ -156,17 +139,21 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to restore file
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
final response = await api.put('/api/v1/admin/files/$fileId/restore');
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (response.data != null && response.data['success'] == true) {
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(l10n.fileRestored),
|
SnackBar(
|
||||||
backgroundColor: Colors.green,
|
content: Text(l10n.fileRestored),
|
||||||
),
|
backgroundColor: Colors.green,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
_loadFiles();
|
|
||||||
|
_loadFiles();
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data?['message'] ?? 'خطا در بازیابی فایل');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
@ -192,8 +179,7 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -224,7 +210,7 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilesList(List<Map<String, dynamic>> files) {
|
Widget _buildFilesList(List<Map<String, dynamic>> files) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
|
|
@ -337,7 +323,7 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.delete, color: theme.colorScheme.error),
|
Icon(Icons.delete, color: theme.colorScheme.error),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(AppLocalizations.of(context)!.forceDelete),
|
Text(AppLocalizations.of(context).forceDelete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -347,7 +333,7 @@ class _FileManagementWidgetState extends State<FileManagementWidget>
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.restore, color: theme.colorScheme.primary),
|
Icon(Icons.restore, color: theme.colorScheme.primary),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(AppLocalizations.of(context)!.restoreFile),
|
Text(AppLocalizations.of(context).restoreFile),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import '../../../core/api_client.dart';
|
||||||
|
|
||||||
class FileStatisticsWidget extends StatefulWidget {
|
class FileStatisticsWidget extends StatefulWidget {
|
||||||
const FileStatisticsWidget({super.key});
|
const FileStatisticsWidget({super.key});
|
||||||
|
|
@ -26,18 +27,17 @@ class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to load statistics
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
final response = await api.get('/api/v1/admin/files/statistics');
|
||||||
|
|
||||||
setState(() {
|
if (response.data != null && response.data['success'] == true) {
|
||||||
_statistics = {
|
setState(() {
|
||||||
'total_files': 1250,
|
_statistics = response.data['data'];
|
||||||
'total_size': 2048576000, // 2GB in bytes
|
_isLoading = false;
|
||||||
'temporary_files': 45,
|
});
|
||||||
'unverified_files': 12,
|
} else {
|
||||||
};
|
throw Exception(response.data?['message'] ?? 'خطا در دریافت آمار');
|
||||||
_isLoading = false;
|
}
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
|
|
@ -46,8 +46,9 @@ class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _cleanupTemporaryFiles() async {
|
Future<void> _cleanupTemporaryFiles() async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
|
|
@ -68,17 +69,21 @@ class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to cleanup temporary files
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 2)); // Simulate API call
|
final response = await api.post('/api/v1/admin/files/cleanup-temporary');
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (response.data != null && response.data['success'] == true) {
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(l10n.cleanupCompleted),
|
SnackBar(
|
||||||
backgroundColor: Colors.green,
|
content: Text(l10n.cleanupCompleted),
|
||||||
),
|
backgroundColor: Colors.green,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
_loadStatistics();
|
|
||||||
|
_loadStatistics();
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data?['message'] ?? 'خطا در پاکسازی فایلهای موقت');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
@ -90,6 +95,7 @@ class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
String _formatFileSize(int bytes) {
|
String _formatFileSize(int bytes) {
|
||||||
if (bytes < 1024) return '$bytes B';
|
if (bytes < 1024) return '$bytes B';
|
||||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
|
|
@ -99,7 +105,7 @@ class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
|
|
|
||||||
|
|
@ -19,165 +19,129 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isDefault = config['is_default'] == true;
|
final isDefault = config['is_default'] == true;
|
||||||
final isActive = config['is_active'] == true;
|
final isActive = config['is_active'] == true;
|
||||||
final storageType = config['storage_type'] as String;
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Header
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
storageType == 'local' ? Icons.storage : Icons.cloud_upload,
|
_getStorageIcon(config['storage_type']),
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
|
size: 24,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Column(
|
||||||
config['name'] ?? '',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
Text(
|
||||||
),
|
config['name'] ?? 'Unknown',
|
||||||
),
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
if (isDefault)
|
),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
l10n.isDefault,
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
),
|
Text(
|
||||||
if (!isActive)
|
_getStorageTypeName(config['storage_type']),
|
||||||
Container(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
margin: const EdgeInsets.only(left: 8),
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Inactive',
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onError,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Status badges
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (isDefault)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n.isDefault,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive ? Colors.green : Colors.red,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isActive ? l10n.isActive : 'غیرفعال',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
l10n.storageType,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
storageType == 'local' ? l10n.localStorage : l10n.ftpStorage,
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
|
||||||
if (storageType == 'local') ...[
|
const SizedBox(height: 16),
|
||||||
Row(
|
|
||||||
children: [
|
// Configuration details
|
||||||
Icon(
|
_buildConfigDetails(context, config),
|
||||||
Icons.folder_outlined,
|
|
||||||
size: 16,
|
const SizedBox(height: 16),
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
// Actions
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
l10n.basePath,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
config['config_data']?['base_path'] ?? '',
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
] else if (storageType == 'ftp') ...[
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.cloud_outlined,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${l10n.ftpHost}: ${config['config_data']?['host'] ?? ''}',
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.person_outline,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${l10n.ftpUsername}: ${config['config_data']?['username'] ?? ''}',
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (onEdit != null)
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: onEdit,
|
|
||||||
icon: const Icon(Icons.edit, size: 16),
|
|
||||||
label: Text(l10n.edit),
|
|
||||||
),
|
|
||||||
if (onTestConnection != null)
|
if (onTestConnection != null)
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onTestConnection,
|
onPressed: onTestConnection,
|
||||||
icon: const Icon(Icons.wifi_protected_setup, size: 16),
|
icon: const Icon(Icons.wifi_protected_setup, size: 16),
|
||||||
label: Text(l10n.testConnection),
|
label: Text(l10n.testConnection),
|
||||||
),
|
),
|
||||||
if (onSetDefault != null)
|
if (onEdit != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: onEdit,
|
||||||
|
icon: const Icon(Icons.edit, size: 16),
|
||||||
|
label: Text(l10n.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (onSetDefault != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onSetDefault,
|
onPressed: onSetDefault,
|
||||||
icon: const Icon(Icons.star, size: 16),
|
icon: const Icon(Icons.star, size: 16),
|
||||||
label: Text(l10n.setAsDefault),
|
label: Text(l10n.setAsDefault),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (onDelete != null)
|
],
|
||||||
|
if (onDelete != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onDelete,
|
onPressed: onDelete,
|
||||||
icon: const Icon(Icons.delete, size: 16),
|
icon: const Icon(Icons.delete, size: 16),
|
||||||
|
|
@ -186,6 +150,7 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
foregroundColor: theme.colorScheme.error,
|
foregroundColor: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -193,4 +158,116 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Widget _buildConfigDetails(BuildContext context, Map<String, dynamic> config) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final configData = config['config_data'] ?? {};
|
||||||
|
final storageType = config['storage_type'];
|
||||||
|
|
||||||
|
if (storageType == 'local') {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDetailRow(
|
||||||
|
context,
|
||||||
|
l10n.basePath,
|
||||||
|
configData['base_path'] ?? 'N/A',
|
||||||
|
Icons.folder,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (storageType == 'ftp') {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDetailRow(
|
||||||
|
context,
|
||||||
|
l10n.ftpHost,
|
||||||
|
configData['host'] ?? 'N/A',
|
||||||
|
Icons.dns,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildDetailRow(
|
||||||
|
context,
|
||||||
|
l10n.ftpPort,
|
||||||
|
configData['port']?.toString() ?? 'N/A',
|
||||||
|
Icons.settings_ethernet,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildDetailRow(
|
||||||
|
context,
|
||||||
|
l10n.ftpUsername,
|
||||||
|
configData['username'] ?? 'N/A',
|
||||||
|
Icons.person,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildDetailRow(
|
||||||
|
context,
|
||||||
|
l10n.ftpDirectory,
|
||||||
|
configData['directory'] ?? 'N/A',
|
||||||
|
Icons.folder,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailRow(
|
||||||
|
BuildContext context,
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
IconData icon,
|
||||||
|
) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'$label: ',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getStorageIcon(String storageType) {
|
||||||
|
switch (storageType) {
|
||||||
|
case 'local':
|
||||||
|
return Icons.storage;
|
||||||
|
case 'ftp':
|
||||||
|
return Icons.cloud_upload;
|
||||||
|
default:
|
||||||
|
return Icons.storage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getStorageTypeName(String storageType) {
|
||||||
|
switch (storageType) {
|
||||||
|
case 'local':
|
||||||
|
return 'Local Storage';
|
||||||
|
case 'ftp':
|
||||||
|
return 'FTP Storage';
|
||||||
|
default:
|
||||||
|
return 'Unknown Storage';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import '../../../core/api_client.dart';
|
||||||
|
|
||||||
class StorageConfigFormDialog extends StatefulWidget {
|
class StorageConfigFormDialog extends StatefulWidget {
|
||||||
final Map<String, dynamic>? config;
|
final Map<String, dynamic>? config;
|
||||||
|
|
@ -94,19 +95,24 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to save configuration
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
final response = await api.post(
|
||||||
|
'/api/v1/admin/files/storage-configs/',
|
||||||
|
data: {
|
||||||
|
'name': _nameController.text,
|
||||||
|
'storage_type': _selectedStorageType,
|
||||||
|
'is_default': _isDefault,
|
||||||
|
'is_active': _isActive,
|
||||||
|
'config_data': _buildConfigData(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final configData = {
|
if (response.data != null && response.data['success'] == true) {
|
||||||
'name': _nameController.text,
|
if (mounted) {
|
||||||
'storage_type': _selectedStorageType,
|
Navigator.of(context).pop(response.data['data']);
|
||||||
'is_default': _isDefault,
|
}
|
||||||
'is_active': _isActive,
|
} else {
|
||||||
'config_data': _buildConfigData(),
|
throw Exception(response.data?['message'] ?? 'خطا در ذخیره تنظیمات');
|
||||||
};
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context).pop(configData);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -128,7 +134,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isEditing = widget.config != null;
|
final isEditing = widget.config != null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart';
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart';
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart';
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart';
|
||||||
|
import '../../../core/api_client.dart';
|
||||||
|
|
||||||
class StorageConfigListWidget extends StatefulWidget {
|
class StorageConfigListWidget extends StatefulWidget {
|
||||||
const StorageConfigListWidget({super.key});
|
const StorageConfigListWidget({super.key});
|
||||||
|
|
@ -28,41 +29,18 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to load storage configurations
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
final response = await api.get('/api/v1/admin/files/storage-configs/');
|
||||||
|
|
||||||
// Mock data for now
|
if (response.data != null && response.data['success'] == true) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_storageConfigs = [
|
_storageConfigs = (response.data['data']['configs'] as List<dynamic>)
|
||||||
{
|
.cast<Map<String, dynamic>>();
|
||||||
'id': '1',
|
_isLoading = false;
|
||||||
'name': 'Local Storage Default',
|
});
|
||||||
'storage_type': 'local',
|
} else {
|
||||||
'is_default': true,
|
throw Exception(response.data?['message'] ?? 'خطا در دریافت تنظیمات ذخیرهسازی');
|
||||||
'is_active': true,
|
}
|
||||||
'config_data': {
|
|
||||||
'base_path': '/var/hesabix/files'
|
|
||||||
},
|
|
||||||
'created_at': '2024-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'name': 'FTP Backup',
|
|
||||||
'storage_type': 'ftp',
|
|
||||||
'is_default': false,
|
|
||||||
'is_active': true,
|
|
||||||
'config_data': {
|
|
||||||
'host': 'ftp.example.com',
|
|
||||||
'port': 21,
|
|
||||||
'username': 'hesabix',
|
|
||||||
'password': '***',
|
|
||||||
'directory': '/hesabix/files'
|
|
||||||
},
|
|
||||||
'created_at': '2024-01-02T00:00:00Z',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
|
|
@ -71,6 +49,7 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _addStorageConfig() async {
|
Future<void> _addStorageConfig() async {
|
||||||
final result = await showDialog<Map<String, dynamic>>(
|
final result = await showDialog<Map<String, dynamic>>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -94,7 +73,7 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setAsDefault(String configId) async {
|
Future<void> _setAsDefault(String configId) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to set as default
|
// TODO: Call API to set as default
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
||||||
|
|
@ -118,29 +97,44 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _testConnection(String configId) async {
|
Future<void> _testConnection(String configId) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to test connection
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 2)); // Simulate API call
|
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (response.data != null && response.data['success'] == true) {
|
||||||
SnackBar(
|
final testResult = response.data['data']['test_result'];
|
||||||
content: Text(l10n.connectionSuccessful),
|
if (testResult['success'] == true) {
|
||||||
backgroundColor: Colors.green,
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
),
|
SnackBar(
|
||||||
);
|
content: Text(l10n.connectionSuccessful),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('${l10n.connectionFailed}: ${testResult['error']}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data?['message'] ?? 'خطا در تست اتصال');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(l10n.connectionFailed),
|
content: Text('${l10n.connectionFailed}: $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _deleteConfig(String configId) async {
|
Future<void> _deleteConfig(String configId) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
|
|
@ -185,7 +179,7 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue