193 lines
4.6 KiB
Go
193 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
const (
|
|
defArchiveFilename = "templates.boardarchive"
|
|
versionFilename = "version.json"
|
|
boardFilename = "board.jsonl"
|
|
minArchiveVersion = 2
|
|
maxArchiveVersion = 2
|
|
)
|
|
|
|
type archiveVersion struct {
|
|
Version int `json:"version"`
|
|
Date int64 `json:"date"`
|
|
}
|
|
|
|
type appConfig struct {
|
|
dir string
|
|
out string
|
|
verbose bool
|
|
}
|
|
|
|
func main() {
|
|
cfg := appConfig{}
|
|
|
|
flag.StringVar(&cfg.dir, "dir", "", "source directory of templates")
|
|
flag.StringVar(&cfg.out, "out", defArchiveFilename, "output filename")
|
|
flag.BoolVar(&cfg.verbose, "verbose", false, "enable verbose output")
|
|
flag.Parse()
|
|
|
|
if cfg.dir == "" {
|
|
flag.Usage()
|
|
os.Exit(-1)
|
|
}
|
|
|
|
var code int
|
|
if err := build(cfg); err != nil {
|
|
code = -1
|
|
fmt.Fprintf(os.Stderr, "error creating archive: %v\n", err)
|
|
} else if cfg.verbose {
|
|
fmt.Fprintf(os.Stdout, "archive created: %s\n", cfg.out)
|
|
}
|
|
|
|
os.Exit(code)
|
|
}
|
|
|
|
func build(cfg appConfig) (err error) {
|
|
version, err := getVersionFile(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create the output archive zip file
|
|
archiveFile, err := os.Create(cfg.out)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating %s: %w", cfg.out, err)
|
|
}
|
|
archiveZip := zip.NewWriter(archiveFile)
|
|
defer func() {
|
|
if err2 := archiveZip.Close(); err2 != nil {
|
|
if err == nil {
|
|
err = fmt.Errorf("error closing zip %s: %w", cfg.out, err2)
|
|
}
|
|
}
|
|
if err2 := archiveFile.Close(); err2 != nil {
|
|
if err == nil {
|
|
err = fmt.Errorf("error closing %s: %w", cfg.out, err2)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// write the version file
|
|
v, err := archiveZip.Create(versionFilename)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating %s: %w", cfg.out, err)
|
|
}
|
|
if _, err = v.Write(version); err != nil {
|
|
return fmt.Errorf("error writing %s: %w", cfg.out, err)
|
|
}
|
|
|
|
// each board is a subdirectory; write each to the archive
|
|
files, err := ioutil.ReadDir(cfg.dir)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading directory %s: %w", cfg.dir, err)
|
|
}
|
|
for _, f := range files {
|
|
if !f.IsDir() {
|
|
if f.Name() != versionFilename && cfg.verbose {
|
|
fmt.Fprintf(os.Stdout, "skipping non-directory %s\n", f.Name())
|
|
}
|
|
continue
|
|
}
|
|
if err = writeBoard(archiveZip, f.Name(), cfg); err != nil {
|
|
return fmt.Errorf("error writing board %s: %w", f.Name(), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getVersionFile(cfg appConfig) ([]byte, error) {
|
|
path := filepath.Join(cfg.dir, versionFilename)
|
|
buf, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read %s: %w", path, err)
|
|
}
|
|
|
|
var version archiveVersion
|
|
if err := json.Unmarshal(buf, &version); err != nil {
|
|
return nil, fmt.Errorf("cannot parse %s: %w", path, err)
|
|
}
|
|
|
|
if version.Version < minArchiveVersion || version.Version > maxArchiveVersion {
|
|
return nil, errUnsupportedVersion{Min: minArchiveVersion, Max: maxArchiveVersion, Got: version.Version}
|
|
}
|
|
|
|
return buf, nil
|
|
}
|
|
|
|
func writeBoard(w *zip.Writer, boardID string, cfg appConfig) error {
|
|
// copy the board's jsonl file first. BoardID is also the directory name.
|
|
srcPath := filepath.Join(cfg.dir, boardID, boardFilename)
|
|
destPath := filepath.Join(boardID, boardFilename)
|
|
if err := writeFile(w, srcPath, destPath, cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
boardPath := filepath.Join(cfg.dir, boardID)
|
|
files, err := ioutil.ReadDir(boardPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading board directory %s: %w", cfg.dir, err)
|
|
}
|
|
for _, f := range files {
|
|
if f.IsDir() {
|
|
if cfg.verbose {
|
|
fmt.Fprintf(os.Stdout, "skipping directory %s\n", f.Name())
|
|
}
|
|
continue
|
|
}
|
|
if f.Name() == boardFilename {
|
|
continue
|
|
}
|
|
|
|
srcPath = filepath.Join(cfg.dir, boardID, f.Name())
|
|
destPath = filepath.Join(boardID, f.Name())
|
|
if err = writeFile(w, srcPath, destPath, cfg); err != nil {
|
|
return fmt.Errorf("error writing %s: %w", destPath, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeFile(w *zip.Writer, srcPath string, destPath string, cfg appConfig) (err error) {
|
|
inFile, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading %s: %w", srcPath, err)
|
|
}
|
|
defer inFile.Close()
|
|
|
|
outFile, err := w.Create(destPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating %s: %w", destPath, err)
|
|
}
|
|
size, err := io.Copy(outFile, inFile)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing %s: %w", destPath, err)
|
|
}
|
|
|
|
if cfg.verbose {
|
|
fmt.Fprintf(os.Stdout, "%s written (%d bytes)\n", destPath, size)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type errUnsupportedVersion struct {
|
|
Min int
|
|
Max int
|
|
Got int
|
|
}
|
|
|
|
func (e errUnsupportedVersion) Error() string {
|
|
return fmt.Sprintf("unsupported archive version; require between %d and %d inclusive, got %d", e.Min, e.Max, e.Got)
|
|
}
|