Former/storage.go

290 lines
7.3 KiB
Go

package main
import (
"encoding/json"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
func formerBaseDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, "former")
}
func formerTempDir() string {
return filepath.Join(formerBaseDir(), "temp")
}
func formerSessionsDir() string {
return filepath.Join(formerBaseDir(), "sessions")
}
func formerProfilesDir() string {
return filepath.Join(formerBaseDir(), "profiles")
}
func ensureFormerDirs() {
td := formerTempDir()
sd := formerSessionsDir()
pd := formerProfilesDir()
debugLog("ensureFormerDirs: temp=%s sessions=%s profiles=%s", td, sd, pd)
if err := os.MkdirAll(td, 0755); err != nil {
debugLog(" ERROR creating temp dir: %v", err)
}
if err := os.MkdirAll(sd, 0755); err != nil {
debugLog(" ERROR creating sessions dir: %v", err)
}
if err := os.MkdirAll(pd, 0755); err != nil {
debugLog(" ERROR creating profiles dir: %v", err)
}
}
// ProjectEntry represents a saved project on disk
type ProjectEntry struct {
Path string
Type string // "session" or "profile"
Data InstanceData
ModTime time.Time
}
// SaveSession persists an enclosure session to ~/former/sessions/
func SaveSession(inst InstanceData, sourceDir string, thumbnail image.Image) (string, error) {
ensureFormerDirs()
name := sanitizeDirName(inst.ProjectName)
if name == "" {
name = "untitled"
}
id := inst.ID
if len(id) > 8 {
id = id[:8]
}
projectDir := filepath.Join(formerSessionsDir(), fmt.Sprintf("%s-%s", name, id))
if err := saveProject(projectDir, inst, sourceDir); err != nil {
return "", err
}
if thumbnail != nil {
SaveThumbnail(projectDir, thumbnail)
}
return projectDir, nil
}
// SaveProfile persists an enclosure session as a named profile to ~/former/profiles/
func SaveProfile(inst InstanceData, name string, sourceDir string, thumbnail image.Image) (string, error) {
ensureFormerDirs()
dirLabel := sanitizeDirName(name)
if dirLabel == "" {
dirLabel = "untitled"
}
id := inst.ID
if len(id) > 8 {
id = id[:8]
}
projectDir := filepath.Join(formerProfilesDir(), fmt.Sprintf("%s-%s", dirLabel, id))
inst.Name = name
if err := saveProject(projectDir, inst, sourceDir); err != nil {
return "", err
}
if thumbnail != nil {
SaveThumbnail(projectDir, thumbnail)
}
return projectDir, nil
}
func saveProject(projectDir string, inst InstanceData, sourceDir string) error {
os.MkdirAll(projectDir, 0755)
// Copy gerber files using original filenames
newGerberFiles := make(map[string]string)
for origName, savedBasename := range inst.GerberFiles {
srcPath := filepath.Join(sourceDir, savedBasename)
dstPath := filepath.Join(projectDir, origName)
if err := CopyFile(srcPath, dstPath); err != nil {
// Fallback: try using origName directly
srcPath = filepath.Join(sourceDir, origName)
if err2 := CopyFile(srcPath, dstPath); err2 != nil {
return fmt.Errorf("copy %s: %v", origName, err)
}
}
newGerberFiles[origName] = origName
}
inst.GerberFiles = newGerberFiles
// Copy drill files
if inst.DrillPath != "" {
srcPath := filepath.Join(sourceDir, inst.DrillPath)
ext := filepath.Ext(inst.DrillPath)
if ext == "" {
ext = ".drl"
}
dstName := "drill" + ext
dstPath := filepath.Join(projectDir, dstName)
if CopyFile(srcPath, dstPath) == nil {
inst.DrillPath = dstName
}
}
if inst.NPTHPath != "" {
srcPath := filepath.Join(sourceDir, inst.NPTHPath)
ext := filepath.Ext(inst.NPTHPath)
if ext == "" {
ext = ".drl"
}
dstName := "npth" + ext
dstPath := filepath.Join(projectDir, dstName)
if CopyFile(srcPath, dstPath) == nil {
inst.NPTHPath = dstName
}
}
// Write former.json
data, err := json.MarshalIndent(inst, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(projectDir, "former.json"), data, 0644)
}
// ListProjects returns all saved projects sorted by modification time (newest first).
// Pass limit=0 for no limit.
func ListProjects(limit int) ([]ProjectEntry, error) {
ensureFormerDirs()
var entries []ProjectEntry
sessEntries, _ := listProjectsInDir(formerSessionsDir(), "session")
entries = append(entries, sessEntries...)
profEntries, _ := listProjectsInDir(formerProfilesDir(), "profile")
entries = append(entries, profEntries...)
sort.Slice(entries, func(i, j int) bool {
return entries[i].ModTime.After(entries[j].ModTime)
})
if limit > 0 && len(entries) > limit {
entries = entries[:limit]
}
return entries, nil
}
func listProjectsInDir(dir, projType string) ([]ProjectEntry, error) {
dirEntries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var entries []ProjectEntry
for _, de := range dirEntries {
if !de.IsDir() {
continue
}
jsonPath := filepath.Join(dir, de.Name(), "former.json")
info, err := os.Stat(jsonPath)
if err != nil {
continue
}
raw, err := os.ReadFile(jsonPath)
if err != nil {
continue
}
var inst InstanceData
if err := json.Unmarshal(raw, &inst); err != nil {
continue
}
entries = append(entries, ProjectEntry{
Path: filepath.Join(dir, de.Name()),
Type: projType,
Data: inst,
ModTime: info.ModTime(),
})
}
return entries, nil
}
// LoadProject reads former.json from a project directory
func LoadProject(projectDir string) (*InstanceData, error) {
raw, err := os.ReadFile(filepath.Join(projectDir, "former.json"))
if err != nil {
return nil, err
}
var inst InstanceData
if err := json.Unmarshal(raw, &inst); err != nil {
return nil, err
}
return &inst, nil
}
// UpdateProjectCutouts writes updated cutouts to an existing project's former.json
func UpdateProjectCutouts(projectDir string, cutouts []Cutout) error {
if projectDir == "" {
return nil
}
inst, err := LoadProject(projectDir)
if err != nil {
return err
}
inst.Cutouts = cutouts
// Clear legacy fields so they don't conflict
inst.SideCutouts = nil
inst.LidCutouts = nil
data, err := json.MarshalIndent(inst, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath.Join(projectDir, "former.json"), data, 0644)
}
// TouchProject updates the mtime of a project's former.json
func TouchProject(projectDir string) {
jsonPath := filepath.Join(projectDir, "former.json")
now := time.Now()
os.Chtimes(jsonPath, now, now)
}
// DeleteProject removes a project directory entirely
func DeleteProject(projectDir string) error {
return os.RemoveAll(projectDir)
}
// RestoreProject loads and rebuilds a session from a project directory
func RestoreProject(projectDir string) (string, *EnclosureSession, *InstanceData, error) {
inst, err := LoadProject(projectDir)
if err != nil {
return "", nil, nil, err
}
sid, session, err := restoreSessionFromDir(inst, projectDir)
if err != nil {
return "", nil, nil, err
}
TouchProject(projectDir)
return sid, session, inst, nil
}
// SaveThumbnail saves a preview image to the project directory
func SaveThumbnail(projectDir string, img image.Image) error {
f, err := os.Create(filepath.Join(projectDir, "thumbnail.png"))
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, img)
}
func sanitizeDirName(name string) string {
name = strings.Map(func(r rune) rune {
if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' {
return '-'
}
return r
}, name)
name = strings.TrimSpace(name)
if len(name) > 50 {
name = name[:50]
}
return name
}