290 lines
7.3 KiB
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
|
|
}
|