329 lines
9.5 KiB
Python
329 lines
9.5 KiB
Python
"""Project management API routes"""
|
|
import os
|
|
import uuid
|
|
import shutil
|
|
from flask import Blueprint, request, jsonify
|
|
from werkzeug.utils import secure_filename
|
|
|
|
from backend import db
|
|
from backend.models import Project, User
|
|
from backend.utils.helpers import ok, err
|
|
from backend.utils.workspace import (
|
|
create_project_directory,
|
|
delete_project_directory,
|
|
get_project_path,
|
|
save_uploaded_files
|
|
)
|
|
|
|
bp = Blueprint("projects", __name__)
|
|
|
|
|
|
@bp.route("/api/projects", methods=["GET"])
|
|
def list_projects():
|
|
"""List all projects for a user"""
|
|
user_id = request.args.get("user_id", type=int)
|
|
|
|
if not user_id:
|
|
return err(400, "Missing user_id parameter")
|
|
|
|
projects = Project.query.filter_by(user_id=user_id).order_by(Project.updated_at.desc()).all()
|
|
|
|
return ok({
|
|
"projects": [
|
|
{
|
|
"id": p.id,
|
|
"name": p.name,
|
|
"path": p.path,
|
|
"description": p.description,
|
|
"created_at": p.created_at.isoformat() if p.created_at else None,
|
|
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
|
"conversation_count": p.conversations.count()
|
|
}
|
|
for p in projects
|
|
],
|
|
"total": len(projects)
|
|
})
|
|
|
|
|
|
@bp.route("/api/projects", methods=["POST"])
|
|
def create_project():
|
|
"""Create a new project"""
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return err(400, "No data provided")
|
|
|
|
user_id = data.get("user_id")
|
|
name = data.get("name", "").strip()
|
|
description = data.get("description", "")
|
|
|
|
if not user_id:
|
|
return err(400, "Missing user_id")
|
|
|
|
if not name:
|
|
return err(400, "Project name is required")
|
|
|
|
# Check if user exists
|
|
user = User.query.get(user_id)
|
|
if not user:
|
|
return err(404, "User not found")
|
|
|
|
# Check if project name already exists for this user
|
|
existing = Project.query.filter_by(user_id=user_id, name=name).first()
|
|
if existing:
|
|
return err(400, f"Project '{name}' already exists")
|
|
|
|
# Create project directory
|
|
try:
|
|
relative_path, absolute_path = create_project_directory(name, user_id)
|
|
except Exception as e:
|
|
return err(500, f"Failed to create project directory: {str(e)}")
|
|
|
|
# Create project record
|
|
project = Project(
|
|
id=str(uuid.uuid4()),
|
|
user_id=user_id,
|
|
name=name,
|
|
path=relative_path,
|
|
description=description
|
|
)
|
|
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
return ok({
|
|
"id": project.id,
|
|
"name": project.name,
|
|
"path": project.path,
|
|
"description": project.description,
|
|
"created_at": project.created_at.isoformat()
|
|
})
|
|
|
|
|
|
@bp.route("/api/projects/<project_id>", methods=["GET"])
|
|
def get_project(project_id):
|
|
"""Get project details"""
|
|
project = Project.query.get(project_id)
|
|
|
|
if not project:
|
|
return err(404, "Project not found")
|
|
|
|
# Get absolute path
|
|
absolute_path = get_project_path(project.id, project.path)
|
|
|
|
# Get directory statistics
|
|
file_count = sum(1 for _ in absolute_path.rglob("*") if _.is_file())
|
|
dir_count = sum(1 for _ in absolute_path.rglob("*") if _.is_dir())
|
|
total_size = sum(f.stat().st_size for f in absolute_path.rglob("*") if f.is_file())
|
|
|
|
return ok({
|
|
"id": project.id,
|
|
"name": project.name,
|
|
"path": project.path,
|
|
"absolute_path": str(absolute_path),
|
|
"description": project.description,
|
|
"created_at": project.created_at.isoformat() if project.created_at else None,
|
|
"updated_at": project.updated_at.isoformat() if project.updated_at else None,
|
|
"conversation_count": project.conversations.count(),
|
|
"stats": {
|
|
"files": file_count,
|
|
"directories": dir_count,
|
|
"total_size": total_size
|
|
}
|
|
})
|
|
|
|
|
|
@bp.route("/api/projects/<project_id>", methods=["PUT"])
|
|
def update_project(project_id):
|
|
"""Update project details"""
|
|
project = Project.query.get(project_id)
|
|
|
|
if not project:
|
|
return err(404, "Project not found")
|
|
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return err(400, "No data provided")
|
|
|
|
# Update name if provided
|
|
if "name" in data:
|
|
name = data["name"].strip()
|
|
if not name:
|
|
return err(400, "Project name cannot be empty")
|
|
|
|
# Check if new name conflicts with existing project
|
|
existing = Project.query.filter(
|
|
Project.user_id == project.user_id,
|
|
Project.name == name,
|
|
Project.id != project_id
|
|
).first()
|
|
|
|
if existing:
|
|
return err(400, f"Project '{name}' already exists")
|
|
|
|
project.name = name
|
|
|
|
# Update description if provided
|
|
if "description" in data:
|
|
project.description = data["description"]
|
|
|
|
db.session.commit()
|
|
|
|
return ok({
|
|
"id": project.id,
|
|
"name": project.name,
|
|
"description": project.description,
|
|
"updated_at": project.updated_at.isoformat()
|
|
})
|
|
|
|
|
|
@bp.route("/api/projects/<project_id>", methods=["DELETE"])
|
|
def delete_project(project_id):
|
|
"""Delete a project"""
|
|
project = Project.query.get(project_id)
|
|
|
|
if not project:
|
|
return err(404, "Project not found")
|
|
|
|
# Delete project directory
|
|
try:
|
|
delete_project_directory(project.path)
|
|
except Exception as e:
|
|
return err(500, f"Failed to delete project directory: {str(e)}")
|
|
|
|
# Delete project record (cascades to conversations and messages)
|
|
db.session.delete(project)
|
|
db.session.commit()
|
|
|
|
return ok({"message": "Project deleted successfully"})
|
|
|
|
|
|
@bp.route("/api/projects/upload", methods=["POST"])
|
|
def upload_project_folder():
|
|
"""Upload a folder as a new project via file upload"""
|
|
user_id = request.form.get("user_id", type=int)
|
|
project_name = request.form.get("name", "").strip()
|
|
description = request.form.get("description", "")
|
|
|
|
files = request.files.getlist("files")
|
|
|
|
if not user_id:
|
|
return err(400, "Missing user_id")
|
|
|
|
if not files:
|
|
return err(400, "No files uploaded")
|
|
|
|
if not project_name:
|
|
# Use first file's top-level folder name
|
|
project_name = files[0].filename.split("/")[0] if files[0].filename else "untitled"
|
|
|
|
# Check if user exists
|
|
user = User.query.get(user_id)
|
|
if not user:
|
|
return err(404, "User not found")
|
|
|
|
# Check if project name already exists
|
|
existing = Project.query.filter_by(user_id=user_id, name=project_name).first()
|
|
if existing:
|
|
return err(400, f"Project '{project_name}' already exists")
|
|
|
|
# Create project directory first
|
|
try:
|
|
relative_path, absolute_path = create_project_directory(project_name, user_id)
|
|
except Exception as e:
|
|
return err(500, f"Failed to create project directory: {str(e)}")
|
|
|
|
# Write uploaded files to project directory
|
|
try:
|
|
stats = save_uploaded_files(files, absolute_path)
|
|
except Exception as e:
|
|
shutil.rmtree(absolute_path, ignore_errors=True)
|
|
return err(500, f"Failed to save uploaded files: {str(e)}")
|
|
|
|
# Create project record
|
|
project = Project(
|
|
id=str(uuid.uuid4()),
|
|
user_id=user_id,
|
|
name=project_name,
|
|
path=relative_path,
|
|
description=description
|
|
)
|
|
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
return ok({
|
|
"id": project.id,
|
|
"name": project.name,
|
|
"path": project.path,
|
|
"description": project.description,
|
|
"created_at": project.created_at.isoformat(),
|
|
"stats": stats
|
|
})
|
|
|
|
|
|
@bp.route("/api/projects/<project_id>/files", methods=["GET"])
|
|
def list_project_files(project_id):
|
|
"""List files in a project directory"""
|
|
project = Project.query.get(project_id)
|
|
|
|
if not project:
|
|
return err(404, "Project not found")
|
|
|
|
project_dir = get_project_path(project.id, project.path)
|
|
|
|
# Get subdirectory parameter
|
|
subdir = request.args.get("path", "")
|
|
|
|
try:
|
|
target_dir = project_dir / subdir if subdir else project_dir
|
|
target_dir = target_dir.resolve()
|
|
|
|
# Validate path is within project
|
|
target_dir.relative_to(project_dir.resolve())
|
|
except ValueError:
|
|
return err(403, "Invalid path: outside project directory")
|
|
|
|
if not target_dir.exists():
|
|
return err(404, "Directory not found")
|
|
|
|
if not target_dir.is_dir():
|
|
return err(400, "Path is not a directory")
|
|
|
|
# List files
|
|
files = []
|
|
directories = []
|
|
|
|
try:
|
|
for item in target_dir.iterdir():
|
|
# Skip hidden files
|
|
if item.name.startswith("."):
|
|
continue
|
|
|
|
relative_path = item.relative_to(project_dir)
|
|
|
|
if item.is_file():
|
|
files.append({
|
|
"name": item.name,
|
|
"path": str(relative_path),
|
|
"size": item.stat().st_size,
|
|
"extension": item.suffix
|
|
})
|
|
elif item.is_dir():
|
|
directories.append({
|
|
"name": item.name,
|
|
"path": str(relative_path)
|
|
})
|
|
except Exception as e:
|
|
return err(500, f"Failed to list directory: {str(e)}")
|
|
|
|
return ok({
|
|
"project_id": project_id,
|
|
"current_path": str(subdir) if subdir else "/",
|
|
"files": files,
|
|
"directories": directories,
|
|
"total_files": len(files),
|
|
"total_dirs": len(directories)
|
|
})
|