Connor McCutcheon
/ SkyCode
project.go
go
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)
}
No comments yet.