551 lines
19 KiB
Go
551 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"eon/common"
|
|
"eon/lib"
|
|
"fmt"
|
|
"golang.org/x/term"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
var logger = &common.DefaultLogger
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Usage: eon <list/info/install/remove/clean/repo/help> [args]")
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "help":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]")
|
|
os.Exit(1)
|
|
} else {
|
|
switch os.Args[2] {
|
|
case "list":
|
|
fmt.Println("Usage: eon list (remote/local)")
|
|
fmt.Println("- remote: lists packages available in the repositories.")
|
|
fmt.Println("- local: lists installed packages.")
|
|
case "info":
|
|
fmt.Println("Usage: eon info <package>")
|
|
fmt.Println("Shows information about a package.")
|
|
case "install":
|
|
fmt.Println("Usage: eon install <package> [<repository>]")
|
|
fmt.Println("Installs a package.")
|
|
case "remove":
|
|
fmt.Println("Usage: eon remove <package>")
|
|
fmt.Println("Removes a package.")
|
|
case "clean":
|
|
fmt.Println("Usage: eon clean")
|
|
fmt.Println("Removes unused dependencies.")
|
|
case "repo":
|
|
fmt.Println("Usage: eon repo (add/remove/list)")
|
|
fmt.Println("- add <url>: adds a repository.")
|
|
fmt.Println("- remove <url>: removes a repository.")
|
|
fmt.Println("- list: lists repositories.")
|
|
case "help":
|
|
fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]")
|
|
fmt.Println("Shows help.")
|
|
default:
|
|
fmt.Println(color.RedString("Unknown command: " + os.Args[2]))
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
case "install":
|
|
var forceMode bool
|
|
var inMemoryMode bool
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("Usage: eon install <package>")
|
|
os.Exit(1)
|
|
}
|
|
var localPackageList []string
|
|
fileInfos := make(map[string]os.FileInfo)
|
|
var repoPackageList []string
|
|
for _, pkg := range os.Args[2:] {
|
|
if !strings.HasPrefix(pkg, "-") {
|
|
fileInfo, err := os.Stat(pkg)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// This unholy regex is a package name validator. It validates repository-name/package-name/1.0.0-prerelease.1+meta.1
|
|
// The regex is so unholy it breaks JetBrains' regex parser, so I had to disable it.
|
|
//goland:noinspection RegExpUnnecessaryNonCapturingGroup
|
|
match, err := regexp.Match(`^(?:([a-z0-9_-]+)/)?([a-z0-9_-]+)(?:/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)?$`, []byte(pkg))
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to validate package name: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
if !match {
|
|
fmt.Println(color.RedString("Invalid package name: " + pkg))
|
|
os.Exit(1)
|
|
} else {
|
|
repoPackageList = append(repoPackageList, pkg)
|
|
}
|
|
}
|
|
} else {
|
|
localPackageList = append(localPackageList, pkg)
|
|
fileInfos[pkg] = fileInfo
|
|
}
|
|
} else {
|
|
switch pkg {
|
|
case "--force", "-f":
|
|
forceMode = true
|
|
case "--optimizeForSpeed", "-O":
|
|
inMemoryMode = true
|
|
}
|
|
}
|
|
}
|
|
// We contact the database before we print anything as to not break the formatting.
|
|
err := common.EstablishDBConnection(logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
// We refresh the package list. This takes a lot of arguments, haha.
|
|
err = common.RefreshPackageList(common.DefaultListRepositoriesInDB, common.DefaultAddRemotePackageToDB,
|
|
common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, common.DefaultCheckRepositoryInDB,
|
|
common.DefaultAddRepositoryToDB, common.DefaultListRemotePackagesInDB, common.DefaultRemoveRemotePackageFromDB,
|
|
logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to refresh package list: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
// Let's start printing things.
|
|
if len(localPackageList) > 0 || len(repoPackageList) > 0 {
|
|
var epkList []common.InstallPackage
|
|
var skipEpkList [][]string
|
|
var dependencies int
|
|
for _, pkg := range localPackageList {
|
|
epkFile, err := os.Open(pkg)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to open package: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
|
|
var displayData lib.EPKPreMap
|
|
var vagueErr error
|
|
var epkBytes bytes.Buffer
|
|
if !inMemoryMode {
|
|
displayData, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{FileStream: epkFile, IsFileStream: true}, fileInfos[pkg].Size())
|
|
} else {
|
|
_, err = io.Copy(bufio.NewWriter(&epkBytes), epkFile)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to read package into memory: " + err.Error() +
|
|
", have you tried disabling in-memory mode?"))
|
|
os.Exit(1)
|
|
}
|
|
err := epkFile.Close()
|
|
if err != nil {
|
|
fmt.Println(color.HiYellowString("Failed to close package file: " + err.Error() + ", memory leak possible."))
|
|
}
|
|
displayData, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{Bytes: epkBytes.Bytes()}, fileInfos[pkg].Size())
|
|
}
|
|
if err != nil || vagueErr != nil {
|
|
common.PreMapEPKHandleError(err, vagueErr, logger)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var installPackage common.InstallPackage
|
|
installPackage.EPKPreMap = &displayData
|
|
installPackage.Url = ""
|
|
installPackage.Priority = 0
|
|
installPackage.IsRemote = false
|
|
installPackage.IsForced = false
|
|
if !inMemoryMode {
|
|
installPackage.StreamOrBytes = lib.StreamOrBytes{
|
|
FileStream: epkFile,
|
|
IsFileStream: true,
|
|
}
|
|
} else {
|
|
installPackage.StreamOrBytes = lib.StreamOrBytes{
|
|
Bytes: epkBytes.Bytes(),
|
|
}
|
|
}
|
|
|
|
installPackage.Repository = lib.Repository{Name: "Local file"}
|
|
displayData.IsUpgrade = true
|
|
|
|
version, exists, err := common.DefaultCheckEPKInDB(displayData.Name)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to check for package in database: " + err.Error()))
|
|
}
|
|
|
|
if exists {
|
|
if version.Compare(&displayData.Version) != -1 {
|
|
if !forceMode {
|
|
skipEpk := []string{displayData.Name, displayData.Architecture, displayData.Version.String(), "Local file", humanize.BigIBytes(displayData.DecompressedSize), "Skip"}
|
|
skipEpkList = append(skipEpkList, skipEpk)
|
|
} else {
|
|
installPackage.IsForced = true
|
|
addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
dependencies += addedDeps
|
|
}
|
|
epkList = append(epkList, installPackage)
|
|
}
|
|
} else {
|
|
addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
dependencies += addedDeps
|
|
}
|
|
epkList = append(epkList, installPackage)
|
|
}
|
|
} else {
|
|
displayData.IsUpgrade = false
|
|
addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
dependencies += addedDeps
|
|
}
|
|
epkList = append(epkList, installPackage)
|
|
}
|
|
}
|
|
|
|
for _, pkg := range repoPackageList {
|
|
// Check if the package is already installed.
|
|
version, exists, err := common.DefaultCheckEPKInDB(pkg)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to check for package in database: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
|
|
var remoteEPK lib.RemoteEPK
|
|
var epkEntry common.InstallPackage
|
|
|
|
remoteEpkList, err := common.DefaultListRemotePackagesInDB()
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to list remote packages: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
|
|
for _, epk := range remoteEpkList {
|
|
if epk.Name == pkg {
|
|
remoteEPK = epk
|
|
break
|
|
}
|
|
}
|
|
|
|
// Calculate the download URL
|
|
epkDownloadUrl, err := url.JoinPath(remoteEPK.Repository.URL, remoteEPK.Path)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to join URL: " + err.Error()))
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
// Pre-map the EPK
|
|
displayData, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.PreMapEPKHandleError(err, vagueErr, logger)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Map the data we have
|
|
epkEntry.Priority = 0
|
|
epkEntry.EPKPreMap = &displayData
|
|
epkEntry.Url = epkDownloadUrl
|
|
epkEntry.IsRemote = true
|
|
epkEntry.Repository = remoteEPK.Repository
|
|
if !inMemoryMode {
|
|
epkEntry.StreamOrBytes.IsURL = true
|
|
epkEntry.StreamOrBytes.URL = epkDownloadUrl
|
|
epkEntry.StreamOrBytes.RepositoryName = remoteEPK.Repository.Name
|
|
} else {
|
|
// Download the entire EPK into memory
|
|
epkBytes, err := http.Get(epkDownloadUrl)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to download package: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
|
|
if epkBytes.StatusCode != 200 {
|
|
fmt.Println(color.RedString("Failed to download package: " + epkBytes.Status))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Calculate the total size of the package
|
|
contentLength := new(big.Int)
|
|
contentLength.SetString(epkBytes.Header.Get("Content-Length"), 10)
|
|
|
|
// Print that we are downloading the package
|
|
fmt.Println("\nDownloading package: " + displayData.Name + " (" + humanize.BigIBytes(contentLength) + ")")
|
|
|
|
// Hide the cursor for reasons explained below.
|
|
fmt.Print("\033[?25l")
|
|
|
|
// Copy the stream into a buffer
|
|
var buffer bytes.Buffer
|
|
_, err = io.Copy(&lib.ProgressWriter{
|
|
Logger: logger,
|
|
Total: contentLength,
|
|
Writer: &buffer,
|
|
}, epkBytes.Body)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to read package into memory: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Set the progress to 100%
|
|
logger.LogFunc(lib.Log{
|
|
Level: "PROGRESS",
|
|
Progress: big.NewInt(1),
|
|
Total: big.NewInt(1),
|
|
})
|
|
|
|
// Show the cursor again because we're done with the progress bar.
|
|
fmt.Print("\033[?25h")
|
|
|
|
// Close the response body
|
|
err = epkBytes.Body.Close()
|
|
if err != nil {
|
|
fmt.Println(color.HiYellowString("Failed to close response body: " + err.Error() + ", memory leak possible."))
|
|
}
|
|
|
|
// Set the buffer as the bytes
|
|
epkEntry.StreamOrBytes.Bytes = buffer.Bytes()
|
|
epkEntry.StreamOrBytes.IsURL = false
|
|
epkEntry.StreamOrBytes.IsFileStream = false
|
|
|
|
// Reset the buffer
|
|
buffer.Reset()
|
|
}
|
|
|
|
// Make decisions on what happens if the package is already installed.
|
|
if exists {
|
|
if version.Compare(&displayData.Version) != -1 {
|
|
// If the version is the same or newer, skip the package.
|
|
if !forceMode {
|
|
skipEpkList = append(skipEpkList, []string{displayData.Name, displayData.Architecture, displayData.Version.String(), remoteEPK.Repository.Name, humanize.BigIBytes(displayData.DecompressedSize), "Skip"})
|
|
} else {
|
|
// If the version is the same or newer, but the user wants to force the installation, install it.
|
|
epkEntry.IsForced = true
|
|
// We can let it use our remoteEPKList to save a SQL query.
|
|
addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
// We add the dependencies to the total count.
|
|
dependencies += addedDeps
|
|
}
|
|
epkList = append(epkList, epkEntry)
|
|
}
|
|
} else {
|
|
// If the version is older, install it.
|
|
addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
dependencies += addedDeps
|
|
}
|
|
epkList = append(epkList, epkEntry)
|
|
}
|
|
} else {
|
|
// If the package is not installed, install it.
|
|
addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
dependencies += addedDeps
|
|
}
|
|
epkList = append(epkList, epkEntry)
|
|
}
|
|
}
|
|
|
|
// Give the summary of the installation.
|
|
fmt.Println("\nThe following packages will be installed:")
|
|
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to get terminal width: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
if width < 42 {
|
|
fmt.Println(color.RedString("Terminal too small. Minimum required width: 42 characters."))
|
|
os.Exit(1)
|
|
}
|
|
for range width {
|
|
fmt.Print("=")
|
|
}
|
|
fmt.Println()
|
|
tableList := []string{"Package", "Architecture", "Version", "Repository", "Installed Size", "Action"}
|
|
maxSize := width / 6
|
|
for _, item := range tableList {
|
|
common.PrintWithEvenPadding(item, maxSize)
|
|
}
|
|
fmt.Println()
|
|
for range width {
|
|
fmt.Print("=")
|
|
}
|
|
fmt.Println()
|
|
|
|
for _, pkg := range skipEpkList {
|
|
for _, item := range pkg {
|
|
common.PrintWithEvenPadding(color.GreenString(item), maxSize)
|
|
}
|
|
}
|
|
|
|
for _, pkg := range epkList {
|
|
finalisedList := make([]string, 6)
|
|
finalisedList[0] = pkg.EPKPreMap.Name
|
|
finalisedList[1] = pkg.EPKPreMap.Architecture
|
|
finalisedList[2] = pkg.EPKPreMap.Version.String()
|
|
if !pkg.IsRemote {
|
|
finalisedList[3] = "Local file"
|
|
} else {
|
|
finalisedList[3] = pkg.Repository.Name
|
|
}
|
|
finalisedList[4] = humanize.BigIBytes(pkg.EPKPreMap.DecompressedSize)
|
|
if pkg.IsForced {
|
|
finalisedList[5] = "Forced installation"
|
|
} else if pkg.EPKPreMap.IsUpgrade {
|
|
finalisedList[5] = "Upgrade"
|
|
} else {
|
|
finalisedList[5] = "Install"
|
|
}
|
|
for _, item := range finalisedList {
|
|
common.PrintWithEvenPadding(item, maxSize)
|
|
}
|
|
}
|
|
|
|
if len(epkList) > 0 {
|
|
for range width {
|
|
fmt.Print("=")
|
|
}
|
|
fmt.Println("Transaction Summary")
|
|
fmt.Println("\nInstalling " + humanize.Comma(int64(len(epkList))) + " packages, of which " + humanize.Comma(int64(dependencies)) + " are dependencies.")
|
|
fmt.Println("Total download size: " + humanize.BigIBytes(common.GetTotalSize(epkList)))
|
|
fmt.Println("Total installed size: " + humanize.BigIBytes(common.GetTotalInstalledSize(epkList)) + "\n")
|
|
response := logger.LogFunc(lib.Log{
|
|
Level: "INFO",
|
|
Content: "Proceed with installation (y/n)?",
|
|
Prompt: true,
|
|
PlaySound: true,
|
|
})
|
|
if strings.ToLower(response) == "y" {
|
|
// We hide the cursor because it makes the progress bar look weird and other package managers hide
|
|
// it during installation. For some reason, it builds suspense. Or it does with me anyway, when
|
|
// a program hides the cursor, it makes me think twice than to Ctrl+C it :P
|
|
// - Arzumify
|
|
fmt.Print("\033[?25l")
|
|
// Time to install things.
|
|
for _, installPackage := range epkList {
|
|
// Map the EPK metadata.
|
|
metadata, err, vagueErr := lib.FullyMapMetadata(installPackage.StreamOrBytes,
|
|
installPackage.EPKPreMap, common.DefaultGetFingerprintFromDB,
|
|
common.DefaultAddFingerprintToDB, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.FullyMapMetadataHandleError(err, vagueErr, logger)
|
|
}
|
|
// Install the package.
|
|
tempDir, err, vagueErr := lib.InstallEPK(installPackage.StreamOrBytes,
|
|
metadata, installPackage.EPKPreMap, common.DefaultAddEPKToDB, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.InstallEPKHandleError(tempDir, err, vagueErr, logger)
|
|
}
|
|
// Done!
|
|
fmt.Println("Installed package: " + installPackage.EPKPreMap.Name)
|
|
}
|
|
// We show the cursor again because we're done with the progress bar.
|
|
fmt.Print("\033[?25h")
|
|
} else {
|
|
fmt.Println("Installation cancelled.")
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
for range width {
|
|
fmt.Print("=")
|
|
}
|
|
fmt.Println("No packages left to install. To force re-install / downgrade, use the --force flag, " +
|
|
"though this may result in breakage.")
|
|
os.Exit(0)
|
|
}
|
|
} else {
|
|
fmt.Println("No packages to install.")
|
|
os.Exit(0)
|
|
}
|
|
case "repo":
|
|
if len(os.Args) < 3 {
|
|
fmt.Println("Usage: eon repo (add/remove/list)")
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch os.Args[2] {
|
|
case "add":
|
|
if len(os.Args) < 4 {
|
|
fmt.Println("Usage: eon repo add <url>")
|
|
os.Exit(1)
|
|
} else {
|
|
err := common.EstablishDBConnection(logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
repoName, err, vagueErr := lib.AddRepository(os.Args[3], common.DefaultAddRepositoryToDB, common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, common.DefaultAddRemotePackageToDB, common.DefaultCheckRepositoryInDB, false, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.AddRepositoryHandleError(err, vagueErr, logger)
|
|
} else {
|
|
fmt.Println("Added repository " + repoName + " to the database.")
|
|
}
|
|
}
|
|
case "remove":
|
|
if len(os.Args) < 4 {
|
|
fmt.Println("Usage: eon repo remove <name>")
|
|
os.Exit(1)
|
|
} else {
|
|
err := common.EstablishDBConnection(logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
err, vagueErr := lib.RemoveRepository(os.Args[3], common.DefaultRemoveRepositoryFromDB, common.DefaultCheckRepositoryInDB, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.RemoveRepositoryHandleError(err, vagueErr, logger)
|
|
}
|
|
}
|
|
case "list":
|
|
err := common.EstablishDBConnection(logger)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
repos, err := common.DefaultListRepositoriesInDB()
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to get repositories: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
if len(repos) == 0 {
|
|
fmt.Println("No repositories.")
|
|
} else {
|
|
fmt.Println("Repositories:")
|
|
for _, repo := range repos {
|
|
fmt.Println("\n" + repo.Name + ":\n" + " " + repo.Description + "\n URL: " + repo.URL + "\n Owner: " + repo.Owner)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
default:
|
|
fmt.Println(color.RedString("Unknown or unimplemented command: " + os.Args[1]))
|
|
os.Exit(1)
|
|
}
|
|
}
|