pkg/vproj/projects.go
package vproj
/**
* SPDX-License-Identifier: Apache-2.0
* Copyright 2020 vorteil.io Pty Ltd
*/
import (
"archive/tar"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/djherbis/buffer"
"github.com/djherbis/nio"
"github.com/vorteil/vorteil/pkg/vcfg"
"github.com/vorteil/vorteil/pkg/vio"
"github.com/vorteil/vorteil/pkg/vpkg"
"github.com/sisatech/toml"
)
const (
// FileName ..
FileName = ".vorteilproject"
UnpackTempPattern = "vorteil-unpack-"
)
// TargetData ..
type TargetData struct {
Name string `toml:"name" json:"name"`
VCFGs []string `toml:"vcfgs,omitempty" json:"vcfgs"`
Icon string `toml:"icon,omitempty" json:"icon"`
Files []string `toml:"files,omitempty" json:"files"`
}
// ProjectData ..
type ProjectData struct {
IgnorePatterns []string `toml:"ignore" json:"ignore"`
Targets []TargetData `toml:"target,omitempty" json:"target"`
}
// Project ..
type Project struct {
Dir string
Project ProjectData
}
// Marshal ..
func (p *ProjectData) Marshal() ([]byte, error) {
buf := new(bytes.Buffer)
err := toml.NewEncoder(buf).Encode(*p)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// LoadProject ..
func LoadProject(path string) (*Project, error) {
p := new(Project)
err := p.loadProjectPath(path)
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(filepath.Join(path, FileName))
if err != nil {
if os.IsNotExist(err) {
err = errors.New("no project in directory")
}
return nil, err
}
err = toml.Unmarshal(data, &p.Project)
if err != nil {
return nil, err
}
return p, nil
}
func (p *Project) loadProjectPath(path string) error {
err := p.checkBasePath(path)
if err != nil {
return err
}
p.Dir = path
err = p.checkProjectFile()
if err != nil {
return err
}
return nil
}
func (p *Project) checkBasePath(path string) error {
fi, err := os.Stat(path)
if err != nil {
return err
}
if !fi.IsDir() {
return fmt.Errorf("'%s' is not a directory", path)
}
return nil
}
func (p *Project) checkProjectFile() error {
path := filepath.Join(p.Dir, FileName)
fi, err := os.Stat(path)
if err != nil {
return err
}
if fi.IsDir() {
return fmt.Errorf("'%s' is a directory", path)
}
if fi.Size() > 64*1024 {
return fmt.Errorf("'%s' is too large", path)
}
return nil
}
// Target ..
func (p *Project) Target(name string) (*Target, error) {
targets := p.Project.Targets
l := len(targets)
t := new(Target)
t.Dir = p.Dir
t.Ignore = p.Project.IgnorePatterns
var found bool
for i := 0; i < l; i++ {
if (i == 0 && name == "") || targets[i].Name == name {
t.Name = targets[i].Name
t.Icon = targets[i].Icon
t.Files = targets[i].Files
t.VCFGs = targets[i].VCFGs
found = true
break
}
}
if !found {
return nil, fmt.Errorf("project target '%s' not found", name)
}
return t, nil
}
// Target ..
type Target struct {
Name string
Dir string
Ignore []string
Icon string
VCFGs []string
Files []string
}
// VCFG ..
func (t *Target) VCFG() (*vcfg.VCFG, error) {
cfg := new(vcfg.VCFG)
for i, path := range t.VCFGs {
if !filepath.IsAbs(path) {
path = filepath.Join(t.Dir, path)
}
err := cfg.LoadFilepath(path)
if err != nil {
if os.IsNotExist(err) {
err = fmt.Errorf("vcfg '%s' not found", t.VCFGs[i])
}
return nil, err
}
}
return cfg, nil
}
func vcfgInfo(cfg *vcfg.VCFG) (vio.File, error) {
newVcfg := new(vcfg.VCFG)
newVcfg.Info = cfg.Info
data, err := newVcfg.Marshal()
if err != nil {
return nil, err
}
return vio.CustomFile(vio.CustomFileArgs{
Size: len(data),
Name: "readme.vcfg",
ReadCloser: ioutil.NopCloser(bytes.NewReader(data)),
}), nil
}
// FileWithoutInfo
func vcfgSansInfo(cfg *vcfg.VCFG, name string) (vio.File, error) {
if name == "" {
name = "default.vcfg"
}
newVcfg := new(vcfg.VCFG)
newVcfg.Merge(cfg)
newVcfg.Info = vcfg.PackageInfo{}
data, err := newVcfg.Marshal()
if err != nil {
return nil, err
}
return vio.CustomFile(vio.CustomFileArgs{
Size: len(data),
Name: name,
ReadCloser: ioutil.NopCloser(bytes.NewReader(data)),
}), nil
}
func genericProjectData() ProjectData {
return ProjectData{
IgnorePatterns: []string{FileName},
Targets: []TargetData{
{
Name: "default",
VCFGs: []string{"default.vcfg"},
Icon: "default.png",
Files: []string{},
},
},
}
}
// TarFromPackage writes a tar to w from the package reader
func TarFromPackage(w io.Writer, pkg vpkg.Reader) error {
// Create tar writer
tw := tar.NewWriter(w)
defer tw.Close()
// Load VCFG from package
v, cfg, err := vcfgFromPkg(pkg)
if err != nil {
return err
}
defer cfg.Close()
// Load icon from package
iconName := "default.png"
ico, iconFile, err := iconFromPkg(pkg)
if err != nil {
return err
}
defer ico.Close()
defer os.Remove(iconFile.Name())
// Load generic project data object
var vprjIncluded bool
vprj := genericProjectData()
// Walk pkg filesystem
files := make([]string, 0)
err = walkFilesystem(pkg, files, tw, vprjIncluded, vprj)
if err != nil {
return err
}
if !vprjIncluded {
// Write default icon/vcfg to tar
err = defaultPNGAndVCFG(&defaultIconAndConfigArgs{v: v, vprj: vprj, tw: tw, ico: ico, iconFile: iconFile, iconName: iconName})
if err != nil {
return err
}
} else {
// Write from the vorteil project definition
err = tarFromVorteilProject(v, vprj, files, tw)
if err != nil {
return err
}
}
return nil
}
// CreateFromPackage tars the reader and proceeds to unpack on path
func CreateFromPackage(path string, pkg vpkg.Reader) error {
pr, pw := nio.Pipe(buffer.New(0x100000))
go func() {
err := TarFromPackage(pw, pkg)
if err != nil {
_ = pw.CloseWithError(err)
}
_ = pw.Close()
}()
defer pr.Close()
tr := tar.NewReader(pr)
for {
err := readFromTarReader(tr, path)
if err != nil {
if err == io.EOF {
break
}
return err
}
}
return nil
}
func readFromTarReader(tr *tar.Reader, path string) error {
hdr, err := tr.Next()
if err != nil {
return err
}
if hdr.FileInfo().Mode()&os.ModeSymlink > 0 {
err = handleSymlinkFromTarReader(hdr, path)
if err != nil {
return err
}
} else {
err = handleFileFromTarReader(tr, hdr, path)
if err != nil {
return err
}
}
return nil
}
func handleSymlinkFromTarReader(hdr *tar.Header, path string) error {
dpath := filepath.Join(path, hdr.Name)
err := os.MkdirAll(filepath.Dir(dpath), 0777)
if err != nil {
return err
}
err = os.Symlink(hdr.Linkname, dpath)
if err != nil {
return err
}
return err
}
func handleDirFromTarReader(hdr *tar.Header, path string) error {
err := os.MkdirAll(filepath.Join(path, hdr.Name), os.FileMode(hdr.Mode))
if err != nil {
return err
}
return nil
}
func handleFileFromTarReader(tr *tar.Reader, hdr *tar.Header, path string) error {
p := filepath.Join(path, filepath.Dir(hdr.Name))
if hdr.FileInfo().IsDir() {
p = filepath.Join(path, hdr.Name)
}
err := os.MkdirAll(p, os.FileMode(hdr.Mode))
if err != nil {
return err
}
if !hdr.FileInfo().IsDir() {
var f *os.File
f, err = os.Create(filepath.Join(path, hdr.Name))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
if err != nil {
return err
}
err = f.Close()
}
return err
}
// Split takes src and returns the path and target of provided value
func Split(src string) (path string, target string) {
src = filepath.ToSlash(src)
parts := strings.Split(src, ":")
if len(parts) == 1 {
return src, ""
}
target = parts[len(parts)-1]
path = strings.Join(parts[:len(parts)-1], ":")
_, err := LoadProject(path)
if err != nil {
if os.IsNotExist(err) {
return src, ""
}
}
return
}