| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- // Package export writes a project to a folder or a zip file: the .xpj and
- // its "<Name>_[ProjectData]" folder holding every pool file (build plan 4.4).
- package export
- import (
- "archive/zip"
- "bytes"
- "errors"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "strings"
- "time"
- "git.monoexp.com/bajdev/mpckit/internal/project"
- )
- // ExistsError is returned when the destination already holds the project
- // and overwriting was not allowed. The caller asks the user, then retries.
- type ExistsError struct {
- Paths []string
- }
- func (e *ExistsError) Error() string {
- return "already exists: " + strings.Join(e.Paths, ", ")
- }
- // MissingError lists pool files whose source can no longer be found.
- type MissingError struct {
- Files []string // pool file names
- Paths []string // where each was expected
- }
- func (e *MissingError) Error() string {
- var b strings.Builder
- fmt.Fprintf(&b, "%d sample file(s) are missing:", len(e.Files))
- for i := range e.Files {
- fmt.Fprintf(&b, "\n %s (%s)", e.Files[i], e.Paths[i])
- }
- return b.String()
- }
- type item struct {
- name string // file name inside the data folder
- src string
- }
- // collect checks that every pool file has a source that exists.
- func collect(p *project.Project) ([]item, error) {
- var items []item
- missing := &MissingError{}
- for _, f := range p.PoolFiles() {
- src := p.Sources[f]
- if src == "" {
- missing.Files = append(missing.Files, f)
- missing.Paths = append(missing.Paths, "no source recorded")
- continue
- }
- if st, err := os.Stat(src); err != nil || !st.Mode().IsRegular() {
- missing.Files = append(missing.Files, f)
- missing.Paths = append(missing.Paths, src)
- continue
- }
- items = append(items, item{name: f, src: src})
- }
- if len(missing.Files) > 0 {
- return nil, missing
- }
- return items, nil
- }
- func encode(p *project.Project) ([]byte, error) {
- var b bytes.Buffer
- if err := p.File().Write(&b); err != nil {
- return nil, err
- }
- return b.Bytes(), nil
- }
- func dataDir(p *project.Project) string { return p.Name + project.DataSuffix }
- // Folder writes <dest>/<Name>.xpj and <dest>/<Name>_[ProjectData]/ and
- // returns the path of the .xpj. Everything is written under a temporary
- // name in dest first and renamed at the end, so a failure leaves any
- // existing project as it was. If either target exists and overwrite is
- // false, nothing is written and an *ExistsError is returned.
- func Folder(p *project.Project, dest string, overwrite bool) (string, error) {
- if err := project.ValidName(p.Name); err != nil {
- return "", fmt.Errorf("project name: %w", err)
- }
- items, err := collect(p)
- if err != nil {
- return "", err
- }
- xpjPath := filepath.Join(dest, p.Name+".xpj")
- dirPath := filepath.Join(dest, dataDir(p))
- if !overwrite {
- if exists := existing(xpjPath, dirPath); len(exists) > 0 {
- return "", &ExistsError{Paths: exists}
- }
- }
- body, err := encode(p)
- if err != nil {
- return "", err
- }
- tmp, err := os.MkdirTemp(dest, ".mpckit-export-")
- if err != nil {
- return "", err
- }
- defer os.RemoveAll(tmp)
- tmpData := filepath.Join(tmp, dataDir(p))
- if err := os.Mkdir(tmpData, 0o755); err != nil {
- return "", err
- }
- for _, it := range items {
- if err := copyFile(it.src, filepath.Join(tmpData, it.name)); err != nil {
- return "", err
- }
- }
- tmpXPJ := filepath.Join(tmp, p.Name+".xpj")
- if err := os.WriteFile(tmpXPJ, body, 0o644); err != nil {
- return "", err
- }
- // Replace: the old pair goes only once the new pair is complete.
- if overwrite {
- if err := os.RemoveAll(dirPath); err != nil {
- return "", err
- }
- if err := os.Remove(xpjPath); err != nil && !errors.Is(err, os.ErrNotExist) {
- return "", err
- }
- }
- if err := os.Rename(tmpData, dirPath); err != nil {
- return "", err
- }
- if err := os.Rename(tmpXPJ, xpjPath); err != nil {
- return "", err
- }
- return xpjPath, nil
- }
- func existing(paths ...string) []string {
- var out []string
- for _, p := range paths {
- if _, err := os.Lstat(p); err == nil {
- out = append(out, p)
- }
- }
- return out
- }
- func copyFile(src, dst string) error {
- in, err := os.Open(src)
- if err != nil {
- return err
- }
- defer in.Close()
- out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
- if err != nil {
- return err
- }
- if _, err := io.Copy(out, in); err != nil {
- out.Close()
- return err
- }
- return out.Close()
- }
- // Zip writes a zip file at path holding <Name>.xpj and
- // <Name>_[ProjectData]/ at its root, ready to extract onto the device's
- // card. It writes to a temporary file and renames it at the end. If path
- // exists and overwrite is false, an *ExistsError is returned.
- func Zip(p *project.Project, path string, overwrite bool) error {
- if err := project.ValidName(p.Name); err != nil {
- return fmt.Errorf("project name: %w", err)
- }
- items, err := collect(p)
- if err != nil {
- return err
- }
- if !overwrite {
- if exists := existing(path); len(exists) > 0 {
- return &ExistsError{Paths: exists}
- }
- }
- body, err := encode(p)
- if err != nil {
- return err
- }
- tmp, err := os.CreateTemp(filepath.Dir(path), ".mpckit-export-*.zip")
- if err != nil {
- return err
- }
- defer os.Remove(tmp.Name())
- if err := writeZip(tmp, p, body, items); err != nil {
- tmp.Close()
- return err
- }
- if err := tmp.Close(); err != nil {
- return err
- }
- if err := os.Chmod(tmp.Name(), 0o644); err != nil {
- return err
- }
- return os.Rename(tmp.Name(), path)
- }
- func writeZip(w io.Writer, p *project.Project, body []byte, items []item) error {
- now := time.Now()
- zw := zip.NewWriter(w)
- // The .xpj is already gzip-compressed: store it.
- fw, err := zw.CreateHeader(&zip.FileHeader{Name: p.Name + ".xpj", Method: zip.Store, Modified: now})
- if err != nil {
- return err
- }
- if _, err := fw.Write(body); err != nil {
- return err
- }
- dir := dataDir(p) + "/"
- if _, err := zw.CreateHeader(&zip.FileHeader{Name: dir, Modified: now}); err != nil {
- return err
- }
- for _, it := range items {
- fw, err := zw.CreateHeader(&zip.FileHeader{Name: dir + it.name, Method: zip.Deflate, Modified: now})
- if err != nil {
- return err
- }
- in, err := os.Open(it.src)
- if err != nil {
- return err
- }
- _, err = io.Copy(fw, in)
- in.Close()
- if err != nil {
- return err
- }
- }
- return zw.Close()
- }
|