mirror of
https://github.com/open-webui/openapi-servers
synced 2025-06-26 18:17:04 +00:00
re-implement pending_confirmation
This commit is contained in:
parent
8bd8f1d712
commit
569d8905ef
@ -3,5 +3,5 @@ import pathlib
|
||||
|
||||
# Constants
|
||||
ALLOWED_DIRECTORIES = [
|
||||
str(pathlib.Path(os.path.expanduser("~/")).resolve())
|
||||
str(pathlib.Path(os.path.expanduser("~/tmp")).resolve())
|
||||
] # 👈 Replace with your paths
|
@ -7,10 +7,11 @@ from pydantic import BaseModel, Field
|
||||
import os
|
||||
import pathlib
|
||||
import asyncio
|
||||
from typing import List, Optional, Literal
|
||||
from typing import List, Optional, Literal, Dict, Union # Added Dict, Union
|
||||
import difflib
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta # Added timedelta
|
||||
import uuid # Added uuid
|
||||
from config import ALLOWED_DIRECTORIES
|
||||
|
||||
app = FastAPI(
|
||||
@ -124,8 +125,8 @@ class DeletePathRequest(BaseModel):
|
||||
recursive: bool = Field(
|
||||
default=False, description="If true and path is a directory, delete recursively. Required if directory is not empty."
|
||||
)
|
||||
confirm_delete: bool = Field(
|
||||
..., description="Must be explicitly set to true to confirm deletion."
|
||||
confirmation_token: Optional[str] = Field(
|
||||
default=None, description="Token required for confirming deletion after initial request."
|
||||
)
|
||||
|
||||
|
||||
@ -138,6 +139,14 @@ class GetMetadataRequest(BaseModel):
|
||||
path: str = Field(..., description="Path to the file or directory to get metadata for.")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Global state for pending confirmations
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Store pending confirmations: {token: {"path": str, "recursive": bool, "expiry": datetime}}
|
||||
pending_confirmations: Dict[str, Dict] = {}
|
||||
CONFIRMATION_TTL_SECONDS = 60 # Token validity period
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Routes
|
||||
# ------------------------------------------------------------------------------
|
||||
@ -155,6 +164,12 @@ class DiffResponse(BaseModel):
|
||||
diff: str = Field(..., description="Unified diff output comparing original and modified content.")
|
||||
|
||||
|
||||
class ConfirmationRequiredResponse(BaseModel):
|
||||
message: str = Field(..., description="Message indicating confirmation is required.")
|
||||
confirmation_token: str = Field(..., description="Token needed for the confirmation step.")
|
||||
expires_at: datetime = Field(..., description="UTC timestamp when the token expires.")
|
||||
|
||||
|
||||
@app.post("/read_file", response_model=ReadFileResponse, summary="Read a file") # Changed response_class to response_model
|
||||
async def read_file(data: ReadFileRequest = Body(...)):
|
||||
"""
|
||||
@ -188,7 +203,7 @@ async def write_file(data: WriteFileRequest = Body(...)):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to write to {data.path}: {str(e)}")
|
||||
|
||||
|
||||
from typing import Union # Add this import at the top with other typing imports
|
||||
# Removed duplicate import - already added Union at the top
|
||||
|
||||
@app.post(
|
||||
"/edit_file",
|
||||
@ -326,51 +341,99 @@ async def search_files(data: SearchFilesRequest = Body(...)):
|
||||
return {"matches": results or ["No matches found"]}
|
||||
|
||||
|
||||
@app.post("/delete_path", response_model=SuccessResponse, summary="Delete a file or directory")
|
||||
@app.post(
|
||||
"/delete_path",
|
||||
response_model=Union[SuccessResponse, ConfirmationRequiredResponse], # Updated response model
|
||||
summary="Delete a file or directory (two-step confirmation)"
|
||||
)
|
||||
async def delete_path(data: DeletePathRequest = Body(...)):
|
||||
"""
|
||||
Delete a specified file or directory. Requires explicit confirmation.
|
||||
Delete a specified file or directory using a two-step confirmation process.
|
||||
|
||||
1. Initial request (without confirmation_token): Returns a confirmation token.
|
||||
2. Confirmation request (with token): Executes the deletion if the token is valid
|
||||
and matches the original request parameters (path, recursive).
|
||||
|
||||
Use 'recursive=True' to delete non-empty directories.
|
||||
Returns JSON success message.
|
||||
"""
|
||||
if not data.confirm_delete:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Deletion not confirmed. Set 'confirm_delete' to true to proceed."
|
||||
)
|
||||
|
||||
path = normalize_path(data.path)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Path not found: {data.path}")
|
||||
# --- Step 2: Confirmation Request ---
|
||||
if data.confirmation_token:
|
||||
if data.confirmation_token not in pending_confirmations:
|
||||
raise HTTPException(status_code=400, detail="Invalid or expired confirmation token.")
|
||||
|
||||
if path.is_file():
|
||||
path.unlink() # Raises FileNotFoundError if not exists, PermissionError if no permission
|
||||
return SuccessResponse(message=f"Successfully deleted file: {data.path}")
|
||||
elif path.is_dir():
|
||||
if data.recursive:
|
||||
shutil.rmtree(path) # Raises FileNotFoundError, PermissionError, NotADirectoryError
|
||||
return SuccessResponse(message=f"Successfully deleted directory recursively: {data.path}")
|
||||
confirmation_data = pending_confirmations[data.confirmation_token]
|
||||
|
||||
# Validate token expiry
|
||||
if now > confirmation_data["expiry"]:
|
||||
del pending_confirmations[data.confirmation_token] # Clean up expired token
|
||||
raise HTTPException(status_code=400, detail="Confirmation token has expired.")
|
||||
|
||||
# Validate request parameters match
|
||||
if confirmation_data["path"] != data.path or confirmation_data["recursive"] != data.recursive:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Request parameters (path, recursive) do not match the original request for this token."
|
||||
)
|
||||
|
||||
# --- Parameters match and token is valid: Proceed with deletion ---
|
||||
del pending_confirmations[data.confirmation_token] # Consume the token
|
||||
|
||||
try:
|
||||
if not path.exists():
|
||||
# Path might have been deleted between requests, treat as success or specific error?
|
||||
# For now, raise 404 as it doesn't exist *now*.
|
||||
raise HTTPException(status_code=404, detail=f"Path not found: {data.path}")
|
||||
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
return SuccessResponse(message=f"Successfully deleted file: {data.path}")
|
||||
elif path.is_dir():
|
||||
if data.recursive:
|
||||
shutil.rmtree(path)
|
||||
return SuccessResponse(message=f"Successfully deleted directory recursively: {data.path}")
|
||||
else:
|
||||
try:
|
||||
path.rmdir()
|
||||
return SuccessResponse(message=f"Successfully deleted empty directory: {data.path}")
|
||||
except OSError as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Directory not empty. Use 'recursive=True' to delete non-empty directories. Original error: {e}"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
path.rmdir() # Raises FileNotFoundError, OSError (e.g., dir not empty, permission denied)
|
||||
return SuccessResponse(message=f"Successfully deleted empty directory: {data.path}")
|
||||
except OSError as e:
|
||||
# Catch error if directory is not empty and recursive is false
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Directory not empty. Use 'recursive=True' to delete non-empty directories. Original error: {e}"
|
||||
)
|
||||
else:
|
||||
# Should not happen if exists() is true and it's not file/dir, but handle defensively
|
||||
raise HTTPException(status_code=400, detail=f"Path is not a file or directory: {data.path}")
|
||||
raise HTTPException(status_code=400, detail=f"Path is not a file or directory: {data.path}")
|
||||
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied to delete {data.path}")
|
||||
except Exception as e:
|
||||
# Catch other potential errors during deletion
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete {data.path}: {e}")
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied to delete {data.path}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete {data.path}: {e}")
|
||||
|
||||
# --- Step 1: Initial Request (No Token Provided) ---
|
||||
else:
|
||||
# Check if path exists before generating token
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Path not found: {data.path}")
|
||||
|
||||
# Generate token and expiry
|
||||
token = uuid.uuid4().hex
|
||||
expiry_time = now + timedelta(seconds=CONFIRMATION_TTL_SECONDS)
|
||||
|
||||
# Store confirmation details
|
||||
pending_confirmations[token] = {
|
||||
"path": data.path,
|
||||
"recursive": data.recursive,
|
||||
"expiry": expiry_time,
|
||||
}
|
||||
|
||||
# Return confirmation required response
|
||||
return ConfirmationRequiredResponse(
|
||||
message="Confirmation required to delete path. Use the provided token in a subsequent request with the same parameters.",
|
||||
confirmation_token=token,
|
||||
expires_at=expiry_time,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/move_path", response_model=SuccessResponse, summary="Move or rename a file or directory")
|
||||
|
Loading…
Reference in New Issue
Block a user