package models
import (
"errors"
"log"
"path/filepath"
"strings"
"theskyscape.com/repo/skykit"
)
type Project struct {
skykit.Model
OwnerID string
Name string // Display name
Path string // Directory name in workspace (unique per user)
RemoteURL string // Git remote URL (optional)
IsDefault bool // If true, this is the default project
}
func (p *Project) GetModel() *skykit.Model {
return &p.Model
}
// GetProject retrieves a project by ID
func GetProject(id string) (*Project, error) {
return Projects.First("WHERE ID = ?", id)
}
// GetProjectByPath retrieves a project by owner and path
func GetProjectByPath(ownerID, path string) (*Project, error) {
path = NormalizeProjectPath(path)
if path == "" {
return nil, errors.New("path required")
}
return Projects.First("WHERE OwnerID = ? AND Path = ?", ownerID, path)
}
// ListProjects returns all projects for a user
func ListProjects(ownerID string) []*Project {
projects, _ := Projects.Search("WHERE OwnerID = ? ORDER BY Name ASC", ownerID)
return projects
}
// GetDefaultProject returns the default project for a user (or creates one)
func GetDefaultProject(ownerID string) (*Project, error) {
// Try to find existing default
project, err := Projects.First("WHERE OwnerID = ? AND IsDefault = 1", ownerID)
if err == nil && project != nil {
return project, nil
}
// Try to find any project
project, err = Projects.First("WHERE OwnerID = ?", ownerID)
if err == nil && project != nil {
// Make it default
project.IsDefault = true
Projects.Update(project)
return project, nil
}
// Create default home project
return CreateProject(ownerID, "home", "", true)
}
// GetOrCreateHomeProject returns or creates the "home" project for root-level files
func GetOrCreateHomeProject(ownerID string) (*Project, error) {
// Check if "home" project exists
project, err := Projects.First("WHERE OwnerID = ? AND Path = ?", ownerID, "home")
if err == nil && project != nil {
return project, nil
}
// Create home project - it's the default project for dotfiles and orphan files
return CreateProject(ownerID, "home", "", true)
}
// GetOrCreateProject returns existing project by path or creates a new one
func GetOrCreateProject(ownerID, name, path, remoteURL string) (*Project, error) {
// Check if project exists by path
existing, _ := GetProjectByPath(ownerID, path)
if existing != nil {
// Update remote URL if provided and different
if remoteURL != "" && existing.RemoteURL != remoteURL {
existing.RemoteURL = remoteURL
Projects.Update(existing)
}
return existing, nil
}
// Create new project
return CreateProject(ownerID, name, remoteURL, false)
}
// CreateProject creates a new project
func CreateProject(ownerID, name, remoteURL string, isDefault bool) (*Project, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("name required")
}
// Generate path from name
path := NormalizeProjectPath(name)
if path == "" {
return nil, errors.New("invalid project name")
}
// Check if path already exists
existing, _ := GetProjectByPath(ownerID, path)
if existing != nil {
return nil, errors.New("project already exists with this name")
}
// If setting as default, clear other defaults
if isDefault {
clearDefaultProjects(ownerID)
}
project := Projects.New()
project.OwnerID = ownerID
project.Name = name
project.Path = path
project.RemoteURL = remoteURL
project.IsDefault = isDefault
return Projects.Insert(project)
}
// UpdateProject updates a project
func UpdateProject(project *Project) error {
if project.IsDefault {
clearDefaultProjects(project.OwnerID)
}
return Projects.Update(project)
}
// DeleteProject removes a project and all its files
func DeleteProject(project *Project) error {
// Delete all files in this project
prefix := project.Path + "/"
files, err := Files.Search("WHERE OwnerID = ? AND (Path = ? OR Path LIKE ?)",
project.OwnerID, project.Path, prefix+"%")
if err != nil {
log.Printf("DeleteProject: search error: %v", err)
}
log.Printf("DeleteProject: project=%s path=%s owner=%s found %d files to delete",
project.ID, project.Path, project.OwnerID, len(files))
for _, f := range files {
if err := Files.Delete(f); err != nil {
log.Printf("DeleteProject: failed to delete file %s: %v", f.Path, err)
} else {
log.Printf("DeleteProject: deleted file %s", f.Path)
}
}
if err := Projects.Delete(project); err != nil {
log.Printf("DeleteProject: failed to delete project: %v", err)
return err
}
log.Printf("DeleteProject: successfully deleted project %s", project.Path)
return nil
}
// SetDefaultProject sets a project as the default
func SetDefaultProject(ownerID, projectID string) error {
project, err := GetProject(projectID)
if err != nil {
return err
}
if project.OwnerID != ownerID {
return errors.New("project belongs to different user")
}
clearDefaultProjects(ownerID)
project.IsDefault = true
return Projects.Update(project)
}
// clearDefaultProjects clears the default flag from all projects
func clearDefaultProjects(ownerID string) {
projects, _ := Projects.Search("WHERE OwnerID = ? AND IsDefault = 1", ownerID)
for _, p := range projects {
p.IsDefault = false
Projects.Update(p)
}
}
// NormalizeProjectPath converts a project name to a safe directory name
func NormalizeProjectPath(name string) string {
name = strings.TrimSpace(name)
name = strings.ToLower(name)
// Replace spaces with dashes
name = strings.ReplaceAll(name, " ", "-")
// Remove any path separators
name = strings.ReplaceAll(name, "/", "")
name = strings.ReplaceAll(name, "\\", "")
// Clean the path
name = filepath.Clean(name)
if name == "." || name == "" {
return ""
}
return name
}
// GetFilesInProject returns all files for a project
func GetFilesInProject(ownerID, projectPath string) []*File {
projectPath = NormalizeProjectPath(projectPath)
if projectPath == "" {
return nil
}
// Files in project are stored with project path prefix
prefix := projectPath + "/"
files, _ := Files.Search("WHERE OwnerID = ? AND Path LIKE ? ORDER BY Path ASC",
ownerID, prefix+"%")
return files
}
// GetFileInProject retrieves a file within a project
func GetFileInProject(ownerID, projectPath, filePath string) (*File, error) {
projectPath = NormalizeProjectPath(projectPath)
filePath = NormalizePath(filePath)
if projectPath == "" || filePath == "" {
return nil, errors.New("project and file path required")
}
fullPath := projectPath + "/" + filePath
return GetFile(ownerID, fullPath)
}
// SaveFileInProject saves a file within a project
func SaveFileInProject(ownerID, projectPath, filePath, content string) (*File, error) {
projectPath = NormalizeProjectPath(projectPath)
filePath = NormalizePath(filePath)
if projectPath == "" || filePath == "" {
return nil, errors.New("project and file path required")
}
fullPath := projectPath + "/" + filePath
return SaveFile(ownerID, fullPath, content)
}
// DeleteFileInProject deletes a file within a project
func DeleteFileInProject(ownerID, projectPath, filePath string) error {
projectPath = NormalizeProjectPath(projectPath)
filePath = NormalizePath(filePath)
if projectPath == "" || filePath == "" {
return errors.New("project and file path required")
}
fullPath := projectPath + "/" + filePath
return DeleteFile(ownerID, fullPath)
}