diff options
Diffstat (limited to 'pkgtools.go')
-rw-r--r-- | pkgtools.go | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/pkgtools.go b/pkgtools.go new file mode 100644 index 0000000..350ab6a --- /dev/null +++ b/pkgtools.go @@ -0,0 +1,530 @@ +package pkgtools + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/bzip2" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/xi2/xz" +) + +const ( + // InstallLockDir is used to prevent install script collisions. + InstallLockDir = "/run/lock/pkgtools" + + // PackageInstallPath is the place for special package files. + PackageInstallPath = "install" + PackageInstallScript = "install/doinst.sh" + PackageUninstallScript = "install/douinst.sh" + PackageSlackDescFile = "install/slack-desc" + + InstalledScriptPath = "var/lib/pkgtools/scripts" + InstalledPackagePath = "var/lib/pkgtools/packages" + // ADMDir is the package database directories (packages, scripts). + ADMDir = "var/lib/pkgtools" + // Is the removed packages/scripts log files. + TmpDir = "var/lib/pkgtools/setup/tmp" + LogDir = "var/log/pkgtools" + InstalledRemovedPackagePath = "var/log/pkgtools/removed_packages" + InstalledRemovedScriptsPath = "var/log/pkgtools/removed_scripts" + InstalledRemovedUninstallScriptPath = "var/log/pkgtools/removed_uninstall_scripts" +) + +type PackageType string + +const ( + UnsupportedPackageFormat = "unsupported package format" + TGZ = "tgz" + TBZ = "tbz" + TLZ = "tlz" + TXZ = "txz" +) + +var SupportedPackageTypes = []string{ + TGZ, + TBZ, + TXZ, +} + +func IsSlackwarePkg(fp string) bool { + ext := filepath.Ext(filepath.Base(fp)) + for _, v := range SupportedPackageTypes { + if "."+v == ext { + return true + } + } + return false +} + +func FileLockPath(dir string, f string) string { + return filepath.Join(dir, f) + ".lock" +} + +func TargetPackagePath(root string, pkg string) string { + return filepath.Join(root, InstalledPackagePath, pkg) +} + +func TargetRemovedPackagePath(root string, pkg string) string { + return filepath.Join(root, InstalledRemovedPackagePath, pkg) +} + +func TargetRemovedUninstalllScriptPath(root string, pkg string) string { + return filepath.Join(root, InstalledRemovedUninstallScriptPath, pkg) +} + +func TargetTmpDir(root string) string { + return filepath.Join(root, ADMDir, "setup", "tmp") +} + +func TargetPackageInfoPath(root string, pkg string) string { + return filepath.Join(root, InstalledPackagePath, pkg) +} + +func TargetScriptPath(root string, pkg string) string { + return filepath.Join(root, InstalledScriptPath, pkg) +} + +func NewCompressedFileReader( + r io.Reader, + format PackageType, +) (io.Reader, error) { + switch format { + case TBZ: + dec := bzip2.NewReader(r) + return dec, nil + case TGZ: + dec, err := gzip.NewReader(r) + return dec, err + case TXZ: + dec, err := xz.NewReader(r, 0) + return dec, err + default: + return nil, errors.New(UnsupportedPackageFormat) + } +} + +func GetFullPackageName( + root string, + name string, +) (string, bool, error) { + pkgs, err := ListPackages(root) + if err != nil { + return "", false, err + } + for _, pkg := range pkgs { + v, err := PackageName(pkg) + if err != nil { + return "", false, err + } + if v == name { + return pkg, true, nil + } else if pkg == name { + return pkg, true, nil + } + } + return "", false, nil +} + +func GetPackageSoftLinks(root string, pkg string) ([]string, error) { + p := filepath.Join(root, InstalledScriptPath, pkg) + if ok, _ := IsFile(p); !ok { + return nil, nil + } + links, err := ReadPackageSoftLinks(p) + return links, err +} + +func ListPackages( + root string, +) ([]string, error) { + entries, err := os.ReadDir(filepath.Join(root, InstalledPackagePath)) + if err != nil { + return nil, err + } + var pkgs []string + for _, entry := range entries { + ok, err := IsFile(filepath.Join(root, InstalledPackagePath, entry.Name())) + if err != nil { + return nil, err + } + if ok { + pkgs = append(pkgs, entry.Name()) + } + } + + return pkgs, nil +} + +// Original from Slackware's 'removepkg' +// sed -n 's,^[ ]*( [ ]*cd[ ]* \(.*\) [ ]*; [ ]*rm [ ]*-rf[ ]* \(.*\) [ ]*)[ ]*$,\1/\2,p' </var/lib/pkgtools/scripts/acl-2.3.1-x86_64-1 +var regExpSoftLinks = regexp.MustCompile("(\\s*cd\\s*(.*)\\s*; rm -rf (.*) )") + +func ParsePackageSoftLinks( + str string, +) ([]string, error) { + var ret []string + matches := regExpSoftLinks.FindAllStringSubmatch(str, -1) + for _, m := range matches { + if len(m) != 4 { + continue + } + ret = append(ret, filepath.Join( + strings.TrimSpace(m[2]), + strings.TrimSpace(m[3]), + )) + } + return ret, nil +} + +func ReadPackageSoftLinks( + path string, +) ([]string, error) { + pkgScript := filepath.Join(path) + f, err := os.Open(pkgScript) + if err != nil { + return nil, err + } + defer f.Close() + + buf, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + links, err := ParsePackageSoftLinks(string(buf)) + if err != nil { + return nil, errors.Wrap(err, "getting package generated links") + } + return links, err +} + +type SlackwarePkg struct { + Format PackageType + file *os.File + + tarR *tar.Reader + r io.Reader + + uncompressedSize int64 + pkgName string + pkgInfo PackageInfo +} + +var _ ArchiveReader = (*SlackwarePkg)(nil) + +func OpenSlackwarePkg(fp string) (*SlackwarePkg, error) { + var pkg SlackwarePkg + var err error + pkg.Format = PackageType(filepath.Ext(fp)[1:]) + pkg.file, err = os.Open(fp) + if err != nil { + return nil, err + } + r, err := NewCompressedFileReader( + pkg.file, + pkg.Format, + ) + if err != nil { + return nil, err + } + pkg.tarR = tar.NewReader(r) + if err != nil { + return nil, err + } + pkg.r = pkg.tarR + + pkg.pkgName, err = PackageName(fp) + if err != nil { + return nil, err + } + + pkg.pkgInfo.Name = PackageBase(fp) + pkg.pkgInfo.PackageLocation = fp + + size, err := FileSize(fp) + if err != nil { + return nil, err + } + pkg.pkgInfo.CompressedPackageSize = HumanReadableSize(size) + + return &pkg, nil +} + +func (s *SlackwarePkg) PkgInfo() *PackageInfo { + ret := s.pkgInfo + ret.UncompressedPackageSize = HumanReadableSize(s.uncompressedSize) + return &ret +} + +func (s *SlackwarePkg) Close() error { + return s.file.Close() +} + +func (s *SlackwarePkg) Next() (*tar.Header, error) { + h, err := s.tarR.Next() + if err != nil { + return nil, err + } + s.r = s.tarR + + s.pkgInfo.FileList = append(s.pkgInfo.FileList, h.Name) + if h.Name == PackageSlackDescFile { + var buf bytes.Buffer + _, err := io.Copy(&buf, s.r) + if err != nil { + return nil, err + } + s.pkgInfo.PackageDescription = parseSlackDesc(buf.String(), s.pkgName) + s.r = bytes.NewReader(buf.Bytes()) + } + return h, nil +} + +func (s *SlackwarePkg) Read(b []byte) (int, error) { + n, err := s.r.Read(b) + s.uncompressedSize += int64(n) + return n, err +} + +// PackageBase is the package's filename without the (supported) extension. +func PackageBase(fp string) string { + filename := filepath.Base(fp) + ext := filepath.Ext(filename) + for _, v := range SupportedPackageTypes { + if "."+v == ext { + return filename[:(len(filename) - len(ext))] + } + } + return filename +} + +func PackageName(fp string) (string, error) { + base := PackageBase(fp) + strs := strings.Split(base, "-") + if len(strs) < 4 { + return "", errors.Errorf("badly formatted package name '%s'", fp) + } + name := strings.Join(strs[:(len(strs)-3)], "-") + return name, nil +} + +type PackageInfo struct { + Name string + // TODO: Should be size + CompressedPackageSize string + UncompressedPackageSize string + PackageLocation string + PackageDescription string + MD5Sum string + FileList []string +} + +func changeExt(fp string, ext string) string { + oldExt := filepath.Ext(fp) + ret := fp[:(len(fp) - len(oldExt))] + ret = ret + "." + ext + return ret +} + +func parseSlackDesc(str string, shortname string) string { + // TODO: What does this do? + //echo "PACKAGE DESCRIPTION:" >> $ADM_DIR/packages/$shortname + //grep "^$packagebase:" $DESCRIPTION >> $ADM_DIR/packages/$shortname 2> /dev/null + //if [ "$shortname" != "$packagebase" ]; then + // grep "^$shortname:" $DESCRIPTION >> $ADM_DIR/packages/$shortname 2> /dev/null + //fi + lines := strings.Split(str, "\n") + var kept []string + for _, l := range lines { + if strings.HasPrefix(l, shortname) { + kept = append(kept, l) + } + } + return strings.Join(kept, "\n") +} + +func GetUncompressedSize(fp string) (int64, error) { + f, err := os.Open(fp) + if err != nil { + return 0, err + } + defer f.Close() + + ext := filepath.Ext(fp)[1:] + r, err := NewCompressedFileReader(f, PackageType(ext)) + if err != nil { + return 0, err + } + + var size int64 + var buf [1024]byte + for { + n, err := r.Read(buf[:]) + if err == io.EOF { + break + } else if err != nil { + return 0, err + } + size += int64(n) + } + return size, nil +} + +// Use package name or package base for name +func GetPackageInfo(root string, pkgName string) (*PackageInfo, error) { + infoPath := TargetPackageInfoPath(root, pkgName) + info, err := ReadPackageInfo(infoPath) + return info, errors.Wrap(err, "getting package information") +} + +func ReadPackageInfo(fp string) (*PackageInfo, error) { + f, err := os.Open(fp) + if err != nil { + return nil, err + } + defer f.Close() + + buf, err := io.ReadAll(f) + if err != nil { + return nil, err + } + info, err := ParsePackageInfo(string(buf)) + if err != nil { + return nil, err + } + + return info, nil +} + +func ParsePackageInfo(text string) (*PackageInfo, error) { + keys := map[string]struct{}{ + "COMPRESSED PACKAGE SIZE": {}, + "FILE LIST": {}, + "MD5SUM": {}, + "PACKAGE DESCRIPTION": {}, + "PACKAGE LOCATION": {}, + "PACKAGE NAME": {}, + "UNCOMPRESSED PACKAGE SIZE": {}, + } + + pkg := PackageInfo{} + scanner := bufio.NewScanner(bytes.NewBufferString(text)) + for scanner.Scan() { + var k string + + l := scanner.Text() + strs := strings.Split(l, ":") + if len(strs) != 2 { + continue + } + k = strings.TrimSpace(strs[0]) + v := strings.TrimSpace(strs[1]) + _ = keys + + loop: + for { + switch k { + case "PACKAGE NAME": + pkg.Name = v + case "COMPRESSED PACKAGE SIZE": + pkg.CompressedPackageSize = v + case "UNCOMPRESSED PACKAGE SIZE": + pkg.UncompressedPackageSize = v + case "MD5SUM": + pkg.MD5Sum = v + case "PACKAGE LOCATION": + pkg.PackageLocation = v + case "PACKAGE DESCRIPTION": + for scanner.Scan() { + l := scanner.Text() + strs := strings.Split(l, ":") + if len(strs) == 2 { + k = strings.TrimSpace(strs[0]) + v = strings.TrimSpace(strs[1]) + if _, ok := keys[k]; ok { + break + } + } + if len(pkg.PackageDescription) > 0 { + pkg.PackageDescription += "\n" + } + pkg.PackageDescription += scanner.Text() + } + continue loop + case "FILE LIST": + for scanner.Scan() { + pkg.FileList = append(pkg.FileList, scanner.Text()) + } + default: + return nil, fmt.Errorf("unknown key '%s'", k) + } + break + } + } + return &pkg, nil +} + +type PackageFormatKey string + +type Encoder struct { + w io.Writer + MD5Sum bool +} + +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} +} + +func (s *Encoder) EnableMD5Sum(arg bool) { + s.MD5Sum = arg +} + +func (s *Encoder) Encode(pkg *PackageInfo) error { + _, err := s.w.Write([]byte(fmt.Sprintf("PACKAGE NAME: %s\n", pkg.Name))) + if err != nil { + return err + } + _, err = s.w.Write([]byte(fmt.Sprintf("COMPRESSED PACKAGE SIZE: %s\n", pkg.CompressedPackageSize))) + if err != nil { + return err + } + _, err = s.w.Write([]byte(fmt.Sprintf("UNCOMPRESSED PACKAGE SIZE: %s\n", pkg.UncompressedPackageSize))) + if err != nil { + return err + } + _, err = s.w.Write([]byte(fmt.Sprintf("PACKAGE LOCATION: %s\n", pkg.PackageLocation))) + if err != nil { + return err + } + if s.MD5Sum { + _, err = s.w.Write([]byte(fmt.Sprintf("PACKAGE MD5SUM: %s\n", pkg.MD5Sum))) + if err != nil { + return err + } + } + + _, err = s.w.Write([]byte(fmt.Sprintf("PACKAGE DESCRIPTION:\n%s\n", pkg.PackageDescription))) + if err != nil { + return err + } + _, err = s.w.Write([]byte("FILE LIST:\n")) + if err != nil { + return err + } + for _, f := range pkg.FileList { + _, err = s.w.Write([]byte(f + "\n")) + if err != nil { + return err + } + } + return nil +} |