git/intake/main
internal/sheet/store.go
84 lines2 KB last change raw · history
package sheet

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"

	"intake/internal/normalize"
)

// Store keeps an album's edited sheet between runs.
//
// An edited album used to live only in memory, so quitting the interface, or
// opening a different album and coming back, threw away every correction that
// had been typed. The sheet is already a complete description of what intake is
// about to write, so keeping it on disk is all that is needed to make a review
// resumable, and it is also what makes `intake album <name>` twice in a row do
// the same thing the second time.
//
// The file is named after the staging folder plus a short digest of the whole
// path, so a directory listing stays readable while two albums with the same
// folder name in different places cannot collide.
type Store struct {
	Dir string
}

func (s Store) Path(albumDir string) string {
	base := normalize.Name(filepath.Base(albumDir))
	if base == "" {
		base = "album"
	}
	sum := sha256.Sum256([]byte(filepath.Clean(albumDir)))
	return filepath.Join(s.Dir, fmt.Sprintf("%s-%s.txt", base, hex.EncodeToString(sum[:4])))
}

// Save writes the sheet as it stands. It returns the path it wrote, for saying
// where the edits went.
func (s Store) Save(albumDir string, text string) (string, error) {
	if s.Dir == "" {
		return "", nil
	}
	if err := os.MkdirAll(s.Dir, 0o755); err != nil {
		return "", err
	}
	path := s.Path(albumDir)
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, []byte(text), 0o644); err != nil {
		return "", err
	}
	if err := os.Rename(tmp, path); err != nil {
		os.Remove(tmp)
		return "", err
	}
	return path, nil
}

// Load returns the saved sheet text. The second result is false when there is
// none, which is the ordinary case rather than an error: most albums are looked
// at once and filed.
func (s Store) Load(albumDir string) (string, bool, error) {
	if s.Dir == "" {
		return "", false, nil
	}
	data, err := os.ReadFile(s.Path(albumDir))
	if err != nil {
		if os.IsNotExist(err) {
			return "", false, nil
		}
		return "", false, err
	}
	return string(data), true, nil
}

// Delete drops a saved sheet, once the album it describes has been filed or the
// person looking at it has said to forget it.
func (s Store) Delete(albumDir string) {
	if s.Dir == "" {
		return
	}
	os.Remove(s.Path(albumDir))
}