394 lines
9.8 KiB
Go
394 lines
9.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"verstak/internal/core/actions"
|
|
"verstak/internal/core/activity"
|
|
"verstak/internal/core/config"
|
|
"verstak/internal/core/files"
|
|
"verstak/internal/core/nodes"
|
|
"verstak/internal/core/notes"
|
|
"verstak/internal/core/plugins"
|
|
"verstak/internal/core/search"
|
|
"verstak/internal/core/storage"
|
|
syncsvc "verstak/internal/core/sync"
|
|
"verstak/internal/core/templates"
|
|
"verstak/internal/core/vault"
|
|
"verstak/internal/core/worklog"
|
|
)
|
|
|
|
// StartupStatus describes the application startup state.
|
|
type StartupStatus struct {
|
|
Status string `json:"status"` // "first_run", "recovery", "ready"
|
|
VaultPath string `json:"vaultPath"` // configured or default vault path
|
|
VaultExists bool `json:"vaultExists"` // whether vault.db exists at the path
|
|
DefaultPath string `json:"defaultPath"` // default vault path suggestion
|
|
Error string `json:"error,omitempty"`
|
|
AppConfig *config.AppConfig `json:"appConfig,omitempty"`
|
|
}
|
|
|
|
// GetStartupStatus checks the global config and vault state.
|
|
func (a *App) GetStartupStatus() (*StartupStatus, error) {
|
|
defaultPath, _ := config.DefaultVaultPath()
|
|
|
|
appCfg, err := config.LoadAppConfig()
|
|
if err != nil {
|
|
return &StartupStatus{
|
|
Status: "first_run",
|
|
DefaultPath: defaultPath,
|
|
Error: fmt.Sprintf("config load error: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// No config at all → first run
|
|
if appCfg == nil {
|
|
return &StartupStatus{
|
|
Status: "first_run",
|
|
DefaultPath: defaultPath,
|
|
}, nil
|
|
}
|
|
|
|
// Config says not completed → first run
|
|
if !appCfg.FirstRunCompleted {
|
|
return &StartupStatus{
|
|
Status: "first_run",
|
|
VaultPath: appCfg.VaultPath,
|
|
DefaultPath: defaultPath,
|
|
}, nil
|
|
}
|
|
|
|
// Config has no vault path → first run
|
|
if appCfg.VaultPath == "" {
|
|
appCfg.VaultPath = defaultPath
|
|
_ = config.SaveAppConfig(appCfg)
|
|
}
|
|
|
|
// Check if vault exists
|
|
vaultExists := vaultExistsAt(appCfg.VaultPath)
|
|
if !vaultExists {
|
|
return &StartupStatus{
|
|
Status: "recovery",
|
|
VaultPath: appCfg.VaultPath,
|
|
DefaultPath: defaultPath,
|
|
AppConfig: appCfg,
|
|
}, nil
|
|
}
|
|
|
|
return &StartupStatus{
|
|
Status: "ready",
|
|
VaultPath: appCfg.VaultPath,
|
|
VaultExists: true,
|
|
AppConfig: appCfg,
|
|
}, nil
|
|
}
|
|
|
|
func vaultExistsAt(vaultPath string) bool {
|
|
dbPath := filepath.Join(vaultPath, ".verstak", "index.db")
|
|
if _, err := os.Stat(dbPath); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// CreateVault creates a new vault at the given path and initializes all services.
|
|
func (a *App) CreateVault(vaultPath string) (*StartupStatus, error) {
|
|
if vaultPath == "" {
|
|
return nil, fmt.Errorf("vault path is empty")
|
|
}
|
|
|
|
// Create vault directories and database
|
|
if err := vault.Init(vaultPath); err != nil {
|
|
return nil, fmt.Errorf("create vault: %w", err)
|
|
}
|
|
|
|
// Initialize services for this vault
|
|
if err := a.initVault(vaultPath); err != nil {
|
|
return nil, fmt.Errorf("init vault services: %w", err)
|
|
}
|
|
|
|
// Save global config
|
|
appCfg, err := config.LoadAppConfig()
|
|
if err != nil || appCfg == nil {
|
|
appCfg = config.DefaultAppConfig()
|
|
}
|
|
appCfg.VaultPath = vaultPath
|
|
appCfg.FirstRunCompleted = true
|
|
if err := config.SaveAppConfig(appCfg); err != nil {
|
|
return nil, fmt.Errorf("save config: %w", err)
|
|
}
|
|
|
|
log.Printf("[startup] vault created at %s", vaultPath)
|
|
return &StartupStatus{
|
|
Status: "ready",
|
|
VaultPath: vaultPath,
|
|
VaultExists: true,
|
|
AppConfig: appCfg,
|
|
}, nil
|
|
}
|
|
|
|
// OpenVault opens an existing vault and initializes services.
|
|
func (a *App) OpenVault(vaultPath string) (*StartupStatus, error) {
|
|
if vaultPath == "" {
|
|
return nil, fmt.Errorf("vault path is empty")
|
|
}
|
|
if !vaultExistsAt(vaultPath) {
|
|
return nil, fmt.Errorf("vault not found at %s", vaultPath)
|
|
}
|
|
|
|
if err := a.initVault(vaultPath); err != nil {
|
|
return nil, fmt.Errorf("init vault: %w", err)
|
|
}
|
|
|
|
// Update config
|
|
appCfg, err := config.LoadAppConfig()
|
|
if err != nil || appCfg == nil {
|
|
appCfg = config.DefaultAppConfig()
|
|
}
|
|
appCfg.VaultPath = vaultPath
|
|
appCfg.FirstRunCompleted = true
|
|
if err := config.SaveAppConfig(appCfg); err != nil {
|
|
return nil, fmt.Errorf("save config: %w", err)
|
|
}
|
|
|
|
log.Printf("[startup] vault opened at %s", vaultPath)
|
|
return &StartupStatus{
|
|
Status: "ready",
|
|
VaultPath: vaultPath,
|
|
VaultExists: true,
|
|
AppConfig: appCfg,
|
|
}, nil
|
|
}
|
|
|
|
// initVault opens the vault DB and initializes all core services.
|
|
func (a *App) initVault(vaultPath string) error {
|
|
// Close previous vault if any
|
|
a.closeVault()
|
|
|
|
abs, err := filepath.Abs(vaultPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
|
db, err := storage.Open(dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open db: %w", err)
|
|
}
|
|
|
|
nodeRepo := nodes.NewRepository(db)
|
|
fileSvc := files.NewService(db, abs, nodeRepo)
|
|
noteSvc := notes.NewService(db, abs, nodeRepo, fileSvc)
|
|
actionSvc := actions.NewService(db)
|
|
activitySvc := activity.NewService(db)
|
|
worklogSvc := worklog.NewService(db)
|
|
searchSvc := search.NewService(db)
|
|
pm := plugins.NewManager(abs)
|
|
pm.Discover()
|
|
|
|
templatesReg := templates.NewRegistry()
|
|
if err := templatesReg.LoadSystem(); err != nil {
|
|
log.Printf("warning: failed to load system templates: %v", err)
|
|
}
|
|
|
|
// Apply enabled templates from config
|
|
appCfg, _ := config.LoadAppConfig()
|
|
if appCfg != nil && len(appCfg.EnabledTemplates) > 0 {
|
|
enabledSet := make(map[string]bool)
|
|
for _, id := range appCfg.EnabledTemplates {
|
|
enabledSet[id] = true
|
|
}
|
|
for _, t := range templatesReg.All() {
|
|
if !enabledSet[t.ID] {
|
|
_ = templatesReg.Disable(t.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sync service
|
|
deviceID := ""
|
|
_ = appCfg // will store sync settings
|
|
if appCfg != nil && appCfg.Vault.Sync.DeviceID != "" {
|
|
deviceID = appCfg.Vault.Sync.DeviceID
|
|
}
|
|
if deviceID == "" {
|
|
deviceID = "gui-" + abs[:8]
|
|
}
|
|
syncSvc := syncsvc.NewService(db, deviceID)
|
|
|
|
a.mu.Lock()
|
|
a.db = db
|
|
a.nodes = nodeRepo
|
|
a.files = fileSvc
|
|
a.notes = noteSvc
|
|
a.activity = activitySvc
|
|
a.actions = actionSvc
|
|
a.worklog = worklogSvc
|
|
a.search = searchSvc
|
|
a.plugins = pm
|
|
a.templates = templatesReg
|
|
a.sync = syncSvc
|
|
a.vault = abs
|
|
a.vaultOpen = true
|
|
a.mu.Unlock()
|
|
|
|
// Start auto-sync loop
|
|
go a.autoSyncLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
// closeVault shuts down current vault services if any.
|
|
func (a *App) closeVault() {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
if !a.vaultOpen {
|
|
return
|
|
}
|
|
if a.db != nil {
|
|
a.db.Close()
|
|
}
|
|
a.db = nil
|
|
a.nodes = nil
|
|
a.files = nil
|
|
a.notes = nil
|
|
a.activity = nil
|
|
a.actions = nil
|
|
a.worklog = nil
|
|
a.search = nil
|
|
a.plugins = nil
|
|
a.templates = nil
|
|
a.sync = nil
|
|
a.vault = ""
|
|
a.vaultOpen = false
|
|
}
|
|
|
|
// IsReady returns true if a vault is open and services are initialized.
|
|
func (a *App) IsReady() bool {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
return a.vaultOpen
|
|
}
|
|
|
|
// GetAppConfig returns the current global app config.
|
|
func (a *App) GetAppConfig() (*config.AppConfig, error) {
|
|
cfg, err := config.LoadAppConfig()
|
|
if err != nil {
|
|
return config.DefaultAppConfig(), nil
|
|
}
|
|
if cfg == nil {
|
|
return config.DefaultAppConfig(), nil
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// SaveAppConfig saves the global app config.
|
|
func (a *App) SaveAppConfig(cfg *config.AppConfig) error {
|
|
return config.SaveAppConfig(cfg)
|
|
}
|
|
|
|
// GetDefaultVaultPath returns the default vault path.
|
|
func (a *App) GetDefaultVaultPath() (string, error) {
|
|
return config.DefaultVaultPath()
|
|
}
|
|
|
|
// CheckVaultPath checks whether a given path is usable as a vault.
|
|
type CheckVaultPathResult struct {
|
|
Exists bool `json:"exists"`
|
|
HasVault bool `json:"hasVault"`
|
|
Writable bool `json:"writable"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (a *App) CheckVaultPath(vaultPath string) (*CheckVaultPathResult, error) {
|
|
if vaultPath == "" {
|
|
return nil, fmt.Errorf("path is empty")
|
|
}
|
|
info, err := os.Stat(vaultPath)
|
|
exists := err == nil
|
|
hasVault := false
|
|
writable := false
|
|
|
|
if exists {
|
|
if info.IsDir() {
|
|
writable = checkDirWritable(vaultPath)
|
|
hasVault = vaultExistsAt(vaultPath)
|
|
}
|
|
} else {
|
|
// Path doesn't exist - check if parent is writable
|
|
parent := filepath.Dir(vaultPath)
|
|
parentInfo, pErr := os.Stat(parent)
|
|
if pErr == nil && parentInfo.IsDir() {
|
|
writable = checkDirWritable(parent)
|
|
}
|
|
}
|
|
|
|
desc := describeVaultPath(vaultPath, exists, hasVault)
|
|
|
|
return &CheckVaultPathResult{
|
|
Exists: exists,
|
|
HasVault: hasVault,
|
|
Writable: writable,
|
|
Description: desc,
|
|
}, nil
|
|
}
|
|
|
|
func checkDirWritable(dir string) bool {
|
|
testFile := filepath.Join(dir, ".verstak-write-test")
|
|
if err := os.WriteFile(testFile, []byte{}, 0o640); err != nil {
|
|
return false
|
|
}
|
|
os.Remove(testFile)
|
|
return true
|
|
}
|
|
|
|
func describeVaultPath(path string, exists, hasVault bool) string {
|
|
if !exists {
|
|
return "Путь не существует. Будет создан новый vault."
|
|
}
|
|
if hasVault {
|
|
return "Найден существующий vault. Можно подключиться."
|
|
}
|
|
return "Папка существует, но vault не найден. Можно создать новый vault."
|
|
}
|
|
|
|
// VaultInfo returns information about the currently open vault.
|
|
type VaultInfo struct {
|
|
Path string `json:"path"`
|
|
DBPath string `json:"dbPath"`
|
|
FilesPath string `json:"filesPath"`
|
|
TrashPath string `json:"trashPath"`
|
|
Healthy bool `json:"healthy"`
|
|
NodeCount int `json:"nodeCount"`
|
|
FileCount int `json:"fileCount"`
|
|
}
|
|
|
|
func (a *App) GetVaultInfo() (*VaultInfo, error) {
|
|
if !a.IsReady() {
|
|
return nil, fmt.Errorf("vault not open")
|
|
}
|
|
a.mu.RLock()
|
|
vp := a.vault
|
|
nodesCount := 0
|
|
if a.nodes != nil {
|
|
roots, _ := a.nodes.ListRoots(true)
|
|
nodesCount = len(roots)
|
|
}
|
|
fileCount := 0
|
|
_ = a.db.QueryRow("SELECT COUNT(*) FROM files").Scan(&fileCount)
|
|
a.mu.RUnlock()
|
|
|
|
return &VaultInfo{
|
|
Path: vp,
|
|
DBPath: filepath.Join(vp, ".verstak", "index.db"),
|
|
FilesPath: filepath.Join(vp, "spaces"),
|
|
TrashPath: filepath.Join(vp, ".verstak", "trash"),
|
|
Healthy: true,
|
|
NodeCount: nodesCount,
|
|
FileCount: fileCount,
|
|
}, nil
|
|
}
|