eon/cmd/main.go
2024-10-10 10:24:56 +01:00

1220 lines
40 KiB
Go

package main
import (
"bufio"
"bytes"
"eon/common"
"eon/lib"
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
"net/http"
"net/url"
"golang.org/x/sys/unix"
"golang.org/x/term"
"github.com/dustin/go-humanize"
"github.com/fatih/color"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: eon <list/info/install/remove/clean/repo/help> [args]")
os.Exit(1)
}
logger := &common.DefaultLogger
for i, arg := range os.Args {
if arg == "--help" || arg == "-h" || (i != 1 && arg == "help") {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "To ask for help, use 'eon help <list/info/install/remove/clean/repo/help>', not --help, -h, or eon <command> help.",
})
}
}
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 (repo/local)")
fmt.Println("- repo: lists packages available in repositories.")
fmt.Println("- local: lists installed packages.")
case "info":
fmt.Println("Usage: eon info <package> [(repo/local)] [<repository>]")
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/del/list)")
fmt.Println("- add <packageUrl>: adds a repository.")
fmt.Println("- del <name>: 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 yesMode 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 || fileInfo.IsDir() {
if os.IsNotExist(err) || fileInfo.IsDir() {
// 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 {
fmt.Println(color.RedString("Failed to get file information: " + err.Error()))
os.Exit(1)
}
} else {
localPackageList = append(localPackageList, pkg)
fileInfos[pkg] = fileInfo
}
} else {
switch pkg {
case "--force", "-f":
forceMode = true
case "--optimizeForSpeed", "-O":
inMemoryMode = true
case "--yes", "-y":
yesMode = 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
localPackageListIteration:
for _, pkg := range localPackageList {
// Check if the package is already in epkList.
for _, epk := range epkList {
if epk.EPKPreMap.DisplayData.Name == pkg {
continue localPackageListIteration
}
}
epkFile, err := os.Open(pkg)
if err != nil {
fmt.Println(color.RedString("Failed to open package: " + err.Error()))
os.Exit(1)
}
var preMap lib.EPKPreMap
var vagueErr error
var epkBytes bytes.Buffer
if !inMemoryMode {
preMap, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{FileStream: epkFile, IsFileStream: true}, uint64(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."))
}
preMap, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{Bytes: epkBytes.Bytes()}, uint64(fileInfos[pkg].Size()))
}
if err != nil || vagueErr != nil {
common.PreMapEPKHandleError(err, vagueErr, logger)
os.Exit(1)
}
// Check if the architecture is supported.
var uts unix.Utsname
err = unix.Uname(&uts)
if err != nil {
fmt.Println(color.RedString("Failed to get system architecture: " + err.Error()))
os.Exit(1)
}
// Check if the architecture is supported.
if preMap.DisplayData.Architecture != strings.TrimRight(string(uts.Machine[:]), "\x00") {
if preMap.DisplayData.Architecture != "noarch" {
fmt.Println(color.RedString("Package architecture not supported: " + preMap.DisplayData.Architecture))
os.Exit(1)
}
}
preMap.DisplayData.IsDependency = false
var installPackage common.InstallPackage
installPackage.EPKPreMap = &preMap
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"}
preMap.IsUpgrade = true
version, exists, err := common.DefaultCheckEPKInDB(preMap.DisplayData.Name)
if err != nil {
fmt.Println(color.RedString("Failed to check for package in database: " + err.Error()))
}
if exists {
if version.Compare(&preMap.DisplayData.Version) != -1 {
if !forceMode {
skipEpk := []string{preMap.DisplayData.Name, preMap.DisplayData.Architecture, preMap.DisplayData.Version.String(), "Local file", humanize.Bytes(preMap.DisplayData.DecompressedSize), "Skip"}
skipEpkList = append(skipEpkList, skipEpk)
} else {
installPackage.IsForced = true
var emptyList []string
var addedDeps int
epkList, addedDeps, err = common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, common.DefaultListEPKsInDB, epkList, &emptyList, 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 {
var emptyList []string
var addedDeps int
epkList, addedDeps, err = common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, common.DefaultListEPKsInDB, epkList, &emptyList, 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 {
preMap.IsUpgrade = false
var emptyList []string
var addedDeps int
epkList, addedDeps, err = common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, common.DefaultListEPKsInDB, epkList, &emptyList, logger)
if err != nil {
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
os.Exit(1)
} else {
dependencies += addedDeps
}
epkList = append(epkList, installPackage)
}
}
repoPackageListIteration:
for _, pkg := range repoPackageList {
// Check if the package is already in epkList.
for _, epk := range epkList {
if epk.EPKPreMap.DisplayData.Name == pkg {
continue repoPackageListIteration
}
}
// 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)
}
epkExists := false
for _, epk := range remoteEpkList {
if epk.Name == pkg {
remoteEPK = epk
epkExists = true
break
}
}
if !epkExists {
fmt.Println(color.RedString("Package not found: " + pkg))
os.Exit(1)
}
// 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
preMap, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger)
if err != nil || vagueErr != nil {
common.PreMapRemoteEPKHandleError(err, vagueErr, logger)
os.Exit(1)
}
preMap.DisplayData.IsDependency = false
// Map the data we have
epkEntry.Priority = 0
epkEntry.EPKPreMap = &preMap
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, err := strconv.ParseUint(epkBytes.Header.Get("Content-Length"), 10, 64)
if err != nil {
fmt.Println(color.RedString("Failed to get content length: " + err.Error()))
os.Exit(1)
}
// Print that we are downloading the package
fmt.Println("\nDownloading package: " + preMap.DisplayData.Name + " (" + humanize.Bytes(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: 1,
Total: 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(&preMap.DisplayData.Version) != -1 {
// If the version is the same or newer, skip the package.
if !forceMode {
skipEpkList = append(skipEpkList, []string{preMap.DisplayData.Name, preMap.DisplayData.Architecture, preMap.DisplayData.Version.String(), remoteEPK.Repository.Name, humanize.Bytes(preMap.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.
var emptyList []string
var addedDeps int
epkList, addedDeps, err = common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, common.DefaultListEPKsInDB, epkList, &emptyList, 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.
var emptyList []string
var addedDeps int
epkList, addedDeps, err = common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, common.DefaultListEPKsInDB, epkList, &emptyList, 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.
var emptyList []string
var addedDeps int
epkList, addedDeps, err = common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, common.DefaultListEPKsInDB, epkList, &emptyList, logger)
if err != nil {
fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error()))
os.Exit(1)
} else {
dependencies += addedDeps
}
epkList = append(epkList, epkEntry)
}
}
// Sort the list of packages to install by priority, then alphabetically.
sort.Slice(epkList, func(i, j int) bool {
if epkList[i].Priority == epkList[j].Priority {
return epkList[i].EPKPreMap.DisplayData.Name < epkList[j].EPKPreMap.DisplayData.Name
}
return epkList[i].Priority > epkList[j].Priority
})
// 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.DisplayData.Name
finalisedList[1] = pkg.EPKPreMap.DisplayData.Architecture
finalisedList[2] = pkg.EPKPreMap.DisplayData.Version.String()
if !pkg.IsRemote {
finalisedList[3] = "Local file"
} else {
finalisedList[3] = pkg.Repository.Name
}
finalisedList[4] = humanize.Bytes(pkg.EPKPreMap.DisplayData.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.Bytes(common.GetTotalSize(epkList)))
fmt.Println("Total installed size: " + humanize.Bytes(common.GetTotalInstalledSize(epkList)) + "\n")
var response string
if !yesMode {
response = logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Proceed with installation (y/n)?",
Prompt: true,
PlaySound: true,
})
}
if strings.ToLower(response) == "y" || yesMode {
// 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 {
if installPackage.IsRemote {
if !inMemoryMode {
// Set the package stream to the URL.
installPackage.StreamOrBytes.IsURL = true
installPackage.StreamOrBytes.IsRemote = true
installPackage.StreamOrBytes.RepositoryName = installPackage.Repository.Name
installPackage.StreamOrBytes.IsFileStream = false
installPackage.StreamOrBytes.URL = installPackage.Url
} else {
// Download the entire EPK into memory
epkBytes, err := http.Get(installPackage.Url)
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)
}
// Start streaming the package into a buffer
contentLength, err := strconv.ParseUint(epkBytes.Header.Get("Content-Length"), 10, 64)
if err != nil {
fmt.Println(color.RedString("Failed to get content length: " + err.Error()))
os.Exit(1)
}
fmt.Println("\nDownloading package: " + installPackage.EPKPreMap.DisplayData.Name + " (" + humanize.Bytes(contentLength) + ")")
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: 1,
Total: 1,
})
// 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."))
}
fmt.Println("Package downloaded")
// Set the buffer as the bytes
installPackage.StreamOrBytes.Bytes = buffer.Bytes()
installPackage.StreamOrBytes.IsURL = false
installPackage.StreamOrBytes.IsRemote = true
installPackage.StreamOrBytes.RepositoryName = installPackage.Repository.Name
installPackage.StreamOrBytes.IsFileStream = false
}
}
// Map the EPK metadata.
metadata, err, vagueErr := lib.FullyMapMetadata(installPackage.StreamOrBytes,
installPackage.EPKPreMap, common.DefaultGetFingerprintFromDB,
common.DefaultAddFingerprintToDB, func(*lib.Logger) {
logger.LogFunc(lib.Log{
Level: "WARN",
Content: "This server does not support range requests. Please use the -O flag to " +
"enable in memory mode, or contact the server administrator to use a web server " +
"that supports range requests, such as Apache or Nginx. The speed of download " +
"is considerably slowed, as we need to discard bytes since we can only read " +
"sequentially.",
Prompt: false,
})
}, 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.DisplayData.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/del/list)")
os.Exit(1)
}
switch os.Args[2] {
case "add":
if len(os.Args) < 4 {
fmt.Println("Usage: eon repo add <packageUrl>")
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 "del":
if len(os.Args) < 4 {
fmt.Println("Usage: eon repo del <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()
}
}
case "remove":
if len(os.Args) < 3 {
fmt.Println("Usage: eon remove <package>")
os.Exit(1)
}
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
// Create a map of all installed packages, to minimise the database queries later.
epkMap, decompressedSizeMap, repositoryMap, _, err := common.DefaultListEPKsInDB()
if err != nil {
fmt.Println(color.RedString("Failed to list installed packages: " + err.Error()))
os.Exit(1)
}
// Create a list of packages to remove.
var packageRemoveList []common.RemovePackage
for _, pkg := range os.Args[2:] {
var removePackage common.RemovePackage
_, exists, err := common.DefaultCheckEPKInDB(pkg)
if err != nil {
fmt.Println(color.RedString("Failed to check for package in database: " + err.Error()))
os.Exit(1)
}
if !exists {
fmt.Println(color.RedString("Package not found: " + pkg))
os.Exit(1)
}
// Search to see if it's parent is still installed.
for _, epk := range epkMap {
for _, dep := range epk.Dependencies {
if dep == os.Args[2] {
// First, check if it's in the list of packages to remove.
var found bool
for _, epkName := range os.Args[2:] {
if epkName == epk.Name {
found = true
break
}
}
if !found {
fmt.Println(color.RedString("Package " + pkg + " is a dependency of another package (" + epk.Name + ") and cannot be removed."))
os.Exit(1)
} else {
// It is in the list of packages to remove, we need to set this package's priority higher.
removePackage.Priority++
}
}
}
}
// Add it to the list of packages to remove.
removePackage.Name = pkg
removePackage.DisplayData = epkMap[pkg]
packageRemoveList = append(packageRemoveList, removePackage)
}
if len(packageRemoveList) > 0 {
// First, re-order the packageRemoveList by priority, then alphabetically.
sort.Slice(packageRemoveList, func(i, j int) bool {
if packageRemoveList[i].Priority == packageRemoveList[j].Priority {
return packageRemoveList[i].Name < packageRemoveList[j].Name
}
return packageRemoveList[i].Priority > packageRemoveList[j].Priority
})
// Print the packages that will be removed.
fmt.Println("The following packages will be removed:")
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 packageRemoveList {
finalisedList := make([]string, 6)
finalisedList[0] = pkg.Name
finalisedList[1] = pkg.DisplayData.Architecture
finalisedList[2] = pkg.DisplayData.Version.String()
finalisedList[3] = repositoryMap[pkg.Name].Name
finalisedList[4] = humanize.Bytes(decompressedSizeMap[pkg.Name])
finalisedList[5] = "Remove"
for _, item := range finalisedList {
common.PrintWithEvenPadding(item, maxSize)
}
}
for range width {
fmt.Print("=")
}
fmt.Println("Transaction Summary")
fmt.Println("\nRemoving " + humanize.Comma(int64(len(packageRemoveList))) + " packages.")
var space uint64
for _, pkg := range packageRemoveList {
space += decompressedSizeMap[pkg.Name]
}
fmt.Println("Total reclaimed space: " + humanize.Bytes(space) + "\n")
response := logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Proceed with removal (y/n)?",
Prompt: true,
})
if strings.ToLower(response) == "y" {
for _, pkg := range packageRemoveList {
err, vagueErr := lib.RemoveEPK(pkg.Name, common.DefaultRemoveEPKFromDB, common.DefaultGetEPKRemoveInfoFromDB, logger)
if err != nil || vagueErr != nil {
common.RemoveEPKHandleError(err, vagueErr, logger)
fmt.Println(color.RedString("Failed to remove package: " + vagueErr.Error() + err.Error()))
}
fmt.Println("Removed package: " + pkg.Name)
}
} else {
fmt.Println("Removal cancelled.")
os.Exit(1)
}
}
case "list":
if len(os.Args) < 3 {
fmt.Println("Usage: eon list (remote/local)")
os.Exit(1)
}
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
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 < 15 {
fmt.Println(color.RedString("Terminal too small. Minimum required width: 15 characters."))
os.Exit(1)
}
maxWidth := width / 3
switch os.Args[2] {
case "remote":
// Refresh the package list.
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)
}
remoteEpkList, err := common.DefaultListRemotePackagesInDB()
if err != nil {
fmt.Println(color.RedString("Failed to list remote packages: " + err.Error()))
os.Exit(1)
}
if len(remoteEpkList) == 0 {
fmt.Println("No remote packages.")
} else {
fmt.Println("\nRemote packages:")
for range width {
fmt.Print("=")
}
for _, item := range []string{"Name", "Version", "Repository"} {
common.PrintWithEvenPadding(item, maxWidth)
}
for range width {
fmt.Print("=")
}
for _, epk := range remoteEpkList {
var printMap []string
printMap = append(printMap, epk.Name, epk.Version.String(), epk.Repository.Name)
for _, item := range printMap {
common.PrintWithEvenPadding(item, maxWidth)
}
fmt.Println()
}
for range width {
fmt.Print("=")
}
}
case "local":
epkMap, _, repositoryMap, _, err := common.DefaultListEPKsInDB()
if err != nil {
fmt.Println(color.RedString("Failed to list installed packages: " + err.Error()))
os.Exit(1)
}
if len(epkMap) == 0 {
fmt.Println("No local packages.")
} else {
// Sort the map by name.
var epkList []string
for epk := range epkMap {
epkList = append(epkList, epk)
}
sort.Strings(epkList)
fmt.Println("Local packages:")
for range width {
fmt.Print("=")
}
for _, item := range []string{"Name", "Version", "Repository"} {
common.PrintWithEvenPadding(item, maxWidth)
}
for range width {
fmt.Print("=")
}
for _, epk := range epkList {
version := epkMap[epk].Version
var printMap []string
printMap = append(printMap, epk, version.String(), repositoryMap[epk].Name)
for _, item := range printMap {
common.PrintWithEvenPadding(item, maxWidth)
}
fmt.Println()
}
for range width {
fmt.Print("=")
}
}
}
case "info":
if len(os.Args) < 3 {
fmt.Println("Usage: eon info <package>")
os.Exit(1)
}
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 = 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()))
}
remoteEpks, err := common.DefaultListRemotePackagesInDB()
if err != nil {
fmt.Println(color.RedString("Failed to list installed packages: " + err.Error()))
os.Exit(1)
}
var remoteEpk lib.RemoteEPK
for _, epk := range remoteEpks {
if epk.Name == os.Args[2] {
remoteEpk = epk
break
}
}
if remoteEpk.Name == "" {
fmt.Println(color.RedString("Package not found: " + os.Args[2]))
os.Exit(1)
}
epkPreMap, err, vagueErr := lib.PreMapRemoteEPK(remoteEpk, logger)
if err != nil || vagueErr != nil {
common.PreMapRemoteEPKHandleError(err, vagueErr, logger)
}
packageUrl, err := url.JoinPath(remoteEpk.Repository.URL, remoteEpk.Path)
if err != nil {
fmt.Println(color.RedString("Failed to join URL: " + err.Error()))
os.Exit(1)
}
metadata, err, vagueErr := lib.FullyMapMetadata(lib.StreamOrBytes{
IsURL: true,
URL: packageUrl,
}, &epkPreMap, common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, func(*lib.Logger) {
logger.LogFunc(lib.Log{
Level: "WARN",
Content: "This server does not support range requests. Please use the -O flag to " +
"enable in memory mode, or contact the server administrator to use a web server " +
"that supports range requests, such as Apache or Nginx. The speed of download " +
"is considerably slowed, as we need to discard bytes since we can only read " +
"sequentially.",
Prompt: false,
})
}, logger)
if err != nil || vagueErr != nil {
common.FullyMapMetadataHandleError(err, vagueErr, logger)
}
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 < 11 {
fmt.Println(color.RedString("Terminal too small. Minimum required width: 11 characters."))
os.Exit(1)
}
fmt.Println()
for range width {
fmt.Print("=")
}
common.PrintWithEvenPadding("Information", width/2)
common.PrintWithEvenPadding("Content", width/2)
for range width {
fmt.Print("=")
}
common.PrintWithEvenPadding("Name", width/2)
common.PrintWithEvenPadding(epkPreMap.DisplayData.Name, width/2)
common.PrintWithEvenPadding("Version", width/2)
common.PrintWithEvenPadding(metadata.Version.String(), width/2)
common.PrintWithEvenPadding("Description", width/2)
common.PrintWithEvenPadding(metadata.Description, width/2)
common.PrintWithEvenPadding("License", width/2)
common.PrintWithEvenPadding(metadata.License, width/2)
common.PrintWithEvenPadding("Architecture", width/2)
common.PrintWithEvenPadding(metadata.Architecture, width/2)
common.PrintWithEvenPadding("Installed Size", width/2)
common.PrintWithEvenPadding(humanize.Bytes(metadata.DecompressedSize), width/2)
common.PrintWithEvenPadding("Download Size", width/2)
common.PrintWithEvenPadding(humanize.Bytes(metadata.Size), width/2)
common.PrintWithEvenPadding("Repository", width/2)
common.PrintWithEvenPadding(remoteEpk.Repository.Name, width/2)
common.PrintWithEvenPadding("Dependencies", width/2)
if len(metadata.Dependencies) == 0 {
common.PrintWithEvenPadding("None", width/2)
} else {
common.PrintWithEvenPadding(strings.Join(metadata.Dependencies, ", "), width/2)
}
// We don't print the long description - it's meant for GUIs.
for range width {
fmt.Print("=")
}
case "clean":
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
packages, _, repositoryMap, _, err := common.DefaultListEPKsInDB()
if err != nil {
fmt.Println(color.RedString("Failed to list installed packages: " + err.Error()))
os.Exit(1)
}
if len(packages) == 0 {
fmt.Println("No packages to clean.")
os.Exit(0)
} else {
var removeList []common.RemovePackage
for _, pkg := range packages {
// Check if it's a dependency of another package.
if pkg.IsDependency {
// Add it to list of candidates for removal.
var removePackage common.RemovePackage
removePackage.Name = pkg.Name
removePackage.DisplayData = pkg
removeList = append(removeList, removePackage)
}
}
for _, pkg := range removeList {
// Check if it's parent is still installed.
for _, epk := range packages {
for _, dep := range epk.Dependencies {
if dep == pkg.Name {
// First, check if it's in the list of packages to remove.
var found bool
for _, epkName := range removeList {
if epkName.Name == epk.Name {
found = true
break
}
}
if !found {
fmt.Println(color.RedString("Package " + pkg.Name + " is a dependency of another package (" + epk.Name + ") and cannot be removed."))
os.Exit(1)
} else {
// It is in the list of packages to remove, we need to set this package's priority higher.
pkg.Priority++
}
}
}
}
}
if len(removeList) > 0 {
// First, re-order the packageRemoveList by priority, then alphabetically.
sort.Slice(removeList, func(i, j int) bool {
if removeList[i].Priority == removeList[j].Priority {
return removeList[i].Name < removeList[j].Name
}
return removeList[i].Priority > removeList[j].Priority
})
// Print the packages that will be removed.
fmt.Println("The following packages will be removed:")
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("=")
}
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("=")
}
for _, pkg := range removeList {
finalisedList := make([]string, 6)
finalisedList[0] = pkg.Name
finalisedList[1] = pkg.DisplayData.Architecture
finalisedList[2] = pkg.DisplayData.Version.String()
finalisedList[3] = repositoryMap[pkg.Name].Name
finalisedList[4] = humanize.Bytes(pkg.DisplayData.DecompressedSize)
finalisedList[5] = "Remove"
for _, item := range finalisedList {
common.PrintWithEvenPadding(item, maxSize)
}
fmt.Println()
}
for range width {
fmt.Print("=")
}
fmt.Println("Transaction Summary")
fmt.Println("\nRemoving " + humanize.Comma(int64(len(removeList))) + " packages.")
var space uint64
for _, pkg := range removeList {
space += pkg.DisplayData.DecompressedSize
}
fmt.Println("Total reclaimed space: " + humanize.Bytes(space) + "\n")
response := logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Proceed with removal (y/n)?",
Prompt: true,
})
if strings.ToLower(response) == "y" {
for _, pkg := range removeList {
err, vagueErr := lib.RemoveEPK(pkg.Name, common.DefaultRemoveEPKFromDB, common.DefaultGetEPKRemoveInfoFromDB, logger)
if err != nil || vagueErr != nil {
common.RemoveEPKHandleError(err, vagueErr, logger)
fmt.Println(color.RedString("Failed to remove package: " + vagueErr.Error() + err.Error()))
}
fmt.Println("Removed package: " + pkg.Name)
}
} else {
fmt.Println("Removal cancelled.")
os.Exit(1)
}
} else {
fmt.Println("No packages to clean.")
os.Exit(0)
}
}
default:
fmt.Println(color.RedString("Unknown or unimplemented command: " + os.Args[1]))
os.Exit(1)
}
}