chore: add token revocation, caching, concurrency, and WebDAV lock modules
This commit is contained in:
+97
-39
@@ -22,6 +22,12 @@ from ..storage import storage_manager
|
||||
from ..activity import log_activity
|
||||
from ..thumbnails import generate_thumbnail, delete_thumbnail
|
||||
|
||||
try:
|
||||
from ..concurrency.atomic import get_atomic_ops
|
||||
ATOMIC_OPS_AVAILABLE = True
|
||||
except ImportError:
|
||||
ATOMIC_OPS_AVAILABLE = False
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/files",
|
||||
tags=["files"],
|
||||
@@ -70,51 +76,103 @@ async def upload_file(
|
||||
else:
|
||||
parent_folder = None
|
||||
|
||||
existing_file = await File.get_or_none(
|
||||
name=file.filename, parent=parent_folder, owner=current_user, is_deleted=False
|
||||
)
|
||||
if existing_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="File with this name already exists in the current folder",
|
||||
)
|
||||
|
||||
file_content = await file.read()
|
||||
file_size = len(file_content)
|
||||
file_hash = hashlib.sha256(file_content).hexdigest()
|
||||
|
||||
if current_user.used_storage_bytes + file_size > current_user.storage_quota_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_507_INSUFFICIENT_STORAGE,
|
||||
detail="Storage quota exceeded",
|
||||
if ATOMIC_OPS_AVAILABLE:
|
||||
atomic_ops = get_atomic_ops()
|
||||
|
||||
async def check_exists():
|
||||
return await File.get_or_none(
|
||||
name=file.filename, parent=parent_folder, owner=current_user, is_deleted=False
|
||||
)
|
||||
|
||||
async def create_file():
|
||||
file_extension = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"{file_hash}{file_extension}"
|
||||
storage_path = unique_filename
|
||||
|
||||
await storage_manager.save_file(current_user.id, storage_path, file_content)
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(file.filename)
|
||||
if not mime_type:
|
||||
mime_type = "application/octet-stream"
|
||||
|
||||
db_file = await File.create(
|
||||
name=file.filename,
|
||||
path=storage_path,
|
||||
size=file_size,
|
||||
mime_type=mime_type,
|
||||
file_hash=file_hash,
|
||||
owner=current_user,
|
||||
parent=parent_folder,
|
||||
)
|
||||
return db_file, storage_path, mime_type
|
||||
|
||||
quota_result = await atomic_ops.atomic_quota_check_and_update(
|
||||
current_user, file_size, lambda u: u.save()
|
||||
)
|
||||
if not quota_result.allowed:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_507_INSUFFICIENT_STORAGE,
|
||||
detail=f"Storage quota exceeded. Available: {quota_result.remaining} bytes",
|
||||
)
|
||||
|
||||
try:
|
||||
db_file, storage_path, mime_type = await atomic_ops.atomic_file_create(
|
||||
current_user,
|
||||
parent_folder.id if parent_folder else None,
|
||||
file.filename,
|
||||
check_exists,
|
||||
create_file,
|
||||
)
|
||||
except FileExistsError as e:
|
||||
await atomic_ops.atomic_quota_check_and_update(
|
||||
current_user, -file_size, lambda u: u.save()
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e),
|
||||
)
|
||||
else:
|
||||
existing_file = await File.get_or_none(
|
||||
name=file.filename, parent=parent_folder, owner=current_user, is_deleted=False
|
||||
)
|
||||
if existing_file:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="File with this name already exists in the current folder",
|
||||
)
|
||||
|
||||
if current_user.used_storage_bytes + file_size > current_user.storage_quota_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_507_INSUFFICIENT_STORAGE,
|
||||
detail="Storage quota exceeded",
|
||||
)
|
||||
|
||||
file_extension = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"{file_hash}{file_extension}"
|
||||
storage_path = unique_filename
|
||||
|
||||
await storage_manager.save_file(current_user.id, storage_path, file_content)
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(file.filename)
|
||||
if not mime_type:
|
||||
mime_type = "application/octet-stream"
|
||||
|
||||
db_file = await File.create(
|
||||
name=file.filename,
|
||||
path=storage_path,
|
||||
size=file_size,
|
||||
mime_type=mime_type,
|
||||
file_hash=file_hash,
|
||||
owner=current_user,
|
||||
parent=parent_folder,
|
||||
)
|
||||
|
||||
# Generate a unique path for storage
|
||||
file_extension = os.path.splitext(file.filename)[1]
|
||||
unique_filename = f"{file_hash}{file_extension}" # Use hash for unique filename
|
||||
storage_path = unique_filename
|
||||
|
||||
# Save file to storage
|
||||
await storage_manager.save_file(current_user.id, storage_path, file_content)
|
||||
|
||||
# Get mime type
|
||||
mime_type, _ = mimetypes.guess_type(file.filename)
|
||||
if not mime_type:
|
||||
mime_type = "application/octet-stream"
|
||||
|
||||
# Create file entry in database
|
||||
db_file = await File.create(
|
||||
name=file.filename,
|
||||
path=storage_path,
|
||||
size=file_size,
|
||||
mime_type=mime_type,
|
||||
file_hash=file_hash,
|
||||
owner=current_user,
|
||||
parent=parent_folder,
|
||||
)
|
||||
|
||||
current_user.used_storage_bytes += file_size
|
||||
await current_user.save()
|
||||
current_user.used_storage_bytes += file_size
|
||||
await current_user.save()
|
||||
|
||||
thumbnail_path = await generate_thumbnail(storage_path, mime_type, current_user.id)
|
||||
if thumbnail_path:
|
||||
|
||||
@@ -183,7 +183,11 @@ async def update_share(
|
||||
|
||||
|
||||
@router.post("/{share_token}/access")
|
||||
async def access_shared_content(share_token: str, password: Optional[str] = None):
|
||||
async def access_shared_content(
|
||||
share_token: str,
|
||||
password: Optional[str] = None,
|
||||
subfolder_id: Optional[int] = None,
|
||||
):
|
||||
share = await Share.get_or_none(token=share_token)
|
||||
if not share:
|
||||
raise HTTPException(
|
||||
@@ -212,7 +216,37 @@ async def access_shared_content(share_token: str, password: Optional[str] = None
|
||||
result["file"] = await FileOut.from_tortoise_orm(file)
|
||||
result["type"] = "file"
|
||||
elif share.folder_id:
|
||||
folder = await Folder.get_or_none(id=share.folder_id, is_deleted=False)
|
||||
# Start with the shared root folder
|
||||
target_folder_id = share.folder_id
|
||||
|
||||
# If subfolder_id is requested, verify it's a descendant of the shared folder
|
||||
if subfolder_id:
|
||||
subfolder = await Folder.get_or_none(id=subfolder_id, is_deleted=False)
|
||||
if not subfolder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Subfolder not found"
|
||||
)
|
||||
|
||||
# Verify hierarchy
|
||||
current = subfolder
|
||||
is_descendant = False
|
||||
while current.parent_id:
|
||||
if current.parent_id == share.folder_id:
|
||||
is_descendant = True
|
||||
break
|
||||
current = await Folder.get_or_none(id=current.parent_id)
|
||||
if not current:
|
||||
break
|
||||
|
||||
if not is_descendant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this folder"
|
||||
)
|
||||
|
||||
target_folder_id = subfolder_id
|
||||
|
||||
folder = await Folder.get_or_none(id=target_folder_id, is_deleted=False)
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
|
||||
Reference in New Issue
Block a user