mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-11 18:10:10 +08:00
211 lines
7.3 KiB
Python
211 lines
7.3 KiB
Python
import time
|
|
from typing import Optional
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from domain.auth import User
|
|
from .registry import (
|
|
get_config_schemas,
|
|
normalize_adapter_type,
|
|
runtime_registry,
|
|
)
|
|
from .types import AdapterCreate, AdapterOut, AdapterUsage
|
|
from .providers.base import UsageCapableAdapter
|
|
from models import StorageAdapter
|
|
|
|
|
|
class AdapterService:
|
|
_usage_cache_ttl = 3600
|
|
_usage_cache: dict[int, tuple[float, AdapterUsage]] = {}
|
|
|
|
@classmethod
|
|
def _get_cached_usage(cls, adapter_id: int) -> AdapterUsage | None:
|
|
cached = cls._usage_cache.get(adapter_id)
|
|
if not cached:
|
|
return None
|
|
expires_at, usage = cached
|
|
if expires_at <= time.time():
|
|
cls._usage_cache.pop(adapter_id, None)
|
|
return None
|
|
return usage
|
|
|
|
@classmethod
|
|
def _set_cached_usage(cls, usage: AdapterUsage):
|
|
cls._usage_cache[usage.id] = (time.time() + cls._usage_cache_ttl, usage)
|
|
|
|
@classmethod
|
|
def _clear_cached_usage(cls, adapter_id: int):
|
|
cls._usage_cache.pop(adapter_id, None)
|
|
|
|
@classmethod
|
|
def _validate_and_normalize_config(cls, adapter_type: str, cfg):
|
|
schemas = get_config_schemas()
|
|
adapter_type = normalize_adapter_type(adapter_type)
|
|
if not adapter_type:
|
|
raise HTTPException(400, detail="不支持的适配器类型")
|
|
if not isinstance(cfg, dict):
|
|
raise HTTPException(400, detail="config 必须是对象")
|
|
schema = schemas.get(adapter_type)
|
|
if not schema:
|
|
raise HTTPException(400, detail=f"不支持的适配器类型: {adapter_type}")
|
|
out = {}
|
|
missing = []
|
|
for f in schema:
|
|
k = f["key"]
|
|
if k in cfg and cfg[k] not in (None, ""):
|
|
out[k] = cfg[k]
|
|
elif "default" in f:
|
|
out[k] = f["default"]
|
|
elif f.get("required"):
|
|
missing.append(k)
|
|
if missing:
|
|
raise HTTPException(400, detail="缺少必填配置字段: " + ", ".join(missing))
|
|
if adapter_type in ("alist", "openlist"):
|
|
username = out.get("username")
|
|
password = out.get("password")
|
|
if (username and not password) or (password and not username):
|
|
raise HTTPException(400, detail="用户名和密码必须同时填写或同时留空")
|
|
return out
|
|
|
|
@classmethod
|
|
async def create_adapter(cls, data: AdapterCreate, current_user: Optional[User]):
|
|
norm_path = AdapterCreate.normalize_mount_path(data.path)
|
|
exists = await StorageAdapter.get_or_none(path=norm_path)
|
|
if exists:
|
|
raise HTTPException(400, detail="Mount path already exists")
|
|
|
|
adapter_fields = {
|
|
"name": data.name,
|
|
"type": data.type,
|
|
"config": cls._validate_and_normalize_config(data.type, data.config or {}),
|
|
"enabled": data.enabled,
|
|
"path": norm_path,
|
|
"sub_path": data.sub_path,
|
|
}
|
|
|
|
rec = await StorageAdapter.create(**adapter_fields)
|
|
await runtime_registry.upsert(rec)
|
|
return AdapterOut.model_validate(rec)
|
|
|
|
@classmethod
|
|
async def list_adapters(cls):
|
|
adapters = await StorageAdapter.all()
|
|
return [AdapterOut.model_validate(a) for a in adapters]
|
|
|
|
@classmethod
|
|
async def available_adapter_types(cls):
|
|
data = []
|
|
for adapter_type, fields in get_config_schemas().items():
|
|
data.append({
|
|
"type": adapter_type,
|
|
"config_schema": fields,
|
|
})
|
|
return data
|
|
|
|
@classmethod
|
|
async def get_adapter(cls, adapter_id: int):
|
|
rec = await StorageAdapter.get_or_none(id=adapter_id)
|
|
if not rec:
|
|
raise HTTPException(404, detail="Not found")
|
|
return AdapterOut.model_validate(rec)
|
|
|
|
@classmethod
|
|
def _unsupported_usage(cls, rec: StorageAdapter, reason: str) -> AdapterUsage:
|
|
return AdapterUsage(
|
|
id=rec.id,
|
|
name=rec.name,
|
|
type=rec.type,
|
|
path=rec.path,
|
|
supported=False,
|
|
reason=reason,
|
|
)
|
|
|
|
@classmethod
|
|
async def get_adapter_usage(cls, adapter_id: int) -> AdapterUsage:
|
|
rec = await StorageAdapter.get_or_none(id=adapter_id)
|
|
if not rec:
|
|
raise HTTPException(404, detail="Not found")
|
|
return await cls._get_adapter_usage_for_record(rec)
|
|
|
|
@classmethod
|
|
async def _get_adapter_usage_for_record(cls, rec: StorageAdapter) -> AdapterUsage:
|
|
cached = cls._get_cached_usage(rec.id)
|
|
if cached:
|
|
return cached
|
|
|
|
if not rec.enabled:
|
|
return cls._unsupported_usage(rec, "adapter_disabled")
|
|
|
|
adapter = runtime_registry.get(rec.id)
|
|
if not adapter:
|
|
await runtime_registry.refresh()
|
|
adapter = runtime_registry.get(rec.id)
|
|
if not adapter:
|
|
return cls._unsupported_usage(rec, "adapter_unavailable")
|
|
if not isinstance(adapter, UsageCapableAdapter):
|
|
return cls._unsupported_usage(rec, "adapter_not_implemented")
|
|
|
|
root = adapter.get_effective_root(rec.sub_path)
|
|
try:
|
|
raw_usage = await adapter.get_usage(root)
|
|
except Exception as e:
|
|
return cls._unsupported_usage(rec, f"usage_failed: {e}")
|
|
|
|
if not isinstance(raw_usage, dict):
|
|
return cls._unsupported_usage(rec, "invalid_usage_response")
|
|
|
|
usage = AdapterUsage(
|
|
id=rec.id,
|
|
name=rec.name,
|
|
type=rec.type,
|
|
path=rec.path,
|
|
supported=True,
|
|
used_bytes=raw_usage.get("used_bytes"),
|
|
total_bytes=raw_usage.get("total_bytes"),
|
|
free_bytes=raw_usage.get("free_bytes"),
|
|
source=raw_usage.get("source") or rec.type,
|
|
scope=raw_usage.get("scope"),
|
|
)
|
|
cls._set_cached_usage(usage)
|
|
return usage
|
|
|
|
@classmethod
|
|
async def list_adapter_usages(cls):
|
|
adapters = await StorageAdapter.all()
|
|
result = []
|
|
for rec in adapters:
|
|
result.append(await cls._get_adapter_usage_for_record(rec))
|
|
return result
|
|
|
|
@classmethod
|
|
async def update_adapter(cls, adapter_id: int, data: AdapterCreate, current_user: Optional[User]):
|
|
rec = await StorageAdapter.get_or_none(id=adapter_id)
|
|
if not rec:
|
|
raise HTTPException(404, detail="Not found")
|
|
|
|
norm_path = AdapterCreate.normalize_mount_path(data.path)
|
|
existing = await StorageAdapter.get_or_none(path=norm_path)
|
|
if existing and existing.id != adapter_id:
|
|
raise HTTPException(400, detail="Mount path already exists")
|
|
|
|
rec.name = data.name
|
|
rec.type = data.type
|
|
rec.config = cls._validate_and_normalize_config(data.type, data.config or {})
|
|
rec.enabled = data.enabled
|
|
rec.path = norm_path
|
|
rec.sub_path = data.sub_path
|
|
await rec.save()
|
|
|
|
await runtime_registry.upsert(rec)
|
|
cls._clear_cached_usage(adapter_id)
|
|
return AdapterOut.model_validate(rec)
|
|
|
|
@classmethod
|
|
async def delete_adapter(cls, adapter_id: int, current_user: Optional[User]):
|
|
deleted = await StorageAdapter.filter(id=adapter_id).delete()
|
|
if not deleted:
|
|
raise HTTPException(404, detail="Not found")
|
|
runtime_registry.remove(adapter_id)
|
|
cls._clear_cached_usage(adapter_id)
|
|
return {"deleted": True}
|