185 lines
4.8 KiB
Python
185 lines
4.8 KiB
Python
"""Workspace path validation utilities"""
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from backend import load_config
|
|
|
|
|
|
def get_workspace_root() -> Path:
|
|
"""Get workspace root directory from config"""
|
|
cfg = load_config()
|
|
workspace_root = cfg.get("workspace_root", "./workspaces")
|
|
|
|
# Convert to absolute path
|
|
workspace_path = Path(workspace_root)
|
|
if not workspace_path.is_absolute():
|
|
# Relative to project root
|
|
workspace_path = Path(__file__).parent.parent.parent / workspace_root
|
|
|
|
# Create if not exists
|
|
workspace_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
return workspace_path.resolve()
|
|
|
|
|
|
def get_project_path(project_id: str, project_path: str) -> Path:
|
|
"""
|
|
Get absolute path for a project
|
|
|
|
Args:
|
|
project_id: Project ID
|
|
project_path: Relative path stored in database
|
|
|
|
Returns:
|
|
Absolute path to project directory
|
|
"""
|
|
workspace_root = get_workspace_root()
|
|
project_dir = workspace_root / project_path
|
|
|
|
# Create if not exists
|
|
project_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
return project_dir.resolve()
|
|
|
|
|
|
def validate_path_in_project(path: str, project_dir: Path) -> Path:
|
|
"""
|
|
Validate that a path is within the project directory
|
|
|
|
Args:
|
|
path: Path to validate (can be relative or absolute)
|
|
project_dir: Project directory path
|
|
|
|
Returns:
|
|
Resolved absolute path
|
|
|
|
Raises:
|
|
ValueError: If path is outside project directory
|
|
"""
|
|
p = Path(path)
|
|
|
|
# If relative, resolve against project directory
|
|
if not p.is_absolute():
|
|
p = project_dir / p
|
|
|
|
# Resolve to absolute path
|
|
p = p.resolve()
|
|
|
|
# Security check: ensure path is within project directory
|
|
try:
|
|
p.relative_to(project_dir.resolve())
|
|
except ValueError:
|
|
raise ValueError(f"Path '{path}' is outside project directory")
|
|
|
|
return p
|
|
|
|
|
|
def create_project_directory(name: str, user_id: int) -> tuple[str, Path]:
|
|
"""
|
|
Create a new project directory
|
|
|
|
Args:
|
|
name: Project name
|
|
user_id: User ID
|
|
|
|
Returns:
|
|
Tuple of (relative_path, absolute_path)
|
|
"""
|
|
workspace_root = get_workspace_root()
|
|
|
|
# Create user-specific directory
|
|
user_dir = workspace_root / f"user_{user_id}"
|
|
user_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create project directory
|
|
project_dir = user_dir / name
|
|
|
|
# Handle name conflicts
|
|
counter = 1
|
|
original_name = name
|
|
while project_dir.exists():
|
|
name = f"{original_name}_{counter}"
|
|
project_dir = user_dir / name
|
|
counter += 1
|
|
|
|
project_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Return relative path (from workspace root) and absolute path
|
|
relative_path = f"user_{user_id}/{name}"
|
|
return relative_path, project_dir.resolve()
|
|
|
|
|
|
def delete_project_directory(project_path: str) -> bool:
|
|
"""
|
|
Delete a project directory
|
|
|
|
Args:
|
|
project_path: Relative path from workspace root
|
|
|
|
Returns:
|
|
True if deleted successfully
|
|
"""
|
|
workspace_root = get_workspace_root()
|
|
project_dir = workspace_root / project_path
|
|
|
|
if project_dir.exists() and project_dir.is_dir():
|
|
# Verify it's within workspace root (security check)
|
|
try:
|
|
project_dir.resolve().relative_to(workspace_root.resolve())
|
|
shutil.rmtree(project_dir)
|
|
return True
|
|
except ValueError:
|
|
raise ValueError("Cannot delete directory outside workspace root")
|
|
|
|
return False
|
|
|
|
|
|
def save_uploaded_files(files, project_dir: Path) -> dict:
|
|
"""
|
|
Save uploaded files to project directory (for folder upload)
|
|
|
|
Args:
|
|
files: List of FileStorage objects from Flask request.files
|
|
project_dir: Target project directory
|
|
|
|
Returns:
|
|
Dict with upload statistics
|
|
"""
|
|
file_count = 0
|
|
dir_count = 0
|
|
total_size = 0
|
|
|
|
for f in files:
|
|
if not f.filename:
|
|
continue
|
|
|
|
# filename contains relative path like "AlgoLab/src/main.py"
|
|
# Skip the first segment (folder name) since project_dir already represents it
|
|
parts = f.filename.split("/")
|
|
if len(parts) > 1:
|
|
relative = "/".join(parts[1:]) # "src/main.py"
|
|
else:
|
|
relative = f.filename # root-level file
|
|
|
|
target = project_dir / relative
|
|
|
|
# Create parent directories if needed
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Save file
|
|
f.save(str(target))
|
|
file_count += 1
|
|
|
|
# Count new directories
|
|
if target.parent != project_dir:
|
|
dir_count += 1
|
|
|
|
total_size += target.stat().st_size
|
|
|
|
return {
|
|
"files": file_count,
|
|
"directories": dir_count,
|
|
"size": total_size
|
|
}
|
|
|