chore: reformat code style with consistent quotes, trailing commas, and spacing across auth, billing, and models
This commit is contained in:
+40
-18
@@ -1,4 +1,4 @@
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
@@ -13,18 +13,25 @@ router = APIRouter(
|
||||
responses={403: {"description": "Not enough permissions"}},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/users", response_model=List[User_Pydantic])
|
||||
async def get_all_users():
|
||||
return await User.all()
|
||||
|
||||
|
||||
@router.get("/users/{user_id}", response_model=User_Pydantic)
|
||||
async def get_user(user_id: int):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
return user
|
||||
|
||||
@router.post("/users", response_model=User_Pydantic, status_code=status.HTTP_201_CREATED)
|
||||
|
||||
@router.post(
|
||||
"/users", response_model=User_Pydantic, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def create_user_by_admin(user_in: UserCreate):
|
||||
user = await User.get_or_none(username=user_in.username)
|
||||
if user:
|
||||
@@ -44,66 +51,81 @@ async def create_user_by_admin(user_in: UserCreate):
|
||||
username=user_in.username,
|
||||
email=user_in.email,
|
||||
hashed_password=hashed_password,
|
||||
is_superuser=False, # Admin creates regular users by default
|
||||
is_superuser=False, # Admin creates regular users by default
|
||||
is_active=True,
|
||||
)
|
||||
return await User_Pydantic.from_tortoise_orm(user)
|
||||
|
||||
|
||||
@router.put("/users/{user_id}", response_model=User_Pydantic)
|
||||
async def update_user_by_admin(user_id: int, user_update: UserAdminUpdate):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
|
||||
if user_update.username is not None and user_update.username != user.username:
|
||||
if await User.get_or_none(username=user_update.username):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken"
|
||||
)
|
||||
user.username = user_update.username
|
||||
|
||||
|
||||
if user_update.email is not None and user_update.email != user.email:
|
||||
if await User.get_or_none(email=user_update.email):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email already registered",
|
||||
)
|
||||
user.email = user_update.email
|
||||
|
||||
if user_update.password is not None:
|
||||
user.hashed_password = get_password_hash(user_update.password)
|
||||
|
||||
|
||||
if user_update.is_active is not None:
|
||||
user.is_active = user_update.is_active
|
||||
|
||||
|
||||
if user_update.is_superuser is not None:
|
||||
user.is_superuser = user_update.is_superuser
|
||||
|
||||
|
||||
if user_update.storage_quota_bytes is not None:
|
||||
user.storage_quota_bytes = user_update.storage_quota_bytes
|
||||
|
||||
|
||||
if user_update.plan_type is not None:
|
||||
user.plan_type = user_update.plan_type
|
||||
|
||||
|
||||
if user_update.is_2fa_enabled is not None:
|
||||
user.is_2fa_enabled = user_update.is_2fa_enabled
|
||||
if not user_update.is_2fa_enabled:
|
||||
user.two_factor_secret = None
|
||||
user.recovery_codes = None
|
||||
|
||||
|
||||
await user.save()
|
||||
return await User_Pydantic.from_tortoise_orm(user)
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user_by_admin(user_id: int):
|
||||
user = await User.get_or_none(id=user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
await user.delete()
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/test-email")
|
||||
async def send_test_email(to_email: str, subject: str = "Test Email", body: str = "This is a test email"):
|
||||
async def send_test_email(
|
||||
to_email: str, subject: str = "Test Email", body: str = "This is a test email"
|
||||
):
|
||||
from ..mail import queue_email
|
||||
|
||||
queue_email(
|
||||
to_email=to_email,
|
||||
subject=subject,
|
||||
body=body,
|
||||
html=f"<h1>{subject}</h1><p>{body}</p>"
|
||||
html=f"<h1>{subject}</h1><p>{body}</p>",
|
||||
)
|
||||
return {"message": "Test email queued"}
|
||||
return {"message": "Test email queued"}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import List
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -8,21 +7,25 @@ from ..models import User
|
||||
from ..billing.models import PricingConfig, Invoice, SubscriptionPlan
|
||||
from ..billing.invoice_generator import InvoiceGenerator
|
||||
|
||||
|
||||
def require_superuser(current_user: User = Depends(get_current_user)):
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="Superuser privileges required")
|
||||
return current_user
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/admin/billing",
|
||||
tags=["admin", "billing"],
|
||||
dependencies=[Depends(require_superuser)]
|
||||
dependencies=[Depends(require_superuser)],
|
||||
)
|
||||
|
||||
|
||||
class PricingConfigUpdate(BaseModel):
|
||||
config_key: str
|
||||
config_value: float
|
||||
|
||||
|
||||
class PlanCreate(BaseModel):
|
||||
name: str
|
||||
display_name: str
|
||||
@@ -32,6 +35,7 @@ class PlanCreate(BaseModel):
|
||||
price_monthly: float
|
||||
price_yearly: float = None
|
||||
|
||||
|
||||
@router.get("/pricing")
|
||||
async def get_all_pricing(current_user: User = Depends(require_superuser)):
|
||||
configs = await PricingConfig.all()
|
||||
@@ -42,16 +46,17 @@ async def get_all_pricing(current_user: User = Depends(require_superuser)):
|
||||
"config_value": float(c.config_value),
|
||||
"description": c.description,
|
||||
"unit": c.unit,
|
||||
"updated_at": c.updated_at
|
||||
"updated_at": c.updated_at,
|
||||
}
|
||||
for c in configs
|
||||
]
|
||||
|
||||
|
||||
@router.put("/pricing/{config_id}")
|
||||
async def update_pricing(
|
||||
config_id: int,
|
||||
update: PricingConfigUpdate,
|
||||
current_user: User = Depends(require_superuser)
|
||||
current_user: User = Depends(require_superuser),
|
||||
):
|
||||
config = await PricingConfig.get_or_none(id=config_id)
|
||||
if not config:
|
||||
@@ -63,11 +68,10 @@ async def update_pricing(
|
||||
|
||||
return {"message": "Pricing updated successfully"}
|
||||
|
||||
|
||||
@router.post("/generate-invoices/{year}/{month}")
|
||||
async def generate_all_invoices(
|
||||
year: int,
|
||||
month: int,
|
||||
current_user: User = Depends(require_superuser)
|
||||
year: int, month: int, current_user: User = Depends(require_superuser)
|
||||
):
|
||||
users = await User.filter(is_active=True).all()
|
||||
generated = []
|
||||
@@ -76,35 +80,36 @@ async def generate_all_invoices(
|
||||
for user in users:
|
||||
invoice = await InvoiceGenerator.generate_monthly_invoice(user, year, month)
|
||||
if invoice:
|
||||
generated.append({
|
||||
"user_id": user.id,
|
||||
"invoice_id": invoice.id,
|
||||
"total": float(invoice.total)
|
||||
})
|
||||
generated.append(
|
||||
{
|
||||
"user_id": user.id,
|
||||
"invoice_id": invoice.id,
|
||||
"total": float(invoice.total),
|
||||
}
|
||||
)
|
||||
else:
|
||||
skipped.append(user.id)
|
||||
|
||||
return {
|
||||
"generated": len(generated),
|
||||
"skipped": len(skipped),
|
||||
"invoices": generated
|
||||
}
|
||||
return {"generated": len(generated), "skipped": len(skipped), "invoices": generated}
|
||||
|
||||
|
||||
@router.post("/plans")
|
||||
async def create_plan(
|
||||
plan_data: PlanCreate,
|
||||
current_user: User = Depends(require_superuser)
|
||||
plan_data: PlanCreate, current_user: User = Depends(require_superuser)
|
||||
):
|
||||
plan = await SubscriptionPlan.create(**plan_data.dict())
|
||||
return {"id": plan.id, "message": "Plan created successfully"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_billing_stats(current_user: User = Depends(require_superuser)):
|
||||
from tortoise.functions import Sum, Count
|
||||
from tortoise.functions import Sum
|
||||
|
||||
total_revenue = await Invoice.filter(status="paid").annotate(
|
||||
total_sum=Sum("total")
|
||||
).values("total_sum")
|
||||
total_revenue = (
|
||||
await Invoice.filter(status="paid")
|
||||
.annotate(total_sum=Sum("total"))
|
||||
.values("total_sum")
|
||||
)
|
||||
|
||||
invoice_count = await Invoice.all().count()
|
||||
pending_invoices = await Invoice.filter(status="open").count()
|
||||
@@ -112,5 +117,5 @@ async def get_billing_stats(current_user: User = Depends(require_superuser)):
|
||||
return {
|
||||
"total_revenue": float(total_revenue[0]["total_sum"] or 0),
|
||||
"total_invoices": invoice_count,
|
||||
"pending_invoices": pending_invoices
|
||||
"pending_invoices": pending_invoices,
|
||||
}
|
||||
|
||||
+89
-27
@@ -4,13 +4,23 @@ from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..auth import authenticate_user, create_access_token, get_password_hash, get_current_user, get_current_verified_user, verify_password
|
||||
from ..auth import (
|
||||
authenticate_user,
|
||||
create_access_token,
|
||||
get_password_hash,
|
||||
get_current_user,
|
||||
get_current_verified_user,
|
||||
verify_password,
|
||||
)
|
||||
from ..models import User
|
||||
from ..schemas import Token, UserCreate, TokenData, UserLoginWith2FA
|
||||
from ..schemas import Token, UserCreate
|
||||
from ..two_factor import (
|
||||
generate_totp_secret, generate_totp_uri, generate_qr_code_base64,
|
||||
verify_totp_code, generate_recovery_codes, hash_recovery_codes,
|
||||
verify_recovery_codes
|
||||
generate_totp_secret,
|
||||
generate_totp_uri,
|
||||
generate_qr_code_base64,
|
||||
verify_totp_code,
|
||||
generate_recovery_codes,
|
||||
hash_recovery_codes,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
@@ -18,27 +28,33 @@ router = APIRouter(
|
||||
tags=["auth"],
|
||||
)
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class TwoFactorLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
two_factor_code: Optional[str] = None
|
||||
|
||||
|
||||
class TwoFactorSetupResponse(BaseModel):
|
||||
secret: str
|
||||
qr_code_base64: str
|
||||
recovery_codes: List[str]
|
||||
|
||||
|
||||
class TwoFactorCode(BaseModel):
|
||||
two_factor_code: str
|
||||
|
||||
|
||||
class TwoFactorDisable(BaseModel):
|
||||
password: str
|
||||
two_factor_code: str
|
||||
|
||||
|
||||
@router.post("/register", response_model=Token)
|
||||
async def register_user(user_in: UserCreate):
|
||||
user = await User.get_or_none(username=user_in.username)
|
||||
@@ -63,22 +79,26 @@ async def register_user(user_in: UserCreate):
|
||||
|
||||
# Send welcome email
|
||||
from ..mail import queue_email
|
||||
|
||||
queue_email(
|
||||
to_email=user.email,
|
||||
subject="Welcome to MyWebdav!",
|
||||
body=f"Hi {user.username},\n\nWelcome to MyWebdav! Your account has been created successfully.\n\nBest regards,\nThe MyWebdav Team",
|
||||
html=f"<h1>Welcome to MyWebdav!</h1><p>Hi {user.username},</p><p>Welcome to MyWebdav! Your account has been created successfully.</p><p>Best regards,<br>The MyWebdav Team</p>"
|
||||
html=f"<h1>Welcome to MyWebdav!</h1><p>Hi {user.username},</p><p>Welcome to MyWebdav! Your account has been created successfully.</p><p>Best regards,<br>The MyWebdav Team</p>",
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=30) # Use settings
|
||||
access_token_expires = timedelta(minutes=30) # Use settings
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
async def login_for_access_token(login_data: LoginRequest):
|
||||
auth_result = await authenticate_user(login_data.username, login_data.password, None)
|
||||
auth_result = await authenticate_user(
|
||||
login_data.username, login_data.password, None
|
||||
)
|
||||
|
||||
if not auth_result:
|
||||
raise HTTPException(
|
||||
@@ -97,16 +117,26 @@ async def login_for_access_token(login_data: LoginRequest):
|
||||
|
||||
access_token_expires = timedelta(minutes=30)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires, two_factor_verified=True
|
||||
data={"sub": user.username},
|
||||
expires_delta=access_token_expires,
|
||||
two_factor_verified=True,
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/2fa/setup", response_model=TwoFactorSetupResponse)
|
||||
async def setup_two_factor_authentication(current_user: User = Depends(get_current_user)):
|
||||
async def setup_two_factor_authentication(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if current_user.is_2fa_enabled:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is already enabled.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is already enabled."
|
||||
)
|
||||
if current_user.two_factor_secret:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="2FA setup already initiated. Verify or disable first.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="2FA setup already initiated. Verify or disable first.",
|
||||
)
|
||||
|
||||
secret = generate_totp_secret()
|
||||
current_user.two_factor_secret = secret
|
||||
@@ -120,39 +150,66 @@ async def setup_two_factor_authentication(current_user: User = Depends(get_curre
|
||||
current_user.recovery_codes = ",".join(hashed_recovery_codes)
|
||||
await current_user.save()
|
||||
|
||||
return TwoFactorSetupResponse(secret=secret, qr_code_base64=qr_code_base64, recovery_codes=recovery_codes)
|
||||
return TwoFactorSetupResponse(
|
||||
secret=secret, qr_code_base64=qr_code_base64, recovery_codes=recovery_codes
|
||||
)
|
||||
|
||||
|
||||
@router.post("/2fa/verify", response_model=Token)
|
||||
async def verify_two_factor_authentication(two_factor_code_data: TwoFactorCode, current_user: User = Depends(get_current_user)):
|
||||
async def verify_two_factor_authentication(
|
||||
two_factor_code_data: TwoFactorCode, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
if current_user.is_2fa_enabled:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is already enabled.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is already enabled."
|
||||
)
|
||||
if not current_user.two_factor_secret:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="2FA setup not initiated.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="2FA setup not initiated."
|
||||
)
|
||||
|
||||
if not verify_totp_code(current_user.two_factor_secret, two_factor_code_data.two_factor_code):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid 2FA code.")
|
||||
if not verify_totp_code(
|
||||
current_user.two_factor_secret, two_factor_code_data.two_factor_code
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid 2FA code."
|
||||
)
|
||||
|
||||
current_user.is_2fa_enabled = True
|
||||
await current_user.save()
|
||||
|
||||
access_token_expires = timedelta(minutes=30) # Use settings
|
||||
access_token_expires = timedelta(minutes=30) # Use settings
|
||||
access_token = create_access_token(
|
||||
data={"sub": current_user.username}, expires_delta=access_token_expires, two_factor_verified=True
|
||||
data={"sub": current_user.username},
|
||||
expires_delta=access_token_expires,
|
||||
two_factor_verified=True,
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/2fa/disable", response_model=dict)
|
||||
async def disable_two_factor_authentication(disable_data: TwoFactorDisable, current_user: User = Depends(get_current_verified_user)):
|
||||
async def disable_two_factor_authentication(
|
||||
disable_data: TwoFactorDisable,
|
||||
current_user: User = Depends(get_current_verified_user),
|
||||
):
|
||||
if not current_user.is_2fa_enabled:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is not enabled.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is not enabled."
|
||||
)
|
||||
|
||||
# Verify password
|
||||
if not verify_password(disable_data.password, current_user.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password."
|
||||
)
|
||||
|
||||
# Verify 2FA code
|
||||
if not verify_totp_code(current_user.two_factor_secret, disable_data.two_factor_code):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid 2FA code.")
|
||||
if not verify_totp_code(
|
||||
current_user.two_factor_secret, disable_data.two_factor_code
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid 2FA code."
|
||||
)
|
||||
|
||||
current_user.two_factor_secret = None
|
||||
current_user.is_2fa_enabled = False
|
||||
@@ -161,10 +218,15 @@ async def disable_two_factor_authentication(disable_data: TwoFactorDisable, curr
|
||||
|
||||
return {"message": "2FA disabled successfully."}
|
||||
|
||||
|
||||
@router.get("/2fa/recovery-codes", response_model=List[str])
|
||||
async def get_new_recovery_codes(current_user: User = Depends(get_current_verified_user)):
|
||||
async def get_new_recovery_codes(
|
||||
current_user: User = Depends(get_current_verified_user),
|
||||
):
|
||||
if not current_user.is_2fa_enabled:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is not enabled.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="2FA is not enabled."
|
||||
)
|
||||
|
||||
recovery_codes = generate_recovery_codes()
|
||||
hashed_recovery_codes = hash_recovery_codes(recovery_codes)
|
||||
|
||||
+117
-74
@@ -1,25 +1,25 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
import calendar
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..models import User
|
||||
from ..billing.models import (
|
||||
Invoice, InvoiceLineItem, UserSubscription, PricingConfig,
|
||||
PaymentMethod, UsageAggregate, SubscriptionPlan
|
||||
Invoice,
|
||||
UserSubscription,
|
||||
PricingConfig,
|
||||
PaymentMethod,
|
||||
UsageAggregate,
|
||||
SubscriptionPlan,
|
||||
)
|
||||
from ..billing.usage_tracker import UsageTracker
|
||||
from ..billing.invoice_generator import InvoiceGenerator
|
||||
from ..billing.stripe_client import StripeClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/billing",
|
||||
tags=["billing"]
|
||||
)
|
||||
router = APIRouter(prefix="/api/billing", tags=["billing"])
|
||||
|
||||
|
||||
class UsageResponse(BaseModel):
|
||||
storage_gb_avg: float
|
||||
@@ -29,6 +29,7 @@ class UsageResponse(BaseModel):
|
||||
total_bandwidth_gb: float
|
||||
period: str
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
id: int
|
||||
invoice_number: str
|
||||
@@ -42,6 +43,7 @@ class InvoiceResponse(BaseModel):
|
||||
paid_at: Optional[datetime]
|
||||
line_items: List[dict]
|
||||
|
||||
|
||||
class SubscriptionResponse(BaseModel):
|
||||
id: int
|
||||
billing_type: str
|
||||
@@ -50,6 +52,7 @@ class SubscriptionResponse(BaseModel):
|
||||
current_period_start: Optional[datetime]
|
||||
current_period_end: Optional[datetime]
|
||||
|
||||
|
||||
@router.get("/usage/current")
|
||||
async def get_current_usage(current_user: User = Depends(get_current_user)):
|
||||
try:
|
||||
@@ -61,25 +64,32 @@ async def get_current_usage(current_user: User = Depends(get_current_user)):
|
||||
if usage_today:
|
||||
return {
|
||||
"storage_gb": round(storage_bytes / (1024**3), 4),
|
||||
"bandwidth_down_gb_today": round(usage_today.bandwidth_down_bytes / (1024**3), 4),
|
||||
"bandwidth_up_gb_today": round(usage_today.bandwidth_up_bytes / (1024**3), 4),
|
||||
"as_of": today.isoformat()
|
||||
"bandwidth_down_gb_today": round(
|
||||
usage_today.bandwidth_down_bytes / (1024**3), 4
|
||||
),
|
||||
"bandwidth_up_gb_today": round(
|
||||
usage_today.bandwidth_up_bytes / (1024**3), 4
|
||||
),
|
||||
"as_of": today.isoformat(),
|
||||
}
|
||||
|
||||
return {
|
||||
"storage_gb": round(storage_bytes / (1024**3), 4),
|
||||
"bandwidth_down_gb_today": 0,
|
||||
"bandwidth_up_gb_today": 0,
|
||||
"as_of": today.isoformat()
|
||||
"as_of": today.isoformat(),
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch usage data: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch usage data: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/usage/monthly")
|
||||
async def get_monthly_usage(
|
||||
year: Optional[int] = None,
|
||||
month: Optional[int] = None,
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> UsageResponse:
|
||||
try:
|
||||
if year is None or month is None:
|
||||
@@ -88,71 +98,85 @@ async def get_monthly_usage(
|
||||
month = now.month
|
||||
|
||||
if not (1 <= month <= 12):
|
||||
raise HTTPException(status_code=400, detail="Month must be between 1 and 12")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Month must be between 1 and 12"
|
||||
)
|
||||
if not (2020 <= year <= 2100):
|
||||
raise HTTPException(status_code=400, detail="Year must be between 2020 and 2100")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Year must be between 2020 and 2100"
|
||||
)
|
||||
|
||||
usage = await UsageTracker.get_monthly_usage(current_user, year, month)
|
||||
|
||||
return UsageResponse(
|
||||
**usage,
|
||||
period=f"{year}-{month:02d}"
|
||||
)
|
||||
return UsageResponse(**usage, period=f"{year}-{month:02d}")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch monthly usage: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch monthly usage: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/invoices")
|
||||
async def list_invoices(
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
current_user: User = Depends(get_current_user)
|
||||
limit: int = 50, offset: int = 0, current_user: User = Depends(get_current_user)
|
||||
) -> List[InvoiceResponse]:
|
||||
try:
|
||||
if limit < 1 or limit > 100:
|
||||
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Limit must be between 1 and 100"
|
||||
)
|
||||
if offset < 0:
|
||||
raise HTTPException(status_code=400, detail="Offset must be non-negative")
|
||||
|
||||
invoices = await Invoice.filter(user=current_user).order_by("-created_at").offset(offset).limit(limit).all()
|
||||
invoices = (
|
||||
await Invoice.filter(user=current_user)
|
||||
.order_by("-created_at")
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for invoice in invoices:
|
||||
line_items = await invoice.line_items.all()
|
||||
result.append(InvoiceResponse(
|
||||
id=invoice.id,
|
||||
invoice_number=invoice.invoice_number,
|
||||
period_start=invoice.period_start,
|
||||
period_end=invoice.period_end,
|
||||
subtotal=float(invoice.subtotal),
|
||||
tax=float(invoice.tax),
|
||||
total=float(invoice.total),
|
||||
status=invoice.status,
|
||||
due_date=invoice.due_date,
|
||||
paid_at=invoice.paid_at,
|
||||
line_items=[
|
||||
{
|
||||
"description": item.description,
|
||||
"quantity": float(item.quantity),
|
||||
"unit_price": float(item.unit_price),
|
||||
"amount": float(item.amount),
|
||||
"type": item.item_type
|
||||
}
|
||||
for item in line_items
|
||||
]
|
||||
))
|
||||
result.append(
|
||||
InvoiceResponse(
|
||||
id=invoice.id,
|
||||
invoice_number=invoice.invoice_number,
|
||||
period_start=invoice.period_start,
|
||||
period_end=invoice.period_end,
|
||||
subtotal=float(invoice.subtotal),
|
||||
tax=float(invoice.tax),
|
||||
total=float(invoice.total),
|
||||
status=invoice.status,
|
||||
due_date=invoice.due_date,
|
||||
paid_at=invoice.paid_at,
|
||||
line_items=[
|
||||
{
|
||||
"description": item.description,
|
||||
"quantity": float(item.quantity),
|
||||
"unit_price": float(item.unit_price),
|
||||
"amount": float(item.amount),
|
||||
"type": item.item_type,
|
||||
}
|
||||
for item in line_items
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to fetch invoices: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch invoices: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}")
|
||||
async def get_invoice(
|
||||
invoice_id: int,
|
||||
current_user: User = Depends(get_current_user)
|
||||
invoice_id: int, current_user: User = Depends(get_current_user)
|
||||
) -> InvoiceResponse:
|
||||
invoice = await Invoice.get_or_none(id=invoice_id, user=current_user)
|
||||
if not invoice:
|
||||
@@ -177,21 +201,22 @@ async def get_invoice(
|
||||
"quantity": float(item.quantity),
|
||||
"unit_price": float(item.unit_price),
|
||||
"amount": float(item.amount),
|
||||
"type": item.item_type
|
||||
"type": item.item_type,
|
||||
}
|
||||
for item in line_items
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/subscription")
|
||||
async def get_subscription(current_user: User = Depends(get_current_user)) -> SubscriptionResponse:
|
||||
async def get_subscription(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> SubscriptionResponse:
|
||||
subscription = await UserSubscription.get_or_none(user=current_user)
|
||||
|
||||
if not subscription:
|
||||
subscription = await UserSubscription.create(
|
||||
user=current_user,
|
||||
billing_type="pay_as_you_go",
|
||||
status="active"
|
||||
user=current_user, billing_type="pay_as_you_go", status="active"
|
||||
)
|
||||
|
||||
plan_name = None
|
||||
@@ -205,15 +230,19 @@ async def get_subscription(current_user: User = Depends(get_current_user)) -> Su
|
||||
plan_name=plan_name,
|
||||
status=subscription.status,
|
||||
current_period_start=subscription.current_period_start,
|
||||
current_period_end=subscription.current_period_end
|
||||
current_period_end=subscription.current_period_end,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/payment-methods/setup-intent")
|
||||
async def create_setup_intent(current_user: User = Depends(get_current_user)):
|
||||
try:
|
||||
from ..settings import settings
|
||||
|
||||
if not settings.STRIPE_SECRET_KEY:
|
||||
raise HTTPException(status_code=503, detail="Payment processing not configured")
|
||||
raise HTTPException(
|
||||
status_code=503, detail="Payment processing not configured"
|
||||
)
|
||||
|
||||
subscription = await UserSubscription.get_or_none(user=current_user)
|
||||
|
||||
@@ -221,7 +250,7 @@ async def create_setup_intent(current_user: User = Depends(get_current_user)):
|
||||
customer_id = await StripeClient.create_customer(
|
||||
email=current_user.email,
|
||||
name=current_user.username,
|
||||
metadata={"user_id": str(current_user.id)}
|
||||
metadata={"user_id": str(current_user.id)},
|
||||
)
|
||||
|
||||
if not subscription:
|
||||
@@ -229,27 +258,30 @@ async def create_setup_intent(current_user: User = Depends(get_current_user)):
|
||||
user=current_user,
|
||||
billing_type="pay_as_you_go",
|
||||
stripe_customer_id=customer_id,
|
||||
status="active"
|
||||
status="active",
|
||||
)
|
||||
else:
|
||||
subscription.stripe_customer_id = customer_id
|
||||
await subscription.save()
|
||||
|
||||
import stripe
|
||||
|
||||
StripeClient._ensure_api_key()
|
||||
setup_intent = stripe.SetupIntent.create(
|
||||
customer=subscription.stripe_customer_id,
|
||||
payment_method_types=["card"]
|
||||
customer=subscription.stripe_customer_id, payment_method_types=["card"]
|
||||
)
|
||||
|
||||
return {
|
||||
"client_secret": setup_intent.client_secret,
|
||||
"customer_id": subscription.stripe_customer_id
|
||||
"customer_id": subscription.stripe_customer_id,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create setup intent: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create setup intent: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/payment-methods")
|
||||
async def list_payment_methods(current_user: User = Depends(get_current_user)):
|
||||
@@ -262,11 +294,12 @@ async def list_payment_methods(current_user: User = Depends(get_current_user)):
|
||||
"brand": m.brand,
|
||||
"exp_month": m.exp_month,
|
||||
"exp_year": m.exp_year,
|
||||
"is_default": m.is_default
|
||||
"is_default": m.is_default,
|
||||
}
|
||||
for m in methods
|
||||
]
|
||||
|
||||
|
||||
@router.post("/webhooks/stripe")
|
||||
async def stripe_webhook(request: Request):
|
||||
import stripe
|
||||
@@ -298,12 +331,14 @@ async def stripe_webhook(request: Request):
|
||||
event_type=event["type"],
|
||||
stripe_event_id=event_id,
|
||||
data=event["data"],
|
||||
processed=False
|
||||
processed=False,
|
||||
)
|
||||
|
||||
if event["type"] == "invoice.payment_succeeded":
|
||||
invoice_data = event["data"]["object"]
|
||||
mywebdav_invoice_id = invoice_data.get("metadata", {}).get("mywebdav_invoice_id")
|
||||
mywebdav_invoice_id = invoice_data.get("metadata", {}).get(
|
||||
"mywebdav_invoice_id"
|
||||
)
|
||||
|
||||
if mywebdav_invoice_id:
|
||||
invoice = await Invoice.get_or_none(id=int(mywebdav_invoice_id))
|
||||
@@ -317,7 +352,9 @@ async def stripe_webhook(request: Request):
|
||||
payment_method = event["data"]["object"]
|
||||
customer_id = payment_method["customer"]
|
||||
|
||||
subscription = await UserSubscription.get_or_none(stripe_customer_id=customer_id)
|
||||
subscription = await UserSubscription.get_or_none(
|
||||
stripe_customer_id=customer_id
|
||||
)
|
||||
if subscription:
|
||||
await PaymentMethod.create(
|
||||
user=subscription.user,
|
||||
@@ -327,7 +364,7 @@ async def stripe_webhook(request: Request):
|
||||
brand=payment_method.get("card", {}).get("brand"),
|
||||
exp_month=payment_method.get("card", {}).get("exp_month"),
|
||||
exp_year=payment_method.get("card", {}).get("exp_year"),
|
||||
is_default=True
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
await BillingEvent.filter(stripe_event_id=event_id).update(processed=True)
|
||||
@@ -335,7 +372,10 @@ async def stripe_webhook(request: Request):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Webhook processing failed: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Webhook processing failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pricing")
|
||||
async def get_pricing():
|
||||
@@ -344,11 +384,12 @@ async def get_pricing():
|
||||
config.config_key: {
|
||||
"value": float(config.config_value),
|
||||
"description": config.description,
|
||||
"unit": config.unit
|
||||
"unit": config.unit,
|
||||
}
|
||||
for config in configs
|
||||
}
|
||||
|
||||
|
||||
@router.get("/plans")
|
||||
async def list_plans():
|
||||
plans = await SubscriptionPlan.filter(is_active=True).all()
|
||||
@@ -361,14 +402,16 @@ async def list_plans():
|
||||
"storage_gb": plan.storage_gb,
|
||||
"bandwidth_gb": plan.bandwidth_gb,
|
||||
"price_monthly": float(plan.price_monthly),
|
||||
"price_yearly": float(plan.price_yearly) if plan.price_yearly else None
|
||||
"price_yearly": float(plan.price_yearly) if plan.price_yearly else None,
|
||||
}
|
||||
for plan in plans
|
||||
]
|
||||
|
||||
|
||||
@router.get("/stripe-key")
|
||||
async def get_stripe_key():
|
||||
from ..settings import settings
|
||||
|
||||
if not settings.STRIPE_PUBLISHABLE_KEY:
|
||||
raise HTTPException(status_code=503, detail="Payment processing not configured")
|
||||
return {"publishable_key": settings.STRIPE_PUBLISHABLE_KEY}
|
||||
|
||||
+297
-93
@@ -1,4 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile, File as FastAPIFile, HTTPException, status, Response, Form
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
UploadFile,
|
||||
File as FastAPIFile,
|
||||
HTTPException,
|
||||
status,
|
||||
Form,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List, Optional
|
||||
import mimetypes
|
||||
@@ -11,7 +19,6 @@ from ..auth import get_current_user
|
||||
from ..models import User, File, Folder
|
||||
from ..schemas import FileOut
|
||||
from ..storage import storage_manager
|
||||
from ..settings import settings
|
||||
from ..activity import log_activity
|
||||
from ..thumbnails import generate_thumbnail, delete_thumbnail
|
||||
|
||||
@@ -20,35 +27,46 @@ router = APIRouter(
|
||||
tags=["files"],
|
||||
)
|
||||
|
||||
|
||||
class FileMove(BaseModel):
|
||||
target_folder_id: Optional[int] = None
|
||||
|
||||
|
||||
class FileRename(BaseModel):
|
||||
new_name: str
|
||||
|
||||
|
||||
class FileCopy(BaseModel):
|
||||
target_folder_id: Optional[int] = None
|
||||
|
||||
|
||||
class BatchFileOperation(BaseModel):
|
||||
file_ids: List[int]
|
||||
operation: str # e.g., "delete", "star", "unstar", "move", "copy"
|
||||
operation: str # e.g., "delete", "star", "unstar", "move", "copy"
|
||||
|
||||
|
||||
class BatchMoveCopyPayload(BaseModel):
|
||||
target_folder_id: Optional[int] = None
|
||||
|
||||
|
||||
class FileContentUpdate(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
@router.post("/upload", response_model=FileOut, status_code=status.HTTP_201_CREATED)
|
||||
async def upload_file(
|
||||
file: UploadFile = FastAPIFile(...),
|
||||
folder_id: Optional[int] = Form(None),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if folder_id:
|
||||
parent_folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
parent_folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not parent_folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
else:
|
||||
parent_folder = None
|
||||
|
||||
@@ -73,7 +91,7 @@ async def upload_file(
|
||||
|
||||
# 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
|
||||
unique_filename = f"{file_hash}{file_extension}" # Use hash for unique filename
|
||||
storage_path = unique_filename
|
||||
|
||||
# Save file to storage
|
||||
@@ -105,16 +123,20 @@ async def upload_file(
|
||||
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
|
||||
@router.get("/download/{file_id}")
|
||||
async def download_file(file_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
db_file.last_accessed_at = datetime.now()
|
||||
await db_file.save()
|
||||
|
||||
try:
|
||||
|
||||
async def file_iterator():
|
||||
async for chunk in storage_manager.get_file(current_user.id, db_file.path):
|
||||
yield chunk
|
||||
@@ -122,16 +144,21 @@ async def download_file(file_id: int, current_user: User = Depends(get_current_u
|
||||
return StreamingResponse(
|
||||
file_iterator(),
|
||||
media_type=db_file.mime_type,
|
||||
headers={"Content-Disposition": f"attachment; filename=\"{db_file.name}\""}
|
||||
headers={"Content-Disposition": f'attachment; filename="{db_file.name}"'},
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found in storage")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found in storage"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_file(file_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
db_file.is_deleted = True
|
||||
db_file.deleted_at = datetime.now()
|
||||
@@ -141,68 +168,110 @@ async def delete_file(file_id: int, current_user: User = Depends(get_current_use
|
||||
|
||||
return
|
||||
|
||||
|
||||
@router.post("/{file_id}/move", response_model=FileOut)
|
||||
async def move_file(file_id: int, move_data: FileMove, current_user: User = Depends(get_current_user)):
|
||||
async def move_file(
|
||||
file_id: int, move_data: FileMove, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
target_folder = None
|
||||
if move_data.target_folder_id:
|
||||
target_folder = await Folder.get_or_none(id=move_data.target_folder_id, owner=current_user, is_deleted=False)
|
||||
target_folder = await Folder.get_or_none(
|
||||
id=move_data.target_folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not target_folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Target folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Target folder not found"
|
||||
)
|
||||
|
||||
existing_file = await File.get_or_none(
|
||||
name=db_file.name, parent=target_folder, owner=current_user, is_deleted=False
|
||||
)
|
||||
if existing_file and existing_file.id != file_id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="File with this name already exists in target folder")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="File with this name already exists in target folder",
|
||||
)
|
||||
|
||||
db_file.parent = target_folder
|
||||
await db_file.save()
|
||||
|
||||
await log_activity(user=current_user, action="file_moved", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user, action="file_moved", target_type="file", target_id=file_id
|
||||
)
|
||||
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
|
||||
@router.post("/{file_id}/rename", response_model=FileOut)
|
||||
async def rename_file(file_id: int, rename_data: FileRename, current_user: User = Depends(get_current_user)):
|
||||
async def rename_file(
|
||||
file_id: int,
|
||||
rename_data: FileRename,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
existing_file = await File.get_or_none(
|
||||
name=rename_data.new_name, parent_id=db_file.parent_id, owner=current_user, is_deleted=False
|
||||
name=rename_data.new_name,
|
||||
parent_id=db_file.parent_id,
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
)
|
||||
if existing_file and existing_file.id != file_id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="File with this name already exists in the same folder")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="File with this name already exists in the same folder",
|
||||
)
|
||||
|
||||
db_file.name = rename_data.new_name
|
||||
await db_file.save()
|
||||
|
||||
await log_activity(user=current_user, action="file_renamed", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user, action="file_renamed", target_type="file", target_id=file_id
|
||||
)
|
||||
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
@router.post("/{file_id}/copy", response_model=FileOut, status_code=status.HTTP_201_CREATED)
|
||||
async def copy_file(file_id: int, copy_data: FileCopy, current_user: User = Depends(get_current_user)):
|
||||
|
||||
@router.post(
|
||||
"/{file_id}/copy", response_model=FileOut, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def copy_file(
|
||||
file_id: int, copy_data: FileCopy, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
target_folder = None
|
||||
if copy_data.target_folder_id:
|
||||
target_folder = await Folder.get_or_none(id=copy_data.target_folder_id, owner=current_user, is_deleted=False)
|
||||
target_folder = await Folder.get_or_none(
|
||||
id=copy_data.target_folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not target_folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Target folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Target folder not found"
|
||||
)
|
||||
|
||||
base_name = db_file.name
|
||||
name_parts = os.path.splitext(base_name)
|
||||
counter = 1
|
||||
new_name = base_name
|
||||
|
||||
while await File.get_or_none(name=new_name, parent=target_folder, owner=current_user, is_deleted=False):
|
||||
while await File.get_or_none(
|
||||
name=new_name, parent=target_folder, owner=current_user, is_deleted=False
|
||||
):
|
||||
new_name = f"{name_parts[0]} (copy {counter}){name_parts[1]}"
|
||||
counter += 1
|
||||
|
||||
@@ -213,139 +282,205 @@ async def copy_file(file_id: int, copy_data: FileCopy, current_user: User = Depe
|
||||
mime_type=db_file.mime_type,
|
||||
file_hash=db_file.file_hash,
|
||||
owner=current_user,
|
||||
parent=target_folder
|
||||
parent=target_folder,
|
||||
)
|
||||
|
||||
await log_activity(user=current_user, action="file_copied", target_type="file", target_id=new_file.id)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_copied",
|
||||
target_type="file",
|
||||
target_id=new_file.id,
|
||||
)
|
||||
|
||||
return await FileOut.from_tortoise_orm(new_file)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FileOut])
|
||||
async def list_files(folder_id: Optional[int] = None, current_user: User = Depends(get_current_user)):
|
||||
async def list_files(
|
||||
folder_id: Optional[int] = None, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
if folder_id:
|
||||
parent_folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
parent_folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not parent_folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
files = await File.filter(parent=parent_folder, owner=current_user, is_deleted=False).order_by("name")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
files = await File.filter(
|
||||
parent=parent_folder, owner=current_user, is_deleted=False
|
||||
).order_by("name")
|
||||
else:
|
||||
files = await File.filter(parent=None, owner=current_user, is_deleted=False).order_by("name")
|
||||
files = await File.filter(
|
||||
parent=None, owner=current_user, is_deleted=False
|
||||
).order_by("name")
|
||||
return [await FileOut.from_tortoise_orm(f) for f in files]
|
||||
|
||||
|
||||
@router.get("/thumbnail/{file_id}")
|
||||
async def get_thumbnail(file_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
db_file.last_accessed_at = datetime.now()
|
||||
await db_file.save()
|
||||
|
||||
thumbnail_path = getattr(db_file, 'thumbnail_path', None)
|
||||
thumbnail_path = getattr(db_file, "thumbnail_path", None)
|
||||
|
||||
if not thumbnail_path:
|
||||
thumbnail_path = await generate_thumbnail(db_file.path, db_file.mime_type, current_user.id)
|
||||
thumbnail_path = await generate_thumbnail(
|
||||
db_file.path, db_file.mime_type, current_user.id
|
||||
)
|
||||
|
||||
if thumbnail_path:
|
||||
db_file.thumbnail_path = thumbnail_path
|
||||
await db_file.save()
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Thumbnail not available")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Thumbnail not available"
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
async def thumbnail_iterator():
|
||||
async for chunk in storage_manager.get_file(current_user.id, thumbnail_path):
|
||||
async for chunk in storage_manager.get_file(
|
||||
current_user.id, thumbnail_path
|
||||
):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(
|
||||
thumbnail_iterator(),
|
||||
media_type="image/jpeg"
|
||||
)
|
||||
return StreamingResponse(thumbnail_iterator(), media_type="image/jpeg")
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Thumbnail not found in storage")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Thumbnail not found in storage",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/photos", response_model=List[FileOut])
|
||||
async def list_photos(current_user: User = Depends(get_current_user)):
|
||||
files = await File.filter(
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
mime_type__istartswith="image/"
|
||||
owner=current_user, is_deleted=False, mime_type__istartswith="image/"
|
||||
).order_by("-created_at")
|
||||
return [await FileOut.from_tortoise_orm(f) for f in files]
|
||||
|
||||
|
||||
@router.get("/recent", response_model=List[FileOut])
|
||||
async def list_recent_files(current_user: User = Depends(get_current_user), limit: int = 10):
|
||||
files = await File.filter(
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
last_accessed_at__isnull=False
|
||||
).order_by("-last_accessed_at").limit(limit)
|
||||
async def list_recent_files(
|
||||
current_user: User = Depends(get_current_user), limit: int = 10
|
||||
):
|
||||
files = (
|
||||
await File.filter(
|
||||
owner=current_user, is_deleted=False, last_accessed_at__isnull=False
|
||||
)
|
||||
.order_by("-last_accessed_at")
|
||||
.limit(limit)
|
||||
)
|
||||
return [await FileOut.from_tortoise_orm(f) for f in files]
|
||||
|
||||
|
||||
@router.post("/{file_id}/star", response_model=FileOut)
|
||||
async def star_file(file_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
db_file.is_starred = True
|
||||
await db_file.save()
|
||||
await log_activity(user=current_user, action="file_starred", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user, action="file_starred", target_type="file", target_id=file_id
|
||||
)
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
|
||||
@router.post("/{file_id}/unstar", response_model=FileOut)
|
||||
async def unstar_file(file_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
db_file.is_starred = False
|
||||
await db_file.save()
|
||||
await log_activity(user=current_user, action="file_unstarred", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_unstarred",
|
||||
target_type="file",
|
||||
target_id=file_id,
|
||||
)
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
|
||||
@router.get("/deleted", response_model=List[FileOut])
|
||||
async def list_deleted_files(current_user: User = Depends(get_current_user)):
|
||||
files = await File.filter(owner=current_user, is_deleted=True).order_by("-deleted_at")
|
||||
files = await File.filter(owner=current_user, is_deleted=True).order_by(
|
||||
"-deleted_at"
|
||||
)
|
||||
return [await FileOut.from_tortoise_orm(f) for f in files]
|
||||
|
||||
|
||||
@router.post("/{file_id}/restore", response_model=FileOut)
|
||||
async def restore_file(file_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=True)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deleted file not found")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Deleted file not found"
|
||||
)
|
||||
|
||||
# Check if a file with the same name exists in the parent folder
|
||||
existing_file = await File.get_or_none(
|
||||
name=db_file.name, parent=db_file.parent, owner=current_user, is_deleted=False
|
||||
)
|
||||
if existing_file:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="A file with the same name already exists in this location. Please rename the existing file or restore to a different location.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="A file with the same name already exists in this location. Please rename the existing file or restore to a different location.",
|
||||
)
|
||||
|
||||
db_file.is_deleted = False
|
||||
db_file.deleted_at = None
|
||||
await db_file.save()
|
||||
await log_activity(user=current_user, action="file_restored", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user, action="file_restored", target_type="file", target_id=file_id
|
||||
)
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
|
||||
class BatchOperationResult(BaseModel):
|
||||
succeeded: List[FileOut]
|
||||
failed: List[dict]
|
||||
|
||||
|
||||
@router.post("/batch")
|
||||
async def batch_file_operations(
|
||||
batch_operation: BatchFileOperation,
|
||||
payload: Optional[BatchMoveCopyPayload] = None,
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if batch_operation.operation not in ["delete", "star", "unstar", "move", "copy"]:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid operation: {batch_operation.operation}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid operation: {batch_operation.operation}",
|
||||
)
|
||||
|
||||
updated_files = []
|
||||
failed_operations = []
|
||||
|
||||
for file_id in batch_operation.file_ids:
|
||||
try:
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
db_file = await File.get_or_none(
|
||||
id=file_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not db_file:
|
||||
failed_operations.append({"file_id": file_id, "reason": "File not found or not owned by user"})
|
||||
failed_operations.append(
|
||||
{
|
||||
"file_id": file_id,
|
||||
"reason": "File not found or not owned by user",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if batch_operation.operation == "delete":
|
||||
@@ -353,47 +488,87 @@ async def batch_file_operations(
|
||||
db_file.deleted_at = datetime.now()
|
||||
await db_file.save()
|
||||
await delete_thumbnail(db_file.id)
|
||||
await log_activity(user=current_user, action="file_deleted_batch", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_deleted_batch",
|
||||
target_type="file",
|
||||
target_id=file_id,
|
||||
)
|
||||
updated_files.append(db_file)
|
||||
elif batch_operation.operation == "star":
|
||||
db_file.is_starred = True
|
||||
await db_file.save()
|
||||
await log_activity(user=current_user, action="file_starred_batch", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_starred_batch",
|
||||
target_type="file",
|
||||
target_id=file_id,
|
||||
)
|
||||
updated_files.append(db_file)
|
||||
elif batch_operation.operation == "unstar":
|
||||
db_file.is_starred = False
|
||||
await db_file.save()
|
||||
await log_activity(user=current_user, action="file_unstarred_batch", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_unstarred_batch",
|
||||
target_type="file",
|
||||
target_id=file_id,
|
||||
)
|
||||
updated_files.append(db_file)
|
||||
elif batch_operation.operation == "move":
|
||||
if not payload or payload.target_folder_id is None:
|
||||
failed_operations.append({"file_id": file_id, "reason": "Target folder not specified"})
|
||||
failed_operations.append(
|
||||
{"file_id": file_id, "reason": "Target folder not specified"}
|
||||
)
|
||||
continue
|
||||
|
||||
target_folder = await Folder.get_or_none(id=payload.target_folder_id, owner=current_user, is_deleted=False)
|
||||
target_folder = await Folder.get_or_none(
|
||||
id=payload.target_folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not target_folder:
|
||||
failed_operations.append({"file_id": file_id, "reason": "Target folder not found"})
|
||||
failed_operations.append(
|
||||
{"file_id": file_id, "reason": "Target folder not found"}
|
||||
)
|
||||
continue
|
||||
|
||||
existing_file = await File.get_or_none(
|
||||
name=db_file.name, parent=target_folder, owner=current_user, is_deleted=False
|
||||
name=db_file.name,
|
||||
parent=target_folder,
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
)
|
||||
if existing_file and existing_file.id != file_id:
|
||||
failed_operations.append({"file_id": file_id, "reason": "File with same name exists in target folder"})
|
||||
failed_operations.append(
|
||||
{
|
||||
"file_id": file_id,
|
||||
"reason": "File with same name exists in target folder",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
db_file.parent = target_folder
|
||||
await db_file.save()
|
||||
await log_activity(user=current_user, action="file_moved_batch", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_moved_batch",
|
||||
target_type="file",
|
||||
target_id=file_id,
|
||||
)
|
||||
updated_files.append(db_file)
|
||||
elif batch_operation.operation == "copy":
|
||||
if not payload or payload.target_folder_id is None:
|
||||
failed_operations.append({"file_id": file_id, "reason": "Target folder not specified"})
|
||||
failed_operations.append(
|
||||
{"file_id": file_id, "reason": "Target folder not specified"}
|
||||
)
|
||||
continue
|
||||
|
||||
target_folder = await Folder.get_or_none(id=payload.target_folder_id, owner=current_user, is_deleted=False)
|
||||
target_folder = await Folder.get_or_none(
|
||||
id=payload.target_folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not target_folder:
|
||||
failed_operations.append({"file_id": file_id, "reason": "Target folder not found"})
|
||||
failed_operations.append(
|
||||
{"file_id": file_id, "reason": "Target folder not found"}
|
||||
)
|
||||
continue
|
||||
|
||||
base_name = db_file.name
|
||||
@@ -401,7 +576,12 @@ async def batch_file_operations(
|
||||
counter = 1
|
||||
new_name = base_name
|
||||
|
||||
while await File.get_or_none(name=new_name, parent=target_folder, owner=current_user, is_deleted=False):
|
||||
while await File.get_or_none(
|
||||
name=new_name,
|
||||
parent=target_folder,
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
):
|
||||
new_name = f"{name_parts[0]} (copy {counter}){name_parts[1]}"
|
||||
counter += 1
|
||||
|
||||
@@ -412,48 +592,70 @@ async def batch_file_operations(
|
||||
mime_type=db_file.mime_type,
|
||||
file_hash=db_file.file_hash,
|
||||
owner=current_user,
|
||||
parent=target_folder
|
||||
parent=target_folder,
|
||||
)
|
||||
await log_activity(
|
||||
user=current_user,
|
||||
action="file_copied_batch",
|
||||
target_type="file",
|
||||
target_id=new_file.id,
|
||||
)
|
||||
await log_activity(user=current_user, action="file_copied_batch", target_type="file", target_id=new_file.id)
|
||||
updated_files.append(new_file)
|
||||
except Exception as e:
|
||||
failed_operations.append({"file_id": file_id, "reason": str(e)})
|
||||
|
||||
return {
|
||||
"succeeded": [await FileOut.from_tortoise_orm(f) for f in updated_files],
|
||||
"failed": failed_operations
|
||||
"failed": failed_operations,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{file_id}/content", response_model=FileOut)
|
||||
async def update_file_content(
|
||||
file_id: int,
|
||||
payload: FileContentUpdate,
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_file = await File.get_or_none(id=file_id, owner=current_user, is_deleted=False)
|
||||
if not db_file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
if not db_file.mime_type or not db_file.mime_type.startswith('text/'):
|
||||
if not db_file.mime_type or not db_file.mime_type.startswith("text/"):
|
||||
editableExtensions = [
|
||||
'txt', 'md', 'log', 'json', 'js', 'py', 'html', 'css',
|
||||
'xml', 'yaml', 'yml', 'sh', 'bat', 'ini', 'conf', 'cfg'
|
||||
"txt",
|
||||
"md",
|
||||
"log",
|
||||
"json",
|
||||
"js",
|
||||
"py",
|
||||
"html",
|
||||
"css",
|
||||
"xml",
|
||||
"yaml",
|
||||
"yml",
|
||||
"sh",
|
||||
"bat",
|
||||
"ini",
|
||||
"conf",
|
||||
"cfg",
|
||||
]
|
||||
file_extension = os.path.splitext(db_file.name)[1][1:].lower()
|
||||
if file_extension not in editableExtensions:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File type is not editable"
|
||||
detail="File type is not editable",
|
||||
)
|
||||
|
||||
content_bytes = payload.content.encode('utf-8')
|
||||
content_bytes = payload.content.encode("utf-8")
|
||||
new_size = len(content_bytes)
|
||||
size_diff = new_size - db_file.size
|
||||
|
||||
if current_user.used_storage_bytes + size_diff > current_user.storage_quota_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_507_INSUFFICIENT_STORAGE,
|
||||
detail="Storage quota exceeded"
|
||||
detail="Storage quota exceeded",
|
||||
)
|
||||
|
||||
new_hash = hashlib.sha256(content_bytes).hexdigest()
|
||||
@@ -465,7 +667,7 @@ async def update_file_content(
|
||||
if new_storage_path != db_file.path:
|
||||
try:
|
||||
await storage_manager.delete_file(current_user.id, db_file.path)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
db_file.path = new_storage_path
|
||||
@@ -477,6 +679,8 @@ async def update_file_content(
|
||||
current_user.used_storage_bytes += size_diff
|
||||
await current_user.save()
|
||||
|
||||
await log_activity(user=current_user, action="file_updated", target_type="file", target_id=file_id)
|
||||
await log_activity(
|
||||
user=current_user, action="file_updated", target_type="file", target_id=file_id
|
||||
)
|
||||
|
||||
return await FileOut.from_tortoise_orm(db_file)
|
||||
|
||||
+109
-34
@@ -3,7 +3,13 @@ from typing import List, Optional
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..models import User, Folder
|
||||
from ..schemas import FolderCreate, FolderOut, FolderUpdate, BatchFolderOperation, BatchMoveCopyPayload
|
||||
from ..schemas import (
|
||||
FolderCreate,
|
||||
FolderOut,
|
||||
FolderUpdate,
|
||||
BatchFolderOperation,
|
||||
BatchMoveCopyPayload,
|
||||
)
|
||||
from ..activity import log_activity
|
||||
|
||||
router = APIRouter(
|
||||
@@ -11,12 +17,17 @@ router = APIRouter(
|
||||
tags=["folders"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=FolderOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_folder(folder_in: FolderCreate, current_user: User = Depends(get_current_user)):
|
||||
async def create_folder(
|
||||
folder_in: FolderCreate, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
# Check if parent folder exists and belongs to the current user
|
||||
parent_folder = None
|
||||
if folder_in.parent_id:
|
||||
parent_folder = await Folder.get_or_none(id=folder_in.parent_id, owner=current_user)
|
||||
parent_folder = await Folder.get_or_none(
|
||||
id=folder_in.parent_id, owner=current_user
|
||||
)
|
||||
if not parent_folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -39,50 +50,82 @@ async def create_folder(folder_in: FolderCreate, current_user: User = Depends(ge
|
||||
await log_activity(current_user, "folder_created", "folder", folder.id)
|
||||
return await FolderOut.from_tortoise_orm(folder)
|
||||
|
||||
|
||||
@router.get("/{folder_id}/path", response_model=List[FolderOut])
|
||||
async def get_folder_path(folder_id: int, current_user: User = Depends(get_current_user)):
|
||||
folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
async def get_folder_path(
|
||||
folder_id: int, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
path = []
|
||||
current = folder
|
||||
while current:
|
||||
path.insert(0, await FolderOut.from_tortoise_orm(current))
|
||||
if current.parent_id:
|
||||
current = await Folder.get_or_none(id=current.parent_id, owner=current_user, is_deleted=False)
|
||||
current = await Folder.get_or_none(
|
||||
id=current.parent_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
else:
|
||||
current = None
|
||||
|
||||
return path
|
||||
|
||||
|
||||
@router.get("/{folder_id}", response_model=FolderOut)
|
||||
async def get_folder(folder_id: int, current_user: User = Depends(get_current_user)):
|
||||
folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
return await FolderOut.from_tortoise_orm(folder)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FolderOut])
|
||||
async def list_folders(parent_id: Optional[int] = None, current_user: User = Depends(get_current_user)):
|
||||
async def list_folders(
|
||||
parent_id: Optional[int] = None, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
if parent_id:
|
||||
parent_folder = await Folder.get_or_none(id=parent_id, owner=current_user, is_deleted=False)
|
||||
parent_folder = await Folder.get_or_none(
|
||||
id=parent_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not parent_folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Parent folder not found or does not belong to the current user",
|
||||
)
|
||||
folders = await Folder.filter(parent=parent_folder, owner=current_user, is_deleted=False).order_by("name")
|
||||
folders = await Folder.filter(
|
||||
parent=parent_folder, owner=current_user, is_deleted=False
|
||||
).order_by("name")
|
||||
else:
|
||||
# List root folders (folders with no parent)
|
||||
folders = await Folder.filter(parent=None, owner=current_user, is_deleted=False).order_by("name")
|
||||
folders = await Folder.filter(
|
||||
parent=None, owner=current_user, is_deleted=False
|
||||
).order_by("name")
|
||||
return [await FolderOut.from_tortoise_orm(folder) for folder in folders]
|
||||
|
||||
|
||||
@router.put("/{folder_id}", response_model=FolderOut)
|
||||
async def update_folder(folder_id: int, folder_in: FolderUpdate, current_user: User = Depends(get_current_user)):
|
||||
folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
async def update_folder(
|
||||
folder_id: int,
|
||||
folder_in: FolderUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
if folder_in.name:
|
||||
existing_folder = await Folder.get_or_none(
|
||||
@@ -97,11 +140,16 @@ async def update_folder(folder_id: int, folder_in: FolderUpdate, current_user: U
|
||||
|
||||
if folder_in.parent_id is not None:
|
||||
if folder_in.parent_id == folder_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot set folder as its own parent")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot set folder as its own parent",
|
||||
)
|
||||
|
||||
new_parent_folder = None
|
||||
if folder_in.parent_id != 0: # 0 could represent moving to root
|
||||
new_parent_folder = await Folder.get_or_none(id=folder_in.parent_id, owner=current_user, is_deleted=False)
|
||||
if folder_in.parent_id != 0: # 0 could represent moving to root
|
||||
new_parent_folder = await Folder.get_or_none(
|
||||
id=folder_in.parent_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not new_parent_folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -113,48 +161,66 @@ async def update_folder(folder_id: int, folder_in: FolderUpdate, current_user: U
|
||||
await log_activity(current_user, "folder_updated", "folder", folder.id)
|
||||
return await FolderOut.from_tortoise_orm(folder)
|
||||
|
||||
|
||||
@router.delete("/{folder_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_folder(folder_id: int, current_user: User = Depends(get_current_user)):
|
||||
folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
|
||||
folder.is_deleted = True
|
||||
await folder.save()
|
||||
await log_activity(current_user, "folder_deleted", "folder", folder.id)
|
||||
return
|
||||
|
||||
|
||||
@router.post("/{folder_id}/star", response_model=FolderOut)
|
||||
async def star_folder(folder_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
db_folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not db_folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
db_folder.is_starred = True
|
||||
await db_folder.save()
|
||||
await log_activity(current_user, "folder_starred", "folder", folder_id)
|
||||
return await FolderOut.from_tortoise_orm(db_folder)
|
||||
|
||||
|
||||
@router.post("/{folder_id}/unstar", response_model=FolderOut)
|
||||
async def unstar_folder(folder_id: int, current_user: User = Depends(get_current_user)):
|
||||
db_folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
db_folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not db_folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
db_folder.is_starred = False
|
||||
await db_folder.save()
|
||||
await log_activity(current_user, "folder_unstarred", "folder", folder_id)
|
||||
return await FolderOut.from_tortoise_orm(db_folder)
|
||||
|
||||
|
||||
@router.post("/batch", response_model=List[FolderOut])
|
||||
async def batch_folder_operations(
|
||||
batch_operation: BatchFolderOperation,
|
||||
payload: Optional[BatchMoveCopyPayload] = None,
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
updated_folders = []
|
||||
for folder_id in batch_operation.folder_ids:
|
||||
db_folder = await Folder.get_or_none(id=folder_id, owner=current_user, is_deleted=False)
|
||||
db_folder = await Folder.get_or_none(
|
||||
id=folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not db_folder:
|
||||
continue # Skip if folder not found or not owned by user
|
||||
continue # Skip if folder not found or not owned by user
|
||||
|
||||
if batch_operation.operation == "delete":
|
||||
db_folder.is_deleted = True
|
||||
@@ -171,13 +237,22 @@ async def batch_folder_operations(
|
||||
await db_folder.save()
|
||||
await log_activity(current_user, "folder_unstarred", "folder", folder_id)
|
||||
updated_folders.append(db_folder)
|
||||
elif batch_operation.operation == "move" and payload and payload.target_folder_id is not None:
|
||||
target_folder = await Folder.get_or_none(id=payload.target_folder_id, owner=current_user, is_deleted=False)
|
||||
elif (
|
||||
batch_operation.operation == "move"
|
||||
and payload
|
||||
and payload.target_folder_id is not None
|
||||
):
|
||||
target_folder = await Folder.get_or_none(
|
||||
id=payload.target_folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not target_folder:
|
||||
continue
|
||||
|
||||
existing_folder = await Folder.get_or_none(
|
||||
name=db_folder.name, parent=target_folder, owner=current_user, is_deleted=False
|
||||
name=db_folder.name,
|
||||
parent=target_folder,
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
)
|
||||
if existing_folder and existing_folder.id != folder_id:
|
||||
continue
|
||||
@@ -186,5 +261,5 @@ async def batch_folder_operations(
|
||||
await db_folder.save()
|
||||
await log_activity(current_user, "folder_moved", "folder", folder_id)
|
||||
updated_folders.append(db_folder)
|
||||
|
||||
|
||||
return [await FolderOut.from_tortoise_orm(f) for f in updated_folders]
|
||||
|
||||
+18
-10
@@ -11,15 +11,22 @@ router = APIRouter(
|
||||
tags=["search"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/files", response_model=List[FileOut])
|
||||
async def search_files(
|
||||
q: str = Query(..., min_length=1, description="Search query"),
|
||||
file_type: Optional[str] = Query(None, description="Filter by MIME type prefix (e.g., 'image', 'video')"),
|
||||
file_type: Optional[str] = Query(
|
||||
None, description="Filter by MIME type prefix (e.g., 'image', 'video')"
|
||||
),
|
||||
min_size: Optional[int] = Query(None, description="Minimum file size in bytes"),
|
||||
max_size: Optional[int] = Query(None, description="Maximum file size in bytes"),
|
||||
date_from: Optional[datetime] = Query(None, description="Filter files created after this date"),
|
||||
date_to: Optional[datetime] = Query(None, description="Filter files created before this date"),
|
||||
current_user: User = Depends(get_current_user)
|
||||
date_from: Optional[datetime] = Query(
|
||||
None, description="Filter files created after this date"
|
||||
),
|
||||
date_to: Optional[datetime] = Query(
|
||||
None, description="Filter files created before this date"
|
||||
),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = File.filter(owner=current_user, is_deleted=False, name__icontains=q)
|
||||
|
||||
@@ -41,15 +48,16 @@ async def search_files(
|
||||
files = await query.order_by("-created_at").limit(100)
|
||||
return [await FileOut.from_tortoise_orm(f) for f in files]
|
||||
|
||||
|
||||
@router.get("/folders", response_model=List[FolderOut])
|
||||
async def search_folders(
|
||||
q: str = Query(..., min_length=1, description="Search query"),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
folders = await Folder.filter(
|
||||
owner=current_user,
|
||||
is_deleted=False,
|
||||
name__icontains=q
|
||||
).order_by("-created_at").limit(100)
|
||||
folders = (
|
||||
await Folder.filter(owner=current_user, is_deleted=False, name__icontains=q)
|
||||
.order_by("-created_at")
|
||||
.limit(100)
|
||||
)
|
||||
|
||||
return [await FolderOut.from_tortoise_orm(folder) for folder in folders]
|
||||
|
||||
+102
-37
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Optional, List
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..models import User, File, Folder, Share
|
||||
@@ -17,25 +17,44 @@ router = APIRouter(
|
||||
tags=["shares"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=ShareOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_share_link(share_in: ShareCreate, current_user: User = Depends(get_current_user)):
|
||||
async def create_share_link(
|
||||
share_in: ShareCreate, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
if not share_in.file_id and not share_in.folder_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Either file_id or folder_id must be provided")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Either file_id or folder_id must be provided",
|
||||
)
|
||||
if share_in.file_id and share_in.folder_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot share both a file and a folder simultaneously")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot share both a file and a folder simultaneously",
|
||||
)
|
||||
|
||||
file = None
|
||||
folder = None
|
||||
|
||||
if share_in.file_id:
|
||||
file = await File.get_or_none(id=share_in.file_id, owner=current_user, is_deleted=False)
|
||||
file = await File.get_or_none(
|
||||
id=share_in.file_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found or does not belong to you")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="File not found or does not belong to you",
|
||||
)
|
||||
|
||||
if share_in.folder_id:
|
||||
folder = await Folder.get_or_none(id=share_in.folder_id, owner=current_user, is_deleted=False)
|
||||
folder = await Folder.get_or_none(
|
||||
id=share_in.folder_id, owner=current_user, is_deleted=False
|
||||
)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found or does not belong to you")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Folder not found or does not belong to you",
|
||||
)
|
||||
|
||||
token = secrets.token_urlsafe(16)
|
||||
hashed_password = None
|
||||
@@ -60,8 +79,14 @@ async def create_share_link(share_in: ShareCreate, current_user: User = Depends(
|
||||
item_type = "file" if file else "folder"
|
||||
item_name = file.name if file else folder.name
|
||||
|
||||
expiry_text = f" until {share_in.expires_at.strftime('%Y-%m-%d %H:%M')}" if share_in.expires_at else ""
|
||||
password_text = f"\n\nPassword: {share_in.password}" if share_in.password else ""
|
||||
expiry_text = (
|
||||
f" until {share_in.expires_at.strftime('%Y-%m-%d %H:%M')}"
|
||||
if share_in.expires_at
|
||||
else ""
|
||||
)
|
||||
password_text = (
|
||||
f"\n\nPassword: {share_in.password}" if share_in.password else ""
|
||||
)
|
||||
|
||||
email_body = f"""Hello,
|
||||
|
||||
@@ -95,80 +120,103 @@ MyWebdav File Sharing Service"""
|
||||
to_email=share_in.invite_email,
|
||||
subject=f"{current_user.username} shared {item_name} with you",
|
||||
body=email_body,
|
||||
html=email_html
|
||||
html=email_html,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send invitation email: {e}")
|
||||
|
||||
return await ShareOut.from_tortoise_orm(share)
|
||||
|
||||
|
||||
@router.get("/my", response_model=List[ShareOut])
|
||||
async def list_my_shares(current_user: User = Depends(get_current_user)):
|
||||
shares = await Share.filter(owner=current_user).order_by("-created_at")
|
||||
return [await ShareOut.from_tortoise_orm(share) for share in shares]
|
||||
|
||||
|
||||
@router.get("/{share_token}", response_model=ShareOut)
|
||||
async def get_share_link_info(share_token: str):
|
||||
share = await Share.get_or_none(token=share_token)
|
||||
if not share:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found"
|
||||
)
|
||||
|
||||
if share.expires_at and share.expires_at < datetime.utcnow():
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Share link has expired")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE, detail="Share link has expired"
|
||||
)
|
||||
|
||||
# Increment access count
|
||||
share.access_count += 1
|
||||
await share.save()
|
||||
|
||||
return await ShareOut.from_tortoise_orm(share)
|
||||
|
||||
|
||||
@router.put("/{share_id}", response_model=ShareOut)
|
||||
async def update_share(share_id: int, share_in: ShareCreate, current_user: User = Depends(get_current_user)):
|
||||
async def update_share(
|
||||
share_id: int, share_in: ShareCreate, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
share = await Share.get_or_none(id=share_id, owner=current_user)
|
||||
if not share:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found or does not belong to you")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Share link not found or does not belong to you",
|
||||
)
|
||||
|
||||
if share_in.expires_at is not None:
|
||||
share.expires_at = share_in.expires_at
|
||||
|
||||
|
||||
if share_in.password is not None:
|
||||
share.hashed_password = get_password_hash(share_in.password)
|
||||
share.password_protected = True
|
||||
elif share_in.password == "": # Allow clearing password
|
||||
elif share_in.password == "": # Allow clearing password
|
||||
share.hashed_password = None
|
||||
share.password_protected = False
|
||||
|
||||
if share_in.permission_level is not None:
|
||||
share.permission_level = share_in.permission_level
|
||||
|
||||
|
||||
await share.save()
|
||||
return await ShareOut.from_tortoise_orm(share)
|
||||
|
||||
|
||||
@router.post("/{share_token}/access")
|
||||
async def access_shared_content(share_token: str, password: Optional[str] = None):
|
||||
share = await Share.get_or_none(token=share_token)
|
||||
if not share:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found"
|
||||
)
|
||||
|
||||
if share.expires_at and share.expires_at < datetime.utcnow():
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Share link has expired")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE, detail="Share link has expired"
|
||||
)
|
||||
|
||||
if share.password_protected:
|
||||
if not password or not verify_password(password, share.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect password")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect password"
|
||||
)
|
||||
|
||||
result = {"message": "Access granted", "permission_level": share.permission_level}
|
||||
|
||||
if share.file_id:
|
||||
file = await File.get_or_none(id=share.file_id, is_deleted=False)
|
||||
if not file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
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)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found"
|
||||
)
|
||||
result["folder"] = await FolderOut.from_tortoise_orm(folder)
|
||||
result["type"] = "folder"
|
||||
|
||||
@@ -179,29 +227,42 @@ async def access_shared_content(share_token: str, password: Optional[str] = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{share_token}/download")
|
||||
async def download_shared_file(share_token: str, password: Optional[str] = None):
|
||||
share = await Share.get_or_none(token=share_token)
|
||||
if not share:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found"
|
||||
)
|
||||
|
||||
if share.expires_at and share.expires_at < datetime.utcnow():
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Share link has expired")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE, detail="Share link has expired"
|
||||
)
|
||||
|
||||
if share.password_protected:
|
||||
if not password or not verify_password(password, share.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect password")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect password"
|
||||
)
|
||||
|
||||
if not share.file_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="This share is not for a file")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This share is not for a file",
|
||||
)
|
||||
|
||||
file = await File.get_or_none(id=share.file_id, is_deleted=False)
|
||||
if not file:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
|
||||
)
|
||||
|
||||
owner = await User.get(id=file.owner_id)
|
||||
|
||||
try:
|
||||
|
||||
async def file_iterator():
|
||||
async for chunk in storage_manager.get_file(owner.id, file.path):
|
||||
yield chunk
|
||||
@@ -209,18 +270,22 @@ async def download_shared_file(share_token: str, password: Optional[str] = None)
|
||||
return StreamingResponse(
|
||||
content=file_iterator(),
|
||||
media_type=file.mime_type,
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{file.name}"'
|
||||
}
|
||||
headers={"Content-Disposition": f'attachment; filename="{file.name}"'},
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="File not found in storage")
|
||||
|
||||
|
||||
@router.delete("/{share_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_share_link(share_id: int, current_user: User = Depends(get_current_user)):
|
||||
async def delete_share_link(
|
||||
share_id: int, current_user: User = Depends(get_current_user)
|
||||
):
|
||||
share = await Share.get_or_none(id=share_id, owner=current_user)
|
||||
if not share:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Share link not found or does not belong to you")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Share link not found or does not belong to you",
|
||||
)
|
||||
|
||||
await share.delete()
|
||||
return
|
||||
|
||||
@@ -11,18 +11,29 @@ router = APIRouter(
|
||||
tags=["starred"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/files", response_model=List[FileOut])
|
||||
async def list_starred_files(current_user: User = Depends(get_current_user)):
|
||||
files = await File.filter(owner=current_user, is_starred=True, is_deleted=False).order_by("name")
|
||||
files = await File.filter(
|
||||
owner=current_user, is_starred=True, is_deleted=False
|
||||
).order_by("name")
|
||||
return [await FileOut.from_tortoise_orm(f) for f in files]
|
||||
|
||||
|
||||
@router.get("/folders", response_model=List[FolderOut])
|
||||
async def list_starred_folders(current_user: User = Depends(get_current_user)):
|
||||
folders = await Folder.filter(owner=current_user, is_starred=True, is_deleted=False).order_by("name")
|
||||
folders = await Folder.filter(
|
||||
owner=current_user, is_starred=True, is_deleted=False
|
||||
).order_by("name")
|
||||
return [await FolderOut.from_tortoise_orm(f) for f in folders]
|
||||
|
||||
@router.get("/all", response_model=List[FileOut]) # This will return files and folders as files for now
|
||||
|
||||
@router.get(
|
||||
"/all", response_model=List[FileOut]
|
||||
) # This will return files and folders as files for now
|
||||
async def list_all_starred(current_user: User = Depends(get_current_user)):
|
||||
starred_files = await File.filter(owner=current_user, is_starred=True, is_deleted=False).order_by("name")
|
||||
starred_files = await File.filter(
|
||||
owner=current_user, is_starred=True, is_deleted=False
|
||||
).order_by("name")
|
||||
# For simplicity, we'll return files only for now. A more complex solution would involve a union or a custom schema.
|
||||
return [await FileOut.from_tortoise_orm(f) for f in starred_files]
|
||||
return [await FileOut.from_tortoise_orm(f) for f in starred_files]
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..auth import get_current_user
|
||||
from ..models import User_Pydantic, User, File, Folder
|
||||
from typing import List, Dict, Any
|
||||
from typing import Dict, Any
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=User_Pydantic)
|
||||
async def read_users_me(current_user: User = Depends(get_current_user)):
|
||||
return await User_Pydantic.from_tortoise_orm(current_user)
|
||||
|
||||
|
||||
@router.get("/me/export", response_model=Dict[str, Any])
|
||||
async def export_my_data(current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
@@ -35,6 +37,7 @@ async def export_my_data(current_user: User = Depends(get_current_user)):
|
||||
# share information, etc., would also be included.
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/me", status_code=204)
|
||||
async def delete_my_account(current_user: User = Depends(get_current_user)):
|
||||
"""
|
||||
@@ -48,5 +51,3 @@ async def delete_my_account(current_user: User = Depends(get_current_user)):
|
||||
# Finally, delete the user account
|
||||
await current_user.delete()
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user