diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 6e3dedd..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${fileDirname}", - "args": [], - } - ] -} \ No newline at end of file diff --git a/cmd/cmd b/cmd/cmd index c83ede4..855cc7a 100755 Binary files a/cmd/cmd and b/cmd/cmd differ diff --git a/cmd/main.go b/cmd/main.go index 953e8f3..5d4ba5b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,27 +6,39 @@ import ( "eon/common" "eon/lib" "fmt" - "golang.org/x/term" "io" + "os" + "regexp" + "sort" + "strings" + "math/big" "net/http" "net/url" - "os" - "regexp" - "strings" + + "golang.org/x/sys/unix" + "golang.org/x/term" "github.com/dustin/go-humanize" "github.com/fatih/color" ) -var logger = &common.DefaultLogger - func main() { if len(os.Args) < 2 { fmt.Println("Usage: eon [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 ', not --help, -h, or eon help.", + }) + } + } switch os.Args[1] { case "help": if len(os.Args) < 3 { @@ -35,11 +47,11 @@ func main() { } 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("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 ") + fmt.Println("Usage: eon info [(repo/local)] []") fmt.Println("Shows information about a package.") case "install": fmt.Println("Usage: eon install []") @@ -51,9 +63,9 @@ func main() { fmt.Println("Usage: eon clean") fmt.Println("Removes unused dependencies.") case "repo": - fmt.Println("Usage: eon repo (add/remove/list)") + fmt.Println("Usage: eon repo (add/del/list)") fmt.Println("- add : adds a repository.") - fmt.Println("- remove : removes a repository.") + fmt.Println("- del : removes a repository.") fmt.Println("- list: lists repositories.") case "help": fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]") @@ -65,6 +77,7 @@ func main() { } case "install": var forceMode bool + var yesMode bool var inMemoryMode bool if len(os.Args) < 3 { fmt.Println("Usage: eon install ") @@ -103,6 +116,8 @@ func main() { forceMode = true case "--optimizeForSpeed", "-O": inMemoryMode = true + case "--yes", "-y": + yesMode = true } } } @@ -126,18 +141,27 @@ func main() { 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 displayData lib.EPKPreMap + var preMap 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()) + preMap, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{FileStream: epkFile, IsFileStream: true}, fileInfos[pkg].Size()) } else { _, err = io.Copy(bufio.NewWriter(&epkBytes), epkFile) if err != nil { @@ -149,15 +173,29 @@ func main() { 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()) + preMap, 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) } + // 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 != "noarch" || preMap.DisplayData.Architecture != string(uts.Machine[:]) { + fmt.Println(color.RedString("Package architecture not supported: " + preMap.DisplayData.Architecture)) + os.Exit(1) + } + var installPackage common.InstallPackage - installPackage.EPKPreMap = &displayData + installPackage.EPKPreMap = &preMap installPackage.Url = "" installPackage.Priority = 0 installPackage.IsRemote = false @@ -174,21 +212,23 @@ func main() { } installPackage.Repository = lib.Repository{Name: "Local file"} - displayData.IsUpgrade = true + preMap.IsUpgrade = true - version, exists, err := common.DefaultCheckEPKInDB(displayData.Name) + 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(&displayData.Version) != -1 { + if version.Compare(&preMap.DisplayData.Version) != -1 { if !forceMode { - skipEpk := []string{displayData.Name, displayData.Architecture, displayData.Version.String(), "Local file", humanize.BigIBytes(displayData.DecompressedSize), "Skip"} + skipEpk := []string{preMap.DisplayData.Name, preMap.DisplayData.Architecture, preMap.DisplayData.Version.String(), "Local file", humanize.BigIBytes(preMap.DisplayData.DecompressedSize), "Skip"} skipEpkList = append(skipEpkList, skipEpk) } else { installPackage.IsForced = true - addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger) + var emptyList []string + var addedDeps int + epkList, addedDeps, err = common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, epkList, &emptyList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) @@ -198,7 +238,9 @@ func main() { epkList = append(epkList, installPackage) } } else { - addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger) + var emptyList []string + var addedDeps int + epkList, addedDeps, err = common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, epkList, &emptyList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) @@ -208,8 +250,10 @@ func main() { epkList = append(epkList, installPackage) } } else { - displayData.IsUpgrade = false - addedDeps, err := common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, &epkList, logger) + preMap.IsUpgrade = false + var emptyList []string + var addedDeps int + epkList, addedDeps, err = common.HandleDependencies(dependencies, installPackage, 0, []lib.RemoteEPK{}, common.DefaultListRemotePackagesInDB, epkList, &emptyList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) @@ -220,7 +264,15 @@ func main() { } } + 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 { @@ -237,13 +289,20 @@ func main() { 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 { @@ -253,15 +312,15 @@ func main() { } // Pre-map the EPK - displayData, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger) + preMap, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger) if err != nil || vagueErr != nil { - common.PreMapEPKHandleError(err, vagueErr, logger) + common.PreMapRemoteEPKHandleError(err, vagueErr, logger) os.Exit(1) } // Map the data we have epkEntry.Priority = 0 - epkEntry.EPKPreMap = &displayData + epkEntry.EPKPreMap = &preMap epkEntry.Url = epkDownloadUrl epkEntry.IsRemote = true epkEntry.Repository = remoteEPK.Repository @@ -287,7 +346,7 @@ func main() { contentLength.SetString(epkBytes.Header.Get("Content-Length"), 10) // Print that we are downloading the package - fmt.Println("\nDownloading package: " + displayData.Name + " (" + humanize.BigIBytes(contentLength) + ")") + fmt.Println("\nDownloading package: " + preMap.DisplayData.Name + " (" + humanize.BigIBytes(contentLength) + ")") // Hide the cursor for reasons explained below. fmt.Print("\033[?25l") @@ -331,15 +390,17 @@ func main() { // Make decisions on what happens if the package is already installed. if exists { - if version.Compare(&displayData.Version) != -1 { + if version.Compare(&preMap.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"}) + skipEpkList = append(skipEpkList, []string{preMap.DisplayData.Name, preMap.DisplayData.Architecture, preMap.DisplayData.Version.String(), remoteEPK.Repository.Name, humanize.BigIBytes(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. - addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger) + var emptyList []string + var addedDeps int + epkList, addedDeps, err = common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, epkList, &emptyList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) @@ -351,7 +412,9 @@ func main() { } } else { // If the version is older, install it. - addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger) + var emptyList []string + var addedDeps int + epkList, addedDeps, err = common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, epkList, &emptyList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) @@ -362,7 +425,9 @@ func main() { } } else { // If the package is not installed, install it. - addedDeps, err := common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, &epkList, logger) + var emptyList []string + var addedDeps int + epkList, addedDeps, err = common.HandleDependencies(dependencies, epkEntry, 0, remoteEpkList, common.DefaultListRemotePackagesInDB, epkList, &emptyList, logger) if err != nil { fmt.Println(color.RedString("Failed to handle dependencies: " + err.Error())) os.Exit(1) @@ -373,6 +438,14 @@ func main() { } } + // 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())) @@ -407,15 +480,15 @@ func main() { for _, pkg := range epkList { finalisedList := make([]string, 6) - finalisedList[0] = pkg.EPKPreMap.Name - finalisedList[1] = pkg.EPKPreMap.Architecture - finalisedList[2] = pkg.EPKPreMap.Version.String() + 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.BigIBytes(pkg.EPKPreMap.DecompressedSize) + finalisedList[4] = humanize.BigIBytes(pkg.EPKPreMap.DisplayData.DecompressedSize) if pkg.IsForced { finalisedList[5] = "Forced installation" } else if pkg.EPKPreMap.IsUpgrade { @@ -436,13 +509,16 @@ func main() { 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" { + 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 @@ -450,10 +526,78 @@ func main() { 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 := new(big.Int).SetBytes([]byte(epkBytes.Header.Get("Content-Length"))) + fmt.Println("\nDownloading package: " + installPackage.EPKPreMap.DisplayData.Name + " (" + humanize.BigIBytes(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: big.NewInt(1), + Total: big.NewInt(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, logger) + 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) } @@ -464,7 +608,7 @@ func main() { common.InstallEPKHandleError(tempDir, err, vagueErr, logger) } // Done! - fmt.Println("Installed package: " + installPackage.EPKPreMap.Name) + 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") @@ -486,7 +630,7 @@ func main() { } case "repo": if len(os.Args) < 3 { - fmt.Println("Usage: eon repo (add/remove/list)") + fmt.Println("Usage: eon repo (add/del/list)") os.Exit(1) } @@ -508,9 +652,9 @@ func main() { fmt.Println("Added repository " + repoName + " to the database.") } } - case "remove": + case "del": if len(os.Args) < 4 { - fmt.Println("Usage: eon repo remove ") + fmt.Println("Usage: eon repo del ") os.Exit(1) } else { err := common.EstablishDBConnection(logger) @@ -539,7 +683,174 @@ func main() { } 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("\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 ") + 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.BigIBytes(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.") + space := new(big.Int) + for _, pkg := range packageRemoveList { + space = new(big.Int).Add(space, decompressedSizeMap[pkg.Name]) + } + fmt.Println("Total reclaimed space: " + humanize.BigIBytes(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) + } + + switch os.Args[2] { + case "remote": + 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("Remote packages:") + for _, epk := range remoteEpkList { + fmt.Println("\n" + epk.Name + ":\n" + + "- " + epk.Description + + "\n- Author: " + epk.Author + + "\n- Size to download: " + humanize.Bytes(uint64(epk.CompressedSize)) + + "\n- Version: " + epk.Version.String() + + "\n- Architecture: " + epk.Architecture + + "\n- Repository: " + epk.Repository.Name) } fmt.Println() } diff --git a/common/main.go b/common/main.go index 643ad7e..6106b6d 100644 --- a/common/main.go +++ b/common/main.go @@ -1,25 +1,27 @@ package common import ( - "encoding/binary" "eon/lib" - "math" - "net/http" - "net/url" + "bufio" "bytes" "errors" "fmt" + "math" "os" "plugin" "regexp" + "sort" "strconv" "strings" "time" "crypto/ed25519" "database/sql" + "encoding/binary" "math/big" + "net/http" + "net/url" "golang.org/x/term" @@ -39,6 +41,12 @@ type InstallPackage struct { Repository lib.Repository } +type RemovePackage struct { + Name string + Priority int + DisplayData lib.DisplayData +} + type PluginInfo struct { Name string HasErrHandler bool @@ -53,7 +61,7 @@ type Plugin struct { } var conn *sql.DB -var dbVersion = semver.MustParse("1.0.0-beta.3") +var dbVersion = semver.MustParse("1.0.0-beta.4") var DefaultLogger = lib.Logger{ LogFunc: func(log lib.Log) string { @@ -83,7 +91,7 @@ var DefaultLogger = lib.Logger{ // Get the terminal width so we can calculate the width of the progress bar. width, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { - fmt.Println(color.RedString("Failed to get terminal width: " + err.Error())) + fmt.Println(color.RedString("Could not get terminal width: " + err.Error())) os.Exit(1) } // Calculate the percentage in text form. @@ -123,13 +131,13 @@ var DefaultLogger = lib.Logger{ } if log.Prompt { fmt.Print(": ") - var userInput string - _, err := fmt.Scanln(&userInput) + reader := bufio.NewReader(os.Stdin) + userInput, err := reader.ReadString('\n') if err != nil { - fmt.Println(color.RedString("Failed to read input: " + err.Error())) + fmt.Println(color.RedString("[FATAL]"), "Could not read user input:", err) os.Exit(1) } else { - return userInput + return userInput[:len(userInput)-1] } } return "" @@ -185,45 +193,44 @@ var TimeMagnitudes = []humanize.RelTimeMagnitude{ // End of Expat / MIT licensed code } -func HandleDependencies(previousDeps int, targetEPK InstallPackage, parentPriority int, epkList []lib.RemoteEPK, ListRemotePackagesInDB func() ([]lib.RemoteEPK, error), InstallPackageList *[]InstallPackage, logger *lib.Logger) (int, error) { +func HandleDependencies(previousDeps int, targetEPK InstallPackage, parentPriority int, epkList []lib.RemoteEPK, ListRemotePackagesInDB func() ([]lib.RemoteEPK, error), InstallPackageList []InstallPackage, previousPackages *[]string, logger *lib.Logger) ([]InstallPackage, int, error) { // Iterate through the dependencies of the target EPK. dependencyLoop: - for _, dependency := range targetEPK.EPKPreMap.Dependencies { + for _, dependency := range targetEPK.EPKPreMap.DisplayData.Dependencies { // Check if the dependency is already in the list of EPKs to install. - for iterator, epk := range *InstallPackageList { - if epk.EPKPreMap.Name == dependency { + for iterator, epk := range InstallPackageList { + if epk.EPKPreMap.DisplayData.Name == dependency { // The dependency is already in the list of EPKs to install, check for its dependencies. - if len(epk.EPKPreMap.Dependencies) == 0 || epk.EPKPreMap.Dependencies == nil { + if len(epk.EPKPreMap.DisplayData.Dependencies) == 0 || epk.EPKPreMap.DisplayData.Dependencies == nil { // All dependencies are handled - change the priority and continue with the next dependency. epk.Priority = parentPriority + 1 - currentInstallPackageList := *InstallPackageList - currentInstallPackageList[iterator] = epk - InstallPackageList = ¤tInstallPackageList + InstallPackageList[iterator] = epk continue dependencyLoop } else { // Check if it's a circular dependency. - for _, epk := range *InstallPackageList { - if epk.EPKPreMap.Name == targetEPK.EPKPreMap.Name { + for _, epk := range *previousPackages { + if epk == targetEPK.EPKPreMap.DisplayData.Name { // We have a circular dependency. Crash immediately. logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Circular dependency detected: " + targetEPK.EPKPreMap.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.Name, + Content: "Circular dependency detected: " + targetEPK.EPKPreMap.DisplayData.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.DisplayData.Name, Prompt: false, }) } } + // Add the dependency to the list of previous packages. + *previousPackages = append(*previousPackages, targetEPK.EPKPreMap.DisplayData.Name) // Recursively handle dependencies. - addedDeps, err := HandleDependencies(previousDeps, epk, parentPriority+1, epkList, ListRemotePackagesInDB, InstallPackageList, logger) + installedPackage, addedDeps, err := HandleDependencies(previousDeps, epk, parentPriority+1, epkList, ListRemotePackagesInDB, InstallPackageList, previousPackages, logger) + InstallPackageList = installedPackage if err != nil { - return 0, err + return nil, 0, err } else { // Add the dependencies to the total number of dependencies. previousDeps += addedDeps // All dependencies are now handled - change the priority and continue with the next dependency. epk.Priority = parentPriority + 1 - currentInstallPackageList := *InstallPackageList - currentInstallPackageList[iterator] = epk - InstallPackageList = ¤tInstallPackageList + InstallPackageList[iterator] = epk continue dependencyLoop } } @@ -235,14 +242,14 @@ dependencyLoop: var err error epkList, err = ListRemotePackagesInDB() if err != nil { - return 0, err + return nil, 0, err } } // Check if we already have the EPK installed. version, exists, err := DefaultCheckEPKInDB(dependency) if err != nil { - return 0, err + return nil, 0, err } var remoteEPK lib.RemoteEPK @@ -270,7 +277,7 @@ dependencyLoop: } // If the dependency doesn't exist, crash. if !dependencyExists { - return 0, errors.New("dependency " + dependency + " does not exist") + return nil, 0, errors.New("dependency " + dependency + " does not exist") } // Increase the dependency's priority. epkEntry.Priority = parentPriority + 1 @@ -279,7 +286,7 @@ dependencyLoop: if err != nil { logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to join URL path: " + err.Error(), + Content: "Could not join URL path: " + err.Error(), Prompt: false, }) } @@ -289,7 +296,7 @@ dependencyLoop: // Map the EPKs display data. epkPreMap, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger) if err != nil || vagueErr != nil { - return 0, err + return nil, 0, err } // Set the EPKs display data. epkEntry.EPKPreMap = &epkPreMap @@ -304,40 +311,42 @@ dependencyLoop: // Check if the dependency has dependencies, and if so, recursively handle them. if !(len(remoteEPK.Dependencies) == 0 || remoteEPK.Dependencies == nil) { // Check if it's a circular dependency. - for _, epk := range *InstallPackageList { - if epk.EPKPreMap.Name == targetEPK.EPKPreMap.Name { + for _, epk := range *previousPackages { + if epk == targetEPK.EPKPreMap.DisplayData.Name { // We have a circular dependency. Crash immediately. logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Circular dependency detected: " + targetEPK.EPKPreMap.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.Name, + Content: "Circular dependency detected: " + targetEPK.EPKPreMap.DisplayData.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.DisplayData.Name, Prompt: false, }) } } + // Add the dependency to the list of previous packages. + *previousPackages = append(*previousPackages, epkEntry.EPKPreMap.DisplayData.Name) // Recursively handle dependencies. - addedDeps, err := HandleDependencies(previousDeps, epkEntry, epkEntry.Priority+1, epkList, ListRemotePackagesInDB, InstallPackageList, logger) + installPackages, addedDeps, err := HandleDependencies(previousDeps, epkEntry, epkEntry.Priority+1, epkList, ListRemotePackagesInDB, InstallPackageList, previousPackages, logger) + InstallPackageList = installPackages if err != nil { - return 0, err + return nil, 0, err } // Add the dependencies to the total number of dependencies. previousDeps += addedDeps } // All dependencies are now handled - continue with the next dependency. - currentInstallPackageList := append(*InstallPackageList, epkEntry) - InstallPackageList = ¤tInstallPackageList + InstallPackageList = append(InstallPackageList, epkEntry) previousDeps++ continue } // If we reach this point, all dependencies have been handled. - return previousDeps, nil + return InstallPackageList, previousDeps, nil } func GetTotalSize(InstallPackageList []InstallPackage) *big.Int { totalSize := new(big.Int) for _, epk := range InstallPackageList { - totalSize.Add(totalSize, big.NewInt(epk.EPKPreMap.Size)) + totalSize.Add(totalSize, big.NewInt(epk.EPKPreMap.DisplayData.Size)) } return totalSize } @@ -346,7 +355,7 @@ func GetTotalInstalledSize(InstallPackageList []InstallPackage) *big.Int { totalSize := new(big.Int) for _, epk := range InstallPackageList { if !epk.IsRemote { - totalSize.Add(totalSize, epk.EPKPreMap.DecompressedSize) + totalSize.Add(totalSize, epk.EPKPreMap.DisplayData.DecompressedSize) } } return totalSize @@ -492,7 +501,7 @@ func InitDB(conn *sql.DB) error { // Huge query moment. This is way too big to read comfortably, but too bad! It breaks the Jetbrains SQL formatter if // I break it into multiple lines. - _, err = conn.Exec("CREATE TABLE packages (name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, longDescription TEXT NOT NULL, version TEXT NOT NULL, author TEXT NOT NULL, license TEXT NOT NULL, architecture TEXT NOT NULL, size INTEGER NOT NULL, dependencies TEXT NOT NULL, removeScript TEXT NOT NULL, hasRemoveScript BOOLEAN NOT NULL, isDependency BOOLEAN NOT NULL DEFAULT false, repository TEXT)") + _, err = conn.Exec("CREATE TABLE packages (name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, longDescription TEXT NOT NULL, version TEXT NOT NULL, author TEXT NOT NULL, license TEXT NOT NULL, architecture TEXT NOT NULL, size INTEGER NOT NULL, dependencies TEXT NOT NULL, removeScript TEXT NOT NULL, hasRemoveScript BOOLEAN NOT NULL, isDependency BOOLEAN NOT NULL DEFAULT false, repository TEXT, installedPaths TEXT)") if err != nil { return err } @@ -590,7 +599,7 @@ func EstablishDBConnection(logger *lib.Logger) error { } else { logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "The database is corrupted and cannot be used. Please reset the database.", + Content: "The database is corrupted and could not be used. Please reset the database.", Prompt: false, }) os.Exit(1) @@ -598,7 +607,7 @@ func EstablishDBConnection(logger *lib.Logger) error { } else { logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "The database is corrupted and cannot be used. Please reset the database.", + Content: "The database is corrupted and could not be used. Please reset the database.", Prompt: false, }) os.Exit(1) @@ -645,7 +654,7 @@ func EstablishDBConnection(logger *lib.Logger) error { return nil } -func DefaultAddEPKToDB(metadata *lib.Metadata, removeScript []byte, dependency bool, hasRemoveScript bool, size int64, repository ...string) error { +func DefaultAddEPKToDB(metadata *lib.Metadata, installedPaths []string, removeScript []byte, dependency bool, hasRemoveScript bool, size int64, repository ...string) error { // If it already exists, delete it. This may happen in a force-install scenario. _, err := conn.Exec("DELETE FROM packages WHERE name = ?", metadata.Name) if err != nil { @@ -663,6 +672,13 @@ func DefaultAddEPKToDB(metadata *lib.Metadata, removeScript []byte, dependency b dependencies = "[]" } + var installedPathsString string + if len(installedPaths) > 0 { + installedPathsString = "[" + strings.Join(installedPaths, ", ") + "]" + } else { + installedPathsString = "[]" + } + // Another fat unreadable line so Jetbrains doesn't break the SQL formatter. Too bad! // But seriously though why doesn't Jetbrains support this? It's literally their own IDE which tells me to not make // super-long lines, but then doesn't support it. @@ -671,14 +687,15 @@ func DefaultAddEPKToDB(metadata *lib.Metadata, removeScript []byte, dependency b // Not that I'm complaining or anything. // - Arzumify if len(repository) > 0 { - _, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + _, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository, installedPaths) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author, metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency, - repository[0]) + repository[0], installedPathsString) } else { - _, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + _, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, installedPaths) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author, - metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency) + metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency, + installedPathsString) } if err != nil { return err @@ -712,32 +729,111 @@ func DefaultCheckEPKInDB(name string) (*semver.Version, bool, error) { return version, true, nil } -func DefaultGetEPKFromDB(name string) (lib.Metadata, string, bool, bool, int64, error) { +func DefaultGetEPKFromDB(name string) (lib.Metadata, string, bool, bool, int64, string, []string, error) { var metadata lib.Metadata var removeScript string var dependency bool var versionString string var dependencies string var hasRemoveScript bool + var repository string + var installedPaths string var size int64 - err := conn.QueryRow("SELECT name, description, longDescription, version, author, license, architecture, size, "+ - "dependencies, removeScript, hasRemoveScript, isDependency FROM packages WHERE name = ?", name).Scan( - &metadata.Name, &metadata.Description, &metadata.LongDescription, &versionString, &metadata.Author, + err := conn.QueryRow("SELECT description, longDescription, version, author, license, architecture, size, "+ + "dependencies, removeScript, hasRemoveScript, isDependency, repository, installedPaths FROM packages WHERE name = ?", + name).Scan(&metadata.Description, &metadata.LongDescription, &versionString, &metadata.Author, &metadata.License, &metadata.Architecture, &size, &dependencies, &removeScript, &hasRemoveScript, - &dependency) + &dependency, &repository, &installedPaths) if err != nil { - return lib.Metadata{}, "", false, false, 0, err + return lib.Metadata{}, "", false, false, 0, "", nil, err } // This is the world's most basic JSON unmarshaller :P // - Arzumify metadata.Dependencies = strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ") version, err := semver.NewVersion(versionString) if err != nil { - return lib.Metadata{}, "", false, false, 0, err + return lib.Metadata{}, "", false, false, 0, "", nil, err } + // Also unmarshal the installed paths + var installedPathsSlice []string + installedPathsSlice = strings.Split(strings.TrimSuffix(strings.TrimPrefix(installedPaths, "["), "]"), ", ") + // For some reason NewVersion returns a pointer metadata.Version = *version - return metadata, removeScript, dependency, hasRemoveScript, size, nil + return metadata, removeScript, dependency, hasRemoveScript, size, repository, installedPathsSlice, nil +} + +func DefaultRemoveEPKFromDB(name string) error { + _, err := conn.Exec("DELETE FROM packages WHERE name = ?", name) + if err != nil { + return err + } + return nil +} + +func DefaultListEPKsInDB() (map[string]lib.DisplayData, map[string]*big.Int, map[string]lib.Repository, map[string]int64, error) { + rows, err := conn.Query("SELECT name, description, version, author, architecture, size, dependencies, repository FROM packages") + if err != nil { + return nil, nil, nil, nil, err + } + + repositoryMap := make(map[string]lib.Repository) + repoNameToRepo := make(map[string]lib.Repository) + metadataMap := make(map[string]lib.DisplayData) + sizes := make(map[string]*big.Int) + compressedSizes := make(map[string]int64) + for rows.Next() { + var metadata lib.DisplayData + var size int64 + var dependencies string + var repository string + var version string + err := rows.Scan(&metadata.Name, &metadata.Description, &version, + &metadata.Author, &metadata.Architecture, &size, &dependencies, &repository) + if err != nil { + return nil, nil, nil, nil, err + } + semVer, err := semver.NewVersion(version) + if err != nil { + return nil, nil, nil, nil, err + } + // Stupid pointer + metadata.Version = *semVer + metadata.Dependencies = strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ") + metadataMap[metadata.Name] = metadata + sizes[metadata.Name] = big.NewInt(size) + compressedSizes[metadata.Name] = size + // Check if the repository is in the repoNameToRepo map + _, ok := repoNameToRepo[repository] + if !ok { + // If it's not, find the repository and add it to the map + var repo lib.Repository + err := conn.QueryRow("SELECT url, owner, description FROM repositories WHERE name = ?", repository).Scan(&repo.URL, &repo.Owner, &repo.Description) + if err != nil { + return nil, nil, nil, nil, err + } + repositoryMap[repository] = repo + } + // Add the repository to the repository map + repositoryMap[metadata.Name] = repoNameToRepo[repository] + } + + return metadataMap, sizes, repositoryMap, compressedSizes, nil +} + +func DefaultGetEPKRemoveInfoFromDB(name string) (string, []string, error) { + var removeScript string + var installedPaths string + err := conn.QueryRow("SELECT removeScript, installedPaths FROM packages WHERE name = ?", name).Scan(&removeScript, &installedPaths) + if err != nil { + return "", nil, err + } + + // Unmarshal the installed paths + var installedPathsSlice []string + installedPathsSlice = strings.Split(strings.TrimSuffix(strings.TrimPrefix(installedPaths, "["), "]"), ", ") + + return removeScript, installedPathsSlice, nil } func DefaultAddFingerprintToDB(fingerprint []byte, owner string, replace bool) error { @@ -781,11 +877,12 @@ func DefaultGetFingerprintFromDB(fingerprint []byte, author string) (bool, bool, func DefaultAddRepositoryToDB(repository lib.Repository, forceReplace bool) error { if forceReplace { // Delete the repository if it already exists. This may happen in a force-install scenario. - _, err := conn.Exec("DELETE FROM repositories WHERE name = ?", repository.Name) + _, err := conn.Exec("DELETE FROM repositories WHERE name = ? OR url = ?", repository.Name, repository.URL) if err != nil { return err } } + _, err := conn.Exec("INSERT INTO repositories (url, name, owner, description) VALUES (?, ?, ?, ?)", repository.URL, repository.Name, repository.Owner, repository.Description) if err != nil { @@ -833,6 +930,7 @@ func DefaultListRepositoriesInDB() ([]lib.Repository, error) { } repositories = append(repositories, lib.Repository{Name: name, URL: repoUrl, Owner: owner, Description: description}) } + return repositories, nil } @@ -876,7 +974,7 @@ func DefaultRemoveRemotePackageFromDB(name string) error { } func DefaultListRemotePackagesInDB() ([]lib.RemoteEPK, error) { - rows, err := conn.Query("SELECT name, author, description, version, architecture, size, dependencies, path, arch, hash, repository FROM remotePackages") + rows, err := conn.Query("SELECT name, author, description, version, architecture, size, dependencies, path, arch, hash, repository FROM remotePackages ORDER BY version") if err != nil { return []lib.RemoteEPK{}, err } @@ -923,6 +1021,11 @@ func DefaultListRemotePackagesInDB() ([]lib.RemoteEPK, error) { }) } + // Re-sort remotePackages by version, latest first - this stops the package manager from installing older versions. + sort.Slice(remotePackages, func(i, j int) bool { + return remotePackages[i].Version.GreaterThan(&remotePackages[j].Version) + }) + return remotePackages, nil } @@ -1029,34 +1132,94 @@ func RefreshPackageList(listRepositoriesInDB func() ([]lib.Repository, error), a // PreMapEPKHandleError handles errors that occur during the mapping of an EPKs display data. func PreMapEPKHandleError(err error, vagueErr error, logger *lib.Logger) { switch { - case errors.Is(vagueErr, lib.ErrPreMapEPKFailedToReadError): + case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotRead): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to read file: " + err.Error(), + Content: "Could not read file: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrPreMapEPKNotEPKError): + case errors.Is(vagueErr, lib.ErrPreMapEPKHasNetworkStream): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Network streams are not supported: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapEPKHasNotGotEPKMagic): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is not an EPK.", Prompt: false, }) - case errors.Is(vagueErr, lib.ErrPreMapEPKInvalidEndianError): + case errors.Is(vagueErr, lib.ErrPreMapEPKHasInvalidEndian): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is corrupted or invalid: invalid endian", Prompt: false, }) - case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotParseJSONError): + case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotMapJSON): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to parse metadata JSON: " + err.Error(), + Content: "Could not map metadata JSON: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotMapJSONError): + } +} + +// PreMapRemoteEPKHandleError handles errors that occur during the mapping of a remote EPKs display data. +func PreMapRemoteEPKHandleError(err error, vagueErr error, logger *lib.Logger) { + switch { + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCreateURL): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to map metadata JSON: " + err.Error(), + Content: "Could not create URL: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCreateRequest): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not create request: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotSendRequest): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not send request: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotRead): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not read EPK: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCloseConnection): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not close connection: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKUnexpectedStatusCode): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Unexpected status code: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapEPKHasNotGotEPKMagic): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "The specified file is not an EPK.", + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKInvalidEndian): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "The specified file is corrupted or invalid: invalid endian", + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotMapJSON): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not map metadata JSON: " + err.Error(), Prompt: false, }) } @@ -1065,40 +1228,40 @@ func PreMapEPKHandleError(err error, vagueErr error, logger *lib.Logger) { // FullyMapMetadataHandleError handles errors that occur during the mapping of an EPK file. func FullyMapMetadataHandleError(err error, vagueErr error, logger *lib.Logger) { switch { - case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToReadError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotRead): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to read file: " + err.Error(), + Content: "Could not read file: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToJumpError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotJump): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to jump to metadata: " + err.Error(), + Content: "Could not jump to metadata: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToAddFingerprintError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotAddFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to add fingerprint to database: " + err.Error(), + Content: "Could not add fingerprint to database: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToGetFingerprintError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotGetFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to get fingerprint from database: " + err.Error(), + Content: "Could not get fingerprint from database: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrFullyMapMetadataInvalidSignatureError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataHasInvalidSignature): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is corrupted or invalid: signature mismatch", Prompt: false, }) - case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotMapJSONError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotMapJSON): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to map metadata JSON: " + err.Error(), + Content: "Could not map metadata JSON: " + err.Error(), Prompt: false, }) } @@ -1108,76 +1271,76 @@ func FullyMapMetadataHandleError(err error, vagueErr error, logger *lib.Logger) func InstallEPKHandleError(tempDir string, err error, vagueErr error, logger *lib.Logger) { doNotRemoveTempDir := false switch { - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateTempDirError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateTempDir): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to create temporary directory: " + err.Error(), + Content: "Could not create temporary directory: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateZStandardReaderError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateZStandardReader): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to create ZStandard reader: " + err.Error(), + Content: "Could not create ZStandard reader: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotDecompressTarArchiveError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotDecompressTarArchive): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to decompress tar archive: " + err.Error(), + Content: "Could not decompress tar archive: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateDirError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateDir): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to create directory: " + err.Error(), + Content: "Could not create directory: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatDirError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatDir): logger.LogFunc(lib.Log{ Level: "ERROR", Content: "Could not get file information about directory: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatFileError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatFile): logger.LogFunc(lib.Log{ Level: "ERROR", Content: "Could not get file information about file: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateFileError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateFile): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to create file: " + err.Error(), + Content: "Could not create file: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCloseTarReaderError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCloseTarReader): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to close tar reader: " + err.Error(), + Content: "Could not close tar reader: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatHookError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatHook): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not get file information about hook: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRunHookError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRunHook): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to run hook: " + err.Error(), + Content: "Could not run hook: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotAddEPKToDBError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotAddEPKToDB): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to add EPK to database: " + err.Error(), + Content: "Could not add EPK to database: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRemoveTempDirError): + case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRemoveTempDir): logger.LogFunc(lib.Log{ Level: "CRITICAL", - Content: "Failed to remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually", + Content: "Could not remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually", Prompt: false, }) doNotRemoveTempDir = true @@ -1187,7 +1350,7 @@ func InstallEPKHandleError(tempDir string, err error, vagueErr error, logger *li if err != nil { logger.LogFunc(lib.Log{ Level: "CRITICAL", - Content: "Failed to remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually", + Content: "Could not remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually", Prompt: false, }) } @@ -1196,94 +1359,82 @@ func InstallEPKHandleError(tempDir string, err error, vagueErr error, logger *li func AddRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) { switch { - case errors.Is(vagueErr, lib.ErrAddRepositoryCannotCreateRequestError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotCreateRequest): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to create request: " + err.Error(), + Content: "Could not create request: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryCannotSendRequestError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotSendRequest): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to send request: " + err.Error(), + Content: "Could not send request: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryUnexpectedStatusCodeError): + case errors.Is(vagueErr, lib.ErrAddRepositoryHasUnexpectedStatusCode): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Unexpected status code: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryCannotReadResponseError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotReadResponse): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to read response: " + err.Error(), + Content: "Could not read response: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryInvalidMagicError): + case errors.Is(vagueErr, lib.ErrAddRepositoryHasInvalidMagic): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Invalid magic: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryCannotHashError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotHash): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to copy file to hash: " + err.Error(), + Content: "Could not copy file to hash: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryUnmarshalMetadataError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotUnmarshalMetadata): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to unmarshal metadata: " + err.Error(), + Content: "Could not unmarshal metadata: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToGetFingerprintError): + case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotGetFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to get fingerprint from database: " + err.Error(), + Content: "Could not get fingerprint from database: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddFingerprintError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to add fingerprint to database: " + err.Error(), + Content: "Could not add fingerprint to database: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryInvalidMetadataError): + case errors.Is(vagueErr, lib.ErrAddRepositoryHasInvalidMetadata): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Invalid metadata: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryCannotCreateDirError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddPackage): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to create repository directory: " + err.Error(), + Content: "Could not add package to database: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryCannotStatDirError): - logger.LogFunc(lib.Log{ - Level: "FATAL", - Content: "Failed to get file information about repository directory: " + err.Error(), - Prompt: false, - }) - case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddPackageError): - logger.LogFunc(lib.Log{ - Level: "FATAL", - Content: "Failed to add package to database: " + err.Error(), - Prompt: false, - }) - case errors.Is(vagueErr, lib.ErrAddRepositoryRepositoryAlreadyExistsError): + case errors.Is(vagueErr, lib.ErrAddRepositoryHasRepositoryExists): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Repository already exists", Prompt: false, }) - case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddRepositoryError): + case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddRepository): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to add repository to database: " + err.Error(), + Content: "Could not add repository to database: " + err.Error(), Prompt: false, }) @@ -1292,28 +1443,70 @@ func AddRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) { func RemoveRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) { switch { - case errors.Is(vagueErr, lib.ErrRemoveRepositoryDoesNotExistError): + case errors.Is(vagueErr, lib.ErrRemoveRepositoryDoesNotExist): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Repository does not exist", Prompt: false, }) - case errors.Is(vagueErr, lib.ErrRemoveRepositoryCannotFindRepositoryError): + case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotFindRepository): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to get file information about repository: " + err.Error(), + Content: "Could not get file information about repository: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrRemoveRepositoryCannotRemoveRepositoryError): + case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotRemoveRepository): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to remove repository files: " + err.Error(), + Content: "Could not remove repository files: " + err.Error(), Prompt: false, }) - case errors.Is(vagueErr, lib.ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError): + case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotRemoveRepositoryFromDB): logger.LogFunc(lib.Log{ Level: "FATAL", - Content: "Failed to remove repository from database: " + err.Error(), + Content: "Could not remove repository from database: " + err.Error(), + Prompt: false, + }) + } +} + +// RemoveEPKHandleError handles errors that occur during the removal of an EPK. +func RemoveEPKHandleError(err error, vagueErr error, logger *lib.Logger) { + switch { + case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotFindEPK): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not get EPK from database: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotCreateTempFile): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not create temporary file: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotWriteTempFile): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not write to temporary file: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRunRemoveHook): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not run remove hook: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRemoveEPKFromDB): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not remove EPK from database: " + err.Error(), + Prompt: false, + }) + case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRemoveFiles): + logger.LogFunc(lib.Log{ + Level: "FATAL", + Content: "Could not remove files: " + err.Error(), Prompt: false, }) } diff --git a/lib/main.go b/lib/main.go index 500b88e..1643c5d 100644 --- a/lib/main.go +++ b/lib/main.go @@ -5,7 +5,6 @@ import ( "bytes" "errors" "io" - "net/url" "os" "strconv" "strings" @@ -18,6 +17,7 @@ import ( "encoding/json" "math/big" "net/http" + "net/url" "os/exec" "path/filepath" @@ -25,7 +25,6 @@ import ( "github.com/cespare/xxhash/v2" "github.com/dustin/go-humanize" "github.com/klauspost/compress/zstd" - "modernc.org/sqlite" ) // RemoteEPK is a struct that contains the metadata of an EPK from a remote repository @@ -74,17 +73,23 @@ type Metadata struct { // EPKPreMap is a struct that contains the metadata of the EPK type EPKPreMap struct { + DisplayData DisplayData + MetadataMap map[string]interface{} + IsLittleEndian bool + IsUpgrade bool + TarOffset int64 +} + +// DisplayData is a struct that contains the display data of the EPK +type DisplayData struct { Name string Author string Architecture string + Description string Version semver.Version Size int64 DecompressedSize *big.Int Dependencies []string - MetadataMap map[string]interface{} - IsLittleEndian bool - IsUpgrade bool - TarOffset int64 } // PotentiallyNullEPKPreMap is a EPKPreMap that can be nil @@ -111,6 +116,7 @@ type StreamOrBytes struct { URL string Bytes []byte IsURL bool + IsRemote bool IsFileStream bool } @@ -160,6 +166,42 @@ func ByteToFingerprint(input []byte) string { return strings.Join(result, ":") } +// MkdirAllWithPaths mimics os.MkdirAll but returns the created directories +func MkdirAllWithPaths(path string, perm os.FileMode) ([]string, error) { + // Make sure to return absolute paths + absPath, err := filepath.Abs(path) + if err != nil { + return nil, err + } + + // Split the path into individual directories + var dirs []string + currentPath := absPath + for currentPath != "/" { + dirs = append([]string{currentPath}, dirs...) + currentPath = filepath.Dir(currentPath) + } + + // Slice to hold the created directory paths + var createdDirs []string + + // Iterate through each directory and create if not exists + for _, dir := range dirs { + _, err := os.Stat(dir) + if errors.Is(err, os.ErrNotExist) { + // Directory doesn't exist, create it + err := os.Mkdir(dir, perm) + if err != nil { + return createdDirs, err + } + // Append created directory's absolute path + createdDirs = append(createdDirs, dir) + } + } + + return createdDirs, nil +} + func preMapEpkFromBytes(metaDataBytes []byte, littleEndian bool, size int64, offset int64) (EPKPreMap, error) { // Unmarshal the JSON var displayDataMap map[string]interface{} @@ -175,7 +217,7 @@ func preMapEpkFromBytes(metaDataBytes []byte, littleEndian bool, size int64, off parsedDisplayData.MetadataMap = displayDataMap parsedDisplayData.IsLittleEndian = littleEndian parsedDisplayData.TarOffset = offset - parsedDisplayData.Size = size + parsedDisplayData.DisplayData.Size = size // Map the display data var ok bool @@ -184,14 +226,14 @@ func preMapEpkFromBytes(metaDataBytes []byte, littleEndian bool, size int64, off if !ok { return EPKPreMap{}, errors.New("size is not a string") } - parsedDisplayData.DecompressedSize = new(big.Int) - parsedDisplayData.DecompressedSize.SetString(sizeBigInt, 10) + parsedDisplayData.DisplayData.DecompressedSize = new(big.Int) + parsedDisplayData.DisplayData.DecompressedSize.SetString(sizeBigInt, 10) // Set the name, author, version, arch, and dependencies - parsedDisplayData.Name, ok = displayDataMap["name"].(string) + parsedDisplayData.DisplayData.Name, ok = displayDataMap["name"].(string) if !ok { return EPKPreMap{}, errors.New("name is not a string") } - parsedDisplayData.Author, ok = displayDataMap["author"].(string) + parsedDisplayData.DisplayData.Author, ok = displayDataMap["author"].(string) if !ok { return EPKPreMap{}, errors.New("author is not a string") } @@ -203,8 +245,8 @@ func preMapEpkFromBytes(metaDataBytes []byte, littleEndian bool, size int64, off if err != nil { return EPKPreMap{}, err } - parsedDisplayData.Version = *versionPointer - parsedDisplayData.Architecture, ok = displayDataMap["arch"].(string) + parsedDisplayData.DisplayData.Version = *versionPointer + parsedDisplayData.DisplayData.Architecture, ok = displayDataMap["arch"].(string) if !ok { return EPKPreMap{}, errors.New("arch is not a string") } @@ -212,7 +254,7 @@ func preMapEpkFromBytes(metaDataBytes []byte, littleEndian bool, size int64, off if !ok { return EPKPreMap{}, errors.New("dependencies is not an array") } - parsedDisplayData.Dependencies, err = interfaceToStringSlice(dependencies, "dependencies") + parsedDisplayData.DisplayData.Dependencies, err = interfaceToStringSlice(dependencies, "dependencies") if err != nil { return EPKPreMap{}, err } @@ -223,18 +265,17 @@ func preMapEpkFromBytes(metaDataBytes []byte, littleEndian bool, size int64, off // ConstMapEPKMetadataOffset is the offset of the metadata in the EPK: 3 magic bytes, 1 endian byte, 8 offset bytes, 64 signature bytes, and 32 public key bytes var ConstMapEPKMetadataOffset int64 = 108 -var ErrPreMapEPKFailedToReadError = errors.New("failed to read EPK") -var ErrPreMapEPKNetworkStreamError = errors.New("network streams are not supported") -var ErrPreMapEPKNotEPKError = errors.New("not an EPK") -var ErrPreMapEPKInvalidEndianError = errors.New("invalid endian") -var ErrPreMapEPKCouldNotParseJSONError = errors.New("error marshaling metadata") -var ErrPreMapEPKCouldNotMapJSONError = errors.New("error mapping metadata") +var ErrPreMapEPKCouldNotRead = errors.New("could not read EPK") +var ErrPreMapEPKHasNetworkStream = errors.New("network streams are not supported") +var ErrPreMapEPKHasNotGotEPK = errors.New("has not got an EPK") +var ErrPreMapEPKHasInvalidEndian = errors.New("has invalid endian") +var ErrPreMapEPKCouldNotMapJSON = errors.New("could not map metadata") // PreMapEPK maps enough data to create the display summary of an EPK func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) { // Say that we don't support network streams if epkBytes.IsURL { - return EPKPreMap{}, nil, ErrPreMapEPKNetworkStreamError + return EPKPreMap{}, nil, ErrPreMapEPKHasNetworkStream } // First, we need to check if it even is a EPK by checking the first 3 magic bytes @@ -242,14 +283,14 @@ func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) var magicBytes = make([]byte, 3) _, err := epkBytes.FileStream.ReadAt(magicBytes, 0) if err != nil { - return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapEPKCouldNotRead } if string(magicBytes) != "epk" { - return EPKPreMap{}, nil, ErrPreMapEPKNotEPKError + return EPKPreMap{}, nil, ErrPreMapEPKHasNotGotEPK } } else { if string(epkBytes.Bytes[0:3]) != "epk" { - return EPKPreMap{}, nil, ErrPreMapEPKNotEPKError + return EPKPreMap{}, nil, ErrPreMapEPKHasNotGotEPK } } @@ -259,14 +300,14 @@ func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) var littleEndianByte = make([]byte, 1) _, err := epkBytes.FileStream.ReadAt(littleEndianByte, 3) if err != nil { - return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapEPKCouldNotRead } if littleEndianByte[0] == 0x6C { littleEndian = true } else if littleEndianByte[0] == 0x62 { littleEndian = false } else { - return EPKPreMap{}, nil, ErrPreMapEPKInvalidEndianError + return EPKPreMap{}, nil, ErrPreMapEPKHasInvalidEndian } } else { if epkBytes.Bytes[3] == 0x6C { @@ -274,7 +315,7 @@ func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) } else if epkBytes.Bytes[3] == 0x62 { littleEndian = false } else { - return EPKPreMap{}, nil, ErrPreMapEPKInvalidEndianError + return EPKPreMap{}, nil, ErrPreMapEPKHasInvalidEndian } } @@ -284,7 +325,7 @@ func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) var tarArchiveOffsetBytes = make([]byte, 8) _, err := epkBytes.FileStream.ReadAt(tarArchiveOffsetBytes, 4) if err != nil { - return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapEPKCouldNotRead } if littleEndian { tarArchiveOffset = int64(binary.LittleEndian.Uint64(tarArchiveOffsetBytes)) @@ -306,45 +347,45 @@ func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) var metadataBuffer = make([]byte, tarArchiveOffset-ConstMapEPKMetadataOffset) _, err := epkBytes.FileStream.ReadAt(metadataBuffer, ConstMapEPKMetadataOffset) if err != nil { - return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapEPKCouldNotRead } preMapEpk, err = preMapEpkFromBytes(metadataBuffer, littleEndian, epkSize, tarArchiveOffset) } else { var err error preMapEpk, err = preMapEpkFromBytes(epkBytes.Bytes[ConstMapEPKMetadataOffset:tarArchiveOffset], littleEndian, epkSize, tarArchiveOffset) if err != nil { - return EPKPreMap{}, err, ErrPreMapEPKCouldNotMapJSONError + return EPKPreMap{}, err, ErrPreMapEPKCouldNotMapJSON } } return preMapEpk, nil, nil } -var ErrPreMapRemoteEPKCannotCreateURLError = errors.New("could not create URL") -var ErrPreMapRemoteEPKCannotCreateRequestError = errors.New("could not create request") -var ErrPreMapRemoteEPKFailedToSendRequestError = errors.New("failed to send request") -var ErrPreMapRemoteEPKFailedToReadError = errors.New("failed to read EPK") -var ErrPreMapRemoteEPKFailedToCloseConnectionError = errors.New("failed to close connection") -var ErrPreMapRemoteEPKUnexpectedStatusCodeError = errors.New("unexpected status code") -var ErrPreMapRemoteEPKNotEPKError = errors.New("not an EPK") -var ErrPreMapRemoteEPKInvalidEndianError = errors.New("invalid endian") -var ErrPreMapRemoteEPKCouldNotMapJSONError = errors.New("error mapping metadata") +var ErrPreMapRemoteEPKCouldNotCreateURL = errors.New("could not create URL") +var ErrPreMapRemoteEPKCouldNotCreateRequest = errors.New("could not create request") +var ErrPreMapRemoteEPKCouldNotSendRequest = errors.New("could not send request") +var ErrPreMapRemoteEPKCouldNotRead = errors.New("could not read EPK") +var ErrPreMapRemoteEPKCouldNotCloseConnection = errors.New("could not close connection") +var ErrPreMapRemoteEPKUnexpectedStatusCode = errors.New("unexpected status code") +var ErrPreMapEPKHasNotGotEPKMagic = errors.New("not an EPK") +var ErrPreMapRemoteEPKInvalidEndian = errors.New("invalid endian") +var ErrPreMapRemoteEPKCouldNotMapJSON = errors.New("error mapping metadata") func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, error) { // Fetch the first 12 bytes of the EPK - this contains the magic, endian, and offset // We use the range header to only fetch the first 12 bytes packageUrl, err := url.JoinPath(remoteEPK.Repository.URL, remoteEPK.Path) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKCannotCreateURLError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotCreateURL } req, err := http.NewRequest("GET", packageUrl, nil) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKCannotCreateRequestError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotCreateRequest } req.Header.Set("Range", "bytes=0-12") resp, err := http.DefaultClient.Do(req) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToSendRequestError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotSendRequest } // Check if the status code is 206 (partial content) @@ -367,29 +408,29 @@ func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, err _, err := resp.Body.Read(epkHeaderBytes) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotRead } rangeSupported = false } else if resp.StatusCode == 206 { // Great, everything is working as expected. _, err := io.ReadFull(resp.Body, epkHeaderBytes) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotRead } rangeSupported = true // Close the connection err = resp.Body.Close() if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToCloseConnectionError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotCloseConnection } } else { // Something went wrong - return EPKPreMap{}, errors.New("unexpected status code: " + strconv.Itoa(resp.StatusCode)), ErrPreMapRemoteEPKUnexpectedStatusCodeError + return EPKPreMap{}, errors.New("unexpected status code: " + strconv.Itoa(resp.StatusCode)), ErrPreMapRemoteEPKUnexpectedStatusCode } // Now we verify the magic bytes if string(epkHeaderBytes[0:3]) != "epk" { - return EPKPreMap{}, nil, ErrPreMapRemoteEPKNotEPKError + return EPKPreMap{}, nil, ErrPreMapEPKHasNotGotEPKMagic } // Let's determine the endian-ness of the EPK via the 3rd byte @@ -399,7 +440,7 @@ func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, err } else if epkHeaderBytes[3] == 0x62 { littleEndian = false } else { - return EPKPreMap{}, nil, ErrPreMapRemoteEPKInvalidEndianError + return EPKPreMap{}, nil, ErrPreMapRemoteEPKInvalidEndian } // Now we can get the offsets of the tar archive @@ -419,17 +460,17 @@ func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, err req.Header.Set("Range", "bytes=108-"+strconv.FormatInt(tarArchiveOffset-1, 10)) resp, err = http.DefaultClient.Do(req) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToSendRequestError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotSendRequest } // Read the display data _, err = io.ReadFull(resp.Body, displayDataBytes) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotRead } // Close the connection err = resp.Body.Close() if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToCloseConnectionError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotCloseConnection } } else { // Re-use the connection to read the display data @@ -437,7 +478,7 @@ func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, err // meaning that the web server will have already iterated past the header _, err = io.ReadFull(resp.Body, displayDataBytes) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToReadError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotRead } // You didn't have to cut me off, make out like it never happened and that we were nothing @@ -445,7 +486,7 @@ func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, err // Now you're just some obscure web server that I used to know err = resp.Body.Close() if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKFailedToCloseConnectionError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotCloseConnection } } @@ -453,7 +494,7 @@ func PreMapRemoteEPK(remoteEPK RemoteEPK, logger *Logger) (EPKPreMap, error, err var preMapEpk EPKPreMap preMapEpk, err = preMapEpkFromBytes(displayDataBytes, littleEndian, remoteEPK.CompressedSize, tarArchiveOffset) if err != nil { - return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotMapJSONError + return EPKPreMap{}, err, ErrPreMapRemoteEPKCouldNotMapJSON } return preMapEpk, nil, nil @@ -558,16 +599,16 @@ func handlePublicKeyCheck(exists bool, matchingAuthor bool, matchingFingerprint return nil } -var ErrFullyMapMetadataFailedToJumpError = errors.New("failed to jump to offset") -var ErrFullyMapMetadataFailedToReadError = errors.New("failed to read EPK") -var ErrFullyMapMetadataFailedToAddFingerprintError = errors.New("failed to add fingerprint") -var ErrFullyMapMetadataFailedToGetFingerprintError = errors.New("failed to get fingerprint") -var ErrFullyMapMetadataInvalidSignatureError = errors.New("invalid signature") -var ErrFullyMapMetadataCouldNotMapJSONError = errors.New("error mapping metadata") +var ErrFullyMapMetadataCouldNotRead = errors.New("could not read EPK") +var ErrFullyMapMetadataCouldNotJump = errors.New("could not jump to offset") +var ErrFullyMapMetadataCouldNotAddFingerprint = errors.New("could not add fingerprint") +var ErrFullyMapMetadataCouldNotGetFingerprint = errors.New("could not get fingerprint") +var ErrFullyMapMetadataHasInvalidSignature = errors.New("invalid signature") +var ErrFullyMapMetadataCouldNotMapJSON = errors.New("error mapping metadata") // FullyMapMetadata maps an EPK file, but is significantly slower than PreMapEPK. Use PreMapEPK if you only need the display data. // it pulls data from PreMapEPK to reduce the amount of work needed to map the EPK. -func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprintInDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, logger *Logger) (*Metadata, error, error) { +func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprintInDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, warnUserAboutNoRange func(*Logger), logger *Logger) (*Metadata, error, error) { // We define the signature and public key bytes here so that we can read them later signature := make([]byte, 64) publicKey := make([]byte, 32) @@ -578,25 +619,25 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin // To get the signature, we read from the 12th byte to the 76th byte _, err := epkBytes.FileStream.ReadAt(signature, 12) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } // To get the public key, we read from the 76th byte to the 108th byte _, err = epkBytes.FileStream.ReadAt(publicKey, 76) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } } else if epkBytes.IsURL { // Before we continue, check if the signature is valid // Fetch range 12 - EOF and read them in req, err := http.NewRequest("GET", epkBytes.URL, nil) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } req.Header.Set("Range", "bytes=12-") resp, err := http.DefaultClient.Do(req) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } // Set the connection connection = resp.Body @@ -605,32 +646,26 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin // Not great, not terrible. // We'll have to cut off the connection early later. // Warn the user - logger.LogFunc(Log{ - Level: "INFO", - Content: "The server does not support range requests. It is recommended to use the -O flag to download " + - "the entire file to memory rather than streaming it, which in this case, will be significantly slower, " + - "as we have to read then immediately discard bytes in order to reach an offset. You've likely already " + - "been warned by the repository refresh command, so I won't prompt you again.", - }) + warnUserAboutNoRange(logger) // Discard the first 12 bytes _, err := io.CopyN(io.Discard, connection, 12) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } } else if resp.StatusCode != 206 { - return &Metadata{}, errors.New("unexpected status code: " + strconv.Itoa(resp.StatusCode)), ErrFullyMapMetadataFailedToReadError + return &Metadata{}, errors.New("unexpected status code: " + strconv.Itoa(resp.StatusCode)), ErrFullyMapMetadataCouldNotRead } // Read the signature _, err = io.ReadFull(connection, signature) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } // Read the public key _, err = io.ReadFull(connection, publicKey) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } } else { // Make signature and public key the optimised bytes @@ -639,13 +674,13 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin } // Let's check for the public key in the database - exists, matchingAuthor, matchingFingerprint, err := checkFingerprintInDB(publicKey, preMap.Author) + exists, matchingAuthor, matchingFingerprint, err := checkFingerprintInDB(publicKey, preMap.DisplayData.Author) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToGetFingerprintError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotGetFingerprint } else { - err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, preMap.Author, addFingerprintToDB, logger) + err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, preMap.DisplayData.Author, addFingerprintToDB, logger) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToAddFingerprintError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotAddFingerprint } } @@ -656,44 +691,44 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin // Seeking is better than using ReadAt because it allows us to not have to load the entire file into memory _, err = epkBytes.FileStream.Seek(ConstMapEPKMetadataOffset, io.SeekStart) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToJumpError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotJump } // Streaming bytes to the hash is more memory efficient _, err = epkBytes.FileStream.WriteTo(xxHash) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } // Verify the signature (we verify the hash because it's cheaper than verifying the entire EPK) if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) { - return &Metadata{}, nil, ErrFullyMapMetadataInvalidSignatureError + return &Metadata{}, nil, ErrFullyMapMetadataHasInvalidSignature } } else if epkBytes.IsURL { // Now we can verify the signature. We can just stream the rest of the EPK to the hash _, err = io.Copy(xxHash, connection) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } // You didn't have to cut me off... // Don't worry, we are reading to EOF anyway, no matter if we do have a non-range supported server, so we // (probably) won't upset the server owner. err = connection.Close() if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } // Verify the signature (we verify the hash because it's cheaper than verifying the entire EPK) if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) { - return &Metadata{}, nil, ErrFullyMapMetadataInvalidSignatureError + return &Metadata{}, nil, ErrFullyMapMetadataHasInvalidSignature } } else { // We now verify the signature in one go without streaming _, err := xxHash.Write(epkBytes.Bytes[ConstMapEPKMetadataOffset:]) if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotRead } if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) { - return &Metadata{}, nil, ErrFullyMapMetadataInvalidSignatureError + return &Metadata{}, nil, ErrFullyMapMetadataHasInvalidSignature } } @@ -704,23 +739,23 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin var parsedSpecialFiles SpecialFiles specialFilesMap, ok := preMap.MetadataMap["specialFiles"].(map[string]interface{}) if !ok { - return &Metadata{}, errors.New("specialFiles is not an object"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("specialFiles is not an object"), ErrFullyMapMetadataCouldNotMapJSON } noDelete, ok := specialFilesMap["noDelete"].([]interface{}) if !ok { - return &Metadata{}, errors.New("noDelete is not an array"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("noDelete is not an array"), ErrFullyMapMetadataCouldNotMapJSON } parsedSpecialFiles.NoDelete, err = interfaceToStringSlice(noDelete, "noDelete") if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSON } noReplace, ok := specialFilesMap["noReplace"].([]interface{}) if !ok { - return &Metadata{}, errors.New("noReplace is not an array"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("noReplace is not an array"), ErrFullyMapMetadataCouldNotMapJSON } parsedSpecialFiles.NoReplace, err = interfaceToStringSlice(noReplace, "noReplace") if err != nil { - return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSON } // Declare the parsedMetadata object @@ -730,32 +765,32 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin parsedMetadata.SpecialFiles = parsedSpecialFiles // Steal some data from the PreMapEPK object - parsedMetadata.Name = preMap.Name - parsedMetadata.Version = preMap.Version - parsedMetadata.Architecture = preMap.Architecture - parsedMetadata.Size = preMap.Size - parsedMetadata.Dependencies = preMap.Dependencies + parsedMetadata.Name = preMap.DisplayData.Name + parsedMetadata.Version = preMap.DisplayData.Version + parsedMetadata.Architecture = preMap.DisplayData.Architecture + parsedMetadata.Size = preMap.DisplayData.Size + parsedMetadata.Dependencies = preMap.DisplayData.Dependencies // Map the metadata parsedMetadata.Description, ok = preMap.MetadataMap["desc"].(string) if !ok { - return &Metadata{}, errors.New("description is not a string"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("description is not a string"), ErrFullyMapMetadataCouldNotMapJSON } parsedMetadata.LongDescription, ok = preMap.MetadataMap["longDesc"].(string) if !ok { - return &Metadata{}, errors.New("longDesc is not a string"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("longDesc is not a string"), ErrFullyMapMetadataCouldNotMapJSON } parsedMetadata.Author, ok = preMap.MetadataMap["author"].(string) if !ok { - return &Metadata{}, errors.New("author is not a string"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("author is not a string"), ErrFullyMapMetadataCouldNotMapJSON } parsedMetadata.License, ok = preMap.MetadataMap["license"].(string) if !ok { - return &Metadata{}, errors.New("license is not a string"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("license is not a string"), ErrFullyMapMetadataCouldNotMapJSON } decompressedSizeString, ok := preMap.MetadataMap["size"].(string) if !ok { - return &Metadata{}, errors.New("size is not a string"), ErrFullyMapMetadataCouldNotMapJSONError + return &Metadata{}, errors.New("size is not a string"), ErrFullyMapMetadataCouldNotMapJSON } parsedMetadata.DecompressedSize = new(big.Int) parsedMetadata.DecompressedSize.SetString(decompressedSizeString, 10) @@ -763,24 +798,18 @@ func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprin return &parsedMetadata, nil, nil } -// ResolveDependencies resolves the dependencies of an EPK -// func ResolveDependencies(epk Epk) (error, error) { -// TODO: Implement this when I finish up repositories -// return nil, nil -// } - -var ErrInstallEPKCouldNotCreateTempDirError = errors.New("could not create temporary directory") -var ErrInstallEPKCouldNotCreateZStandardReaderError = errors.New("could not create ZStandard reader") -var ErrInstallEPKCouldNotDecompressTarArchiveError = errors.New("could not decompress tar archive") -var ErrInstallEPKCouldNotCreateDirError = errors.New("could not create directory") -var ErrInstallEPKCouldNotStatDirError = errors.New("could not stat directory") -var ErrInstallEPKCouldNotStatFileError = errors.New("could not stat file") -var ErrInstallEPKCouldNotCreateFileError = errors.New("could not create file") -var ErrInstallEPKCouldNotCloseTarReaderError = errors.New("could not close tar reader") -var ErrInstallEPKCouldNotStatHookError = errors.New("could not stat hook") -var ErrInstallEPKCouldNotRunHookError = errors.New("could not run hook") -var ErrInstallEPKCouldNotAddEPKToDBError = errors.New("could not add EPK to database") -var ErrInstallEPKCouldNotRemoveTempDirError = errors.New("could not remove temporary directory") +var ErrInstallEPKCouldNotCreateTempDir = errors.New("could not create temporary directory") +var ErrInstallEPKCouldNotCreateZStandardReader = errors.New("could not create ZStandard reader") +var ErrInstallEPKCouldNotDecompressTarArchive = errors.New("could not decompress tar archive") +var ErrInstallEPKCouldNotCreateDir = errors.New("could not create directory") +var ErrInstallEPKCouldNotStatDir = errors.New("could not stat directory") +var ErrInstallEPKCouldNotStatFile = errors.New("could not stat file") +var ErrInstallEPKCouldNotCreateFile = errors.New("could not create file") +var ErrInstallEPKCouldNotCloseTarReader = errors.New("could not close tar reader") +var ErrInstallEPKCouldNotStatHook = errors.New("could not stat hook") +var ErrInstallEPKCouldNotRunHook = errors.New("could not run hook") +var ErrInstallEPKCouldNotAddEPKToDB = errors.New("could not add EPK to database") +var ErrInstallEPKCouldNotRemoveTempDir = errors.New("could not remove temporary directory") // ProgressWriter implements a writer that intercepts writes in order to log progress type ProgressWriter struct { @@ -819,11 +848,11 @@ func (writer *ProgressWriter) Write(p []byte) (n int, err error) { } // InstallEPK installs an EPK file -func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, addEPKToDB func(*Metadata, []byte, bool, bool, int64, ...string) error, logger *Logger) (string, error, error) { +func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, addEPKToDB func(*Metadata, []string, []byte, bool, bool, int64, ...string) error, logger *Logger) (string, error, error) { // Create the temporary directory tempDir, err := os.MkdirTemp("/tmp", "eon-install-") if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateTempDirError + return tempDir, err, ErrInstallEPKCouldNotCreateTempDir } var zStandardReader *zstd.Decoder @@ -838,20 +867,20 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a // Create a ZStandard reader reading from the EPK zStandardReader, err = zstd.NewReader(epkBytes.FileStream) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError + return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReader } } else if epkBytes.IsURL { // Range header to the tar offset req, err := http.NewRequest("GET", epkBytes.URL, nil) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError + return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReader } // Set the range header req.Header.Set("Range", "bytes="+strconv.FormatInt(preMap.TarOffset, 10)+"-") // Send the request resp, err := http.DefaultClient.Do(req) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError + return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReader } // Set connection to the response body connection = resp.Body @@ -866,22 +895,22 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a Writer: io.Discard, }, connection, preMap.TarOffset) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError + return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchive } } else if resp.StatusCode != 206 { // Something went wrong - return tempDir, errors.New("unexpected status code: " + strconv.Itoa(resp.StatusCode)), ErrInstallEPKCouldNotCreateZStandardReaderError + return tempDir, errors.New("unexpected status code: " + strconv.Itoa(resp.StatusCode)), ErrInstallEPKCouldNotCreateZStandardReader } // Create a ZStandard reader reading from the EPK zStandardReader, err = zstd.NewReader(connection) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError + return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReader } } else { // Create a ZStandard reader reading from the EPKs in-memory bytes zStandardReader, err = zstd.NewReader(bytes.NewReader(epkBytes.Bytes[preMap.TarOffset:])) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError + return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReader } } @@ -916,6 +945,9 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a } }() + // Create a slice of the installed files + var installedFiles []string + // Iterate through the tar archive for { // Read the next header @@ -931,7 +963,7 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a // If there was an error, return the error case err != nil: - return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError + return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchive // This should never happen, but if it does, we should just continue case header == nil: @@ -940,12 +972,14 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a // Get the target path var target string + var isHook bool if strings.HasPrefix(header.Name, "root") { target = strings.TrimPrefix(header.Name, "root") } else if strings.HasPrefix(header.Name, "hooks") { target = filepath.Join(tempDir, header.Name) + isHook = true } else { - return tempDir, errors.New("invalid path in EPK: " + header.Name), ErrInstallEPKCouldNotDecompressTarArchiveError + return tempDir, errors.New("invalid path in EPK: " + header.Name), ErrInstallEPKCouldNotDecompressTarArchive } switch header.Typeflag { @@ -954,14 +988,31 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a _, err := os.Stat(target) if err != nil { // If the directory does not exist, create it - if os.IsNotExist(err) { - // Make sure to replicate the permissions of the directory - err := os.MkdirAll(target, header.FileInfo().Mode()) + if errors.Is(err, os.ErrNotExist) { + // All directories are 0755 + paths, err := MkdirAllWithPaths(target, 0755) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateDirError + return tempDir, err, ErrInstallEPKCouldNotCreateDir + } else { + // Check if the files are in noDelete + for _, file := range metadata.SpecialFiles.NoDelete { + if strings.TrimSuffix(target, "/") == strings.TrimSuffix(file, "/") { + // This file is a special file and should not be deleted + continue + } + } + if !isHook { + // Add the directory to the installed files + installedFiles = append(installedFiles, target) + + // Add the paths to the installed files + if paths != nil { + installedFiles = append(installedFiles, paths...) + } + } } } else { - return tempDir, err, ErrInstallEPKCouldNotStatDirError + return tempDir, err, ErrInstallEPKCouldNotStatDir } } else { // If it does exist, don't touch it @@ -972,40 +1023,69 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a _, err := os.Stat(filepath.Dir(target)) if err != nil { // No, it doesn't. Create the directory - if os.IsNotExist(err) { - // We assume 0755 for any directories that don't exist - err := os.MkdirAll(filepath.Dir(target), 0755) + if errors.Is(err, os.ErrNotExist) { + // We assume 0755 for directories + paths, err := MkdirAllWithPaths(filepath.Dir(target), 0755) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateDirError + return tempDir, err, ErrInstallEPKCouldNotCreateDir + } else { + // Check if the files are in noDelete + for _, file := range metadata.SpecialFiles.NoDelete { + if strings.TrimSuffix(target, "/") == strings.TrimSuffix(file, "/") { + // This file is a special file and should not be deleted + continue + } + } + if !isHook { + // Add the directory to the installed files + installedFiles = append(installedFiles, filepath.Dir(target)) + + // Add the paths to the installed files + if paths != nil { + installedFiles = append(installedFiles, paths...) + } + } } } else { - return tempDir, err, ErrInstallEPKCouldNotStatDirError + return tempDir, err, ErrInstallEPKCouldNotStatDir } } // Check if the file already exists _, err = os.Stat(target) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { // Great, the file does not exist. Let's create it. file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode)) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCreateFileError + return tempDir, err, ErrInstallEPKCouldNotCreateFile } writtenFile, err := io.Copy(file, tarReader) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError + return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchive } written.Add(written, big.NewInt(writtenFile)) err = file.Close() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCloseTarReaderError + return tempDir, err, ErrInstallEPKCouldNotCloseTarReader + } else { + // Check if the files are in noDelete + for _, file := range metadata.SpecialFiles.NoDelete { + if strings.TrimSuffix(target, "/") == strings.TrimSuffix(file, "/") { + // This file is a special file and should not be deleted + continue + } + } + if !isHook { + // Add the file to the installed files + installedFiles = append(installedFiles, target) + } } } else { - return tempDir, err, ErrInstallEPKCouldNotStatFileError + return tempDir, err, ErrInstallEPKCouldNotStatFile } } else { // See if it's an upgrade or not @@ -1028,7 +1108,7 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a if epkBytes.IsURL { err = connection.Close() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotCloseTarReaderError + return tempDir, err, ErrInstallEPKCouldNotCloseTarReader } } @@ -1036,18 +1116,18 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a if preMap.IsUpgrade { _, err := os.Stat(filepath.Join(tempDir, "hooks", "upgrade.sh")) if err != nil { - if !os.IsNotExist(err) { - return tempDir, err, ErrInstallEPKCouldNotStatHookError + if !errors.Is(err, os.ErrNotExist) { + return tempDir, err, ErrInstallEPKCouldNotStatHook } } else { cmd := exec.Command("/bin/sh", filepath.Join(tempDir, "hooks", "upgrade.sh"), metadata.Version.String()) stderr, err := cmd.StderrPipe() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRunHookError + return tempDir, err, ErrInstallEPKCouldNotRunHook } err = cmd.Start() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRunHookError + return tempDir, err, ErrInstallEPKCouldNotRunHook } scanner := bufio.NewScanner(stderr) scanner.Split(bufio.ScanWords) @@ -1060,24 +1140,24 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a } err = cmd.Wait() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRunHookError + return tempDir, err, ErrInstallEPKCouldNotRunHook } } } else { _, err := os.Stat(filepath.Join(tempDir, "hooks", "install.sh")) if err != nil { - if !os.IsNotExist(err) { - return tempDir, err, ErrInstallEPKCouldNotStatHookError + if !errors.Is(err, os.ErrNotExist) { + return tempDir, err, ErrInstallEPKCouldNotStatHook } } else { cmd := exec.Command("/bin/sh", filepath.Join(tempDir, "hooks", "install.sh"), metadata.Version.String()) stderr, err := cmd.StderrPipe() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRunHookError + return tempDir, err, ErrInstallEPKCouldNotRunHook } err = cmd.Start() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRunHookError + return tempDir, err, ErrInstallEPKCouldNotRunHook } scanner := bufio.NewScanner(stderr) scanner.Split(bufio.ScanWords) @@ -1090,7 +1170,17 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a } err = cmd.Wait() if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRunHookError + return tempDir, err, ErrInstallEPKCouldNotRunHook + } + } + } + + // Do one more double-check to make sure nothing in installedFiles is in noDelete + for _, file := range metadata.SpecialFiles.NoDelete { + for index, installedFile := range installedFiles { + if strings.TrimSuffix(installedFile, "/") == strings.TrimSuffix(file, "/") { + // Remove the file from the installed files + installedFiles = append(installedFiles[:index], installedFiles[index+1:]...) } } } @@ -1098,35 +1188,35 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a // Finally, add the EPK and remove script to the database file, err := os.ReadFile(filepath.Join(tempDir, "hooks", "remove.sh")) if err != nil { - if !os.IsNotExist(err) { + if !errors.Is(err, os.ErrNotExist) { var err error - if !epkBytes.IsURL { - err = addEPKToDB(metadata, []byte{}, false, false, metadata.Size) + if !epkBytes.IsRemote { + err = addEPKToDB(metadata, installedFiles, []byte{}, false, false, metadata.Size) } else { - err = addEPKToDB(metadata, []byte{}, false, false, metadata.Size, epkBytes.RepositoryName) + err = addEPKToDB(metadata, installedFiles, []byte{}, false, false, metadata.Size, epkBytes.RepositoryName) } if err != nil { - return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError + return tempDir, err, ErrInstallEPKCouldNotAddEPKToDB } } else { - return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError + return tempDir, err, ErrInstallEPKCouldNotAddEPKToDB } } else { var err error - if !epkBytes.IsURL { - err = addEPKToDB(metadata, file, false, true, metadata.Size) + if !epkBytes.IsRemote { + err = addEPKToDB(metadata, installedFiles, file, false, true, metadata.Size) } else { - err = addEPKToDB(metadata, file, false, true, metadata.Size, epkBytes.RepositoryName) + err = addEPKToDB(metadata, installedFiles, file, false, true, metadata.Size, epkBytes.RepositoryName) } if err != nil { - return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError + return tempDir, err, ErrInstallEPKCouldNotAddEPKToDB } } // Remove the temporary directory err = os.RemoveAll(tempDir) if err != nil { - return tempDir, err, ErrInstallEPKCouldNotRemoveTempDirError + return tempDir, err, ErrInstallEPKCouldNotRemoveTempDir } stop <- true @@ -1140,21 +1230,19 @@ func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, a return "", nil, nil } -var ErrAddRepositoryCannotCreateRequestError = errors.New("cannot create request") -var ErrAddRepositoryCannotSendRequestError = errors.New("cannot send request") -var ErrAddRepositoryUnexpectedStatusCodeError = errors.New("unexpected status code") -var ErrAddRepositoryCannotReadResponseError = errors.New("cannot read response") -var ErrAddRepositoryInvalidMagicError = errors.New("invalid magic") -var ErrAddRepositoryCannotHashError = errors.New("cannot write to hash") -var ErrAddRepositoryUnmarshalMetadataError = errors.New("cannot unmarshal metadata") -var ErrAddRepositoryFailedToGetFingerprintError = errors.New("failed to get fingerprint") -var ErrAddRepositoryFailedToAddFingerprintError = errors.New("failed to add fingerprint") -var ErrAddRepositoryInvalidMetadataError = errors.New("invalid metadata") -var ErrAddRepositoryCannotCreateDirError = errors.New("cannot create directory") -var ErrAddRepositoryCannotStatDirError = errors.New("cannot stat directory") -var ErrAddRepositoryFailedToAddPackageError = errors.New("failed to add package to database") -var ErrAddRepositoryRepositoryAlreadyExistsError = errors.New("repository already exists") -var ErrAddRepositoryFailedToAddRepositoryError = errors.New("failed to add repository to database") +var ErrAddRepositoryCouldNotCreateRequest = errors.New("could not create request") +var ErrAddRepositoryCouldNotSendRequest = errors.New("could not send request") +var ErrAddRepositoryHasUnexpectedStatusCode = errors.New("unexpected status code") +var ErrAddRepositoryCouldNotReadResponse = errors.New("could not read response") +var ErrAddRepositoryHasInvalidMagic = errors.New("invalid magic") +var ErrAddRepositoryCouldNotHash = errors.New("could not write to hash") +var ErrAddRepositoryCouldNotUnmarshalMetadata = errors.New("could not unmarshal metadata") +var ErrAddRepositoryCouldNotGetFingerprint = errors.New("could not get fingerprint") +var ErrAddRepositoryCouldNotAddFingerprint = errors.New("could not add fingerprint") +var ErrAddRepositoryHasInvalidMetadata = errors.New("invalid metadata") +var ErrAddRepositoryCouldNotAddPackage = errors.New("could not add package to database") +var ErrAddRepositoryHasRepositoryExists = errors.New("repository already exists") +var ErrAddRepositoryCouldNotAddRepository = errors.New("could not add repository to database") // AddRepository adds a repository to the database func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, getFingerprintFromDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, addRemotePackageToDB func(RemoteEPK) error, checkRepositoryInDB func(string) (bool, error), forceReplace bool, logger *Logger) (string, error, error) { @@ -1164,7 +1252,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g // Create the request magicRequest, err := http.NewRequest("GET", url+"/repository.erf", nil) if err != nil { - return "", err, ErrAddRepositoryCannotCreateRequestError + return "", err, ErrAddRepositoryCouldNotCreateRequest } // Add the range header @@ -1173,7 +1261,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g // Send the request magicResponse, err := http.DefaultClient.Do(magicRequest) if err != nil { - return "", err, ErrAddRepositoryCannotSendRequestError + return "", err, ErrAddRepositoryCouldNotSendRequest } // Check if the status code is 206 @@ -1184,7 +1272,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g // Mark it as such. hasEntireFile = true } else { - return "", errors.New("status code " + strconv.Itoa(magicResponse.StatusCode)), ErrAddRepositoryUnexpectedStatusCodeError + return "", errors.New("status code " + strconv.Itoa(magicResponse.StatusCode)), ErrAddRepositoryHasUnexpectedStatusCode } } @@ -1192,12 +1280,12 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g var magicBytes = make([]byte, 3) _, err = magicResponse.Body.Read(magicBytes) if err != nil { - return "", err, ErrAddRepositoryCannotReadResponseError + return "", err, ErrAddRepositoryCouldNotReadResponse } // Check if the magic bytes are "eon" if string(magicBytes) != "eon" { - return "", nil, ErrAddRepositoryInvalidMagicError + return "", nil, ErrAddRepositoryHasInvalidMagic } // Great. We either confirmed the repository is an Eon repository or we have the entire file. @@ -1207,7 +1295,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g var err error fullFetch, err = http.Get(url + "/repository.erf") if err != nil { - return "", err, ErrAddRepositoryCannotSendRequestError + return "", err, ErrAddRepositoryCouldNotSendRequest } } else { fullFetch = magicResponse @@ -1216,7 +1304,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g // Now we get the contents of the file contents, err := io.ReadAll(fullFetch.Body) if err != nil { - return "", err, ErrAddRepositoryCannotReadResponseError + return "", err, ErrAddRepositoryCouldNotReadResponse } // Verify the file's signature @@ -1228,7 +1316,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g jsonDecoder.UseNumber() err = jsonDecoder.Decode(&repositoryMetadata) if err != nil { - return "", err, ErrAddRepositoryUnmarshalMetadataError + return "", err, ErrAddRepositoryCouldNotUnmarshalMetadata } // Get the public key and signature @@ -1238,11 +1326,11 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g // Look for the public key in the database exists, matchingAuthor, matchingFingerprint, err := getFingerprintFromDB(publicKey, repositoryMetadata["author"].(string)) if err != nil { - return "", err, ErrAddRepositoryFailedToGetFingerprintError + return "", err, ErrAddRepositoryCouldNotGetFingerprint } else { err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, repositoryMetadata["author"].(string), addFingerprintToDB, logger) if err != nil { - return "", err, ErrAddRepositoryFailedToAddFingerprintError + return "", err, ErrAddRepositoryCouldNotAddFingerprint } } @@ -1250,12 +1338,12 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g xxHash := xxhash.New() _, err = xxHash.Write(contents[99:]) if err != nil { - return "", err, ErrAddRepositoryCannotHashError + return "", err, ErrAddRepositoryCouldNotHash } // Verify the signature if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) { - return "", errors.New("invalid signature"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("invalid signature"), ErrAddRepositoryHasInvalidMetadata } // Now we can create the repository object @@ -1264,7 +1352,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g repository.URL = url repository.Name, ok = repositoryMetadata["name"].(string) if !ok { - return "", errors.New("name is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("name is not a string"), ErrAddRepositoryHasInvalidMetadata } // In force replace mode, we don't check if the repository already exists and just replace it @@ -1272,98 +1360,98 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g // Side quest: check if the repository already exists repoExists, err := checkRepositoryInDB(repository.Name) if err != nil { - return "", err, ErrAddRepositoryFailedToAddRepositoryError + return "", err, ErrAddRepositoryCouldNotAddRepository } else if repoExists { - return "", nil, ErrAddRepositoryRepositoryAlreadyExistsError + return "", nil, ErrAddRepositoryHasRepositoryExists } } repository.Description, ok = repositoryMetadata["desc"].(string) if !ok { - return "", errors.New("desc is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("desc is not a string"), ErrAddRepositoryHasInvalidMetadata } repository.Owner, ok = repositoryMetadata["author"].(string) if !ok { - return "", errors.New("author is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("author is not a string"), ErrAddRepositoryHasInvalidMetadata } // Write the contents of the repository to the database packageList, ok := repositoryMetadata["packages"].([]interface{}) if !ok { - return "", errors.New("packages is not an array"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("packages is not an array"), ErrAddRepositoryHasInvalidMetadata } var remoteEPKs []RemoteEPK for _, epk := range packageList { epk, ok := epk.(map[string]interface{}) if !ok { - return "", errors.New("package is not an object"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package is not an object"), ErrAddRepositoryHasInvalidMetadata } name, ok := epk["name"].(string) if !ok { - return "", errors.New("package name is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package name is not a string"), ErrAddRepositoryHasInvalidMetadata } author, ok := epk["author"].(string) if !ok { - return "", errors.New("package author is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package author is not a string"), ErrAddRepositoryHasInvalidMetadata } arch, ok := epk["arch"].(string) if !ok { - return "", errors.New("package arch is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package arch is not a string"), ErrAddRepositoryHasInvalidMetadata } versionString, ok := epk["version"].(string) if !ok { - return "", errors.New("package version is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package version is not a string"), ErrAddRepositoryHasInvalidMetadata } versionPointer, err := semver.NewVersion(versionString) if err != nil { - return "", errors.New("package version is not a valid semver version"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package version is not a valid semver version"), ErrAddRepositoryHasInvalidMetadata } sizeString, ok := epk["size"].(string) if !ok { - return "", errors.New("package size is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package size is not a string"), ErrAddRepositoryHasInvalidMetadata } size, err := strconv.ParseInt(sizeString, 10, 64) if err != nil { - return "", errors.New("package size is not a number"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package size is not a number"), ErrAddRepositoryHasInvalidMetadata } dependenciesInterface, ok := epk["deps"].([]interface{}) if !ok { - return "", errors.New("package dependencies is not an array"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package dependencies is not an array"), ErrAddRepositoryHasInvalidMetadata } dependencies, err := interfaceToStringSlice(dependenciesInterface, "dependencies") if err != nil { - return "", err, ErrAddRepositoryInvalidMetadataError + return "", err, ErrAddRepositoryHasInvalidMetadata } hashJsonNumber, ok := epk["hash"].(json.Number) if !ok { - return "", errors.New("package hash is not a number"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package hash is not a number"), ErrAddRepositoryHasInvalidMetadata } var hash uint64 hash, err = strconv.ParseUint(hashJsonNumber.String(), 10, 64) if err != nil { - return "", errors.New("package hash is not a valid number"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package hash is not a valid number"), ErrAddRepositoryHasInvalidMetadata } path, ok := epk["path"].(string) if !ok { - return "", errors.New("package path is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package path is not a string"), ErrAddRepositoryHasInvalidMetadata } description, ok := epk["desc"].(string) if !ok { - return "", errors.New("package description is not a string"), ErrAddRepositoryInvalidMetadataError + return "", errors.New("package description is not a string"), ErrAddRepositoryHasInvalidMetadata } remoteEPKs = append(remoteEPKs, RemoteEPK{ @@ -1380,7 +1468,7 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g Repository: repository, }) if err != nil { - return "", err, ErrAddRepositoryFailedToAddPackageError + return "", err, ErrAddRepositoryCouldNotAddPackage } } @@ -1388,49 +1476,45 @@ func AddRepository(url string, addRepositoryToDB func(Repository, bool) error, g for _, epk := range remoteEPKs { err := addRemotePackageToDB(epk) if err != nil { - return "", err, ErrAddRepositoryFailedToAddPackageError + return "", err, ErrAddRepositoryCouldNotAddPackage } } // Add the repository to the database err = addRepositoryToDB(repository, forceReplace) if err != nil { - if err.(*sqlite.Error).Code() == 2067 { - return "", nil, ErrAddRepositoryRepositoryAlreadyExistsError - } else { - return "", err, ErrAddRepositoryFailedToAddRepositoryError - } + return "", err, ErrAddRepositoryCouldNotAddRepository } return repository.Name, nil, nil } -var ErrRemoveRepositoryDoesNotExistError = errors.New("repository does not exist") -var ErrRemoveRepositoryCannotFindRepositoryError = errors.New("cannot check for repository") -var ErrRemoveRepositoryCannotRemoveRepositoryError = errors.New("cannot remove repository") -var ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError = errors.New("failed to remove repository from database") +var ErrRemoveRepositoryDoesNotExist = errors.New("repository does not exist") +var ErrRemoveRepositoryCouldNotFindRepository = errors.New("could not check for repository") +var ErrRemoveRepositoryCouldNotRemoveRepository = errors.New("could not remove repository") +var ErrRemoveRepositoryCouldNotRemoveRepositoryFromDB = errors.New("could not remove repository from database") -func RemoveRepository(repository string, removeRepositoryFromDB func(repository string) error, checkRepositoryInDB func(repository string) (bool, error), logger *Logger) (error, error) { +func RemoveRepository(repository string, removeRepositoryFromDB func(string) error, checkRepositoryInDB func(string) (bool, error), logger *Logger) (error, error) { // First check if the repository exists exists, err := checkRepositoryInDB(repository) if err != nil { - return err, ErrRemoveRepositoryCannotFindRepositoryError + return err, ErrRemoveRepositoryCouldNotFindRepository } if !exists { - return nil, ErrRemoveRepositoryDoesNotExistError + return nil, ErrRemoveRepositoryDoesNotExist } // Purge the download cache err = os.RemoveAll(filepath.Join("/var/cache/eon/repositories/", repository)) if err != nil { - return err, ErrRemoveRepositoryCannotRemoveRepositoryError + return err, ErrRemoveRepositoryCouldNotRemoveRepository } // Remove the repository from the database err = removeRepositoryFromDB(repository) if err != nil { - return err, ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError + return err, ErrRemoveRepositoryCouldNotRemoveRepositoryFromDB } // Alright, we're done here. @@ -1441,3 +1525,109 @@ func RemoveRepository(repository string, removeRepositoryFromDB func(repository return nil, nil } + +var ErrRemoveEPKCouldNotFindEPK = errors.New("could not get EPK from database") +var ErrRemoveEPKCouldNotCreateTempFile = errors.New("could not create temporary file") +var ErrRemoveEPKCouldNotWriteTempFile = errors.New("could not write to temporary file") +var ErrRemoveEPKCouldNotRunRemoveHook = errors.New("could not run remove hook") +var ErrRemoveEPKCouldNotRemoveEPKFromDB = errors.New("could not remove EPK from database") +var ErrRemoveEPKCouldNotRemoveFiles = errors.New("could not remove files") + +func RemoveEPK(name string, removeEPKFromDB func(string) error, getEPKRemoveInfoFromDB func(name string) (string, []string, error), logger *Logger) (error, error) { + // Try to fetch the EPK from the database + removeScript, installedPaths, err := getEPKRemoveInfoFromDB(name) + if err != nil { + return err, ErrRemoveEPKCouldNotFindEPK + } + + // Save the remove script to a temporary file + removeScriptFile, err := os.CreateTemp("", "eon-remove-*.sh") + if err != nil { + return err, ErrRemoveEPKCouldNotCreateTempFile + } + + // Write the remove script to the file + _, err = removeScriptFile.Write([]byte(removeScript)) + if err != nil { + return err, ErrRemoveEPKCouldNotWriteTempFile + } + + // Run the remove script + cmd := exec.Command("/bin/sh", removeScriptFile.Name()) + stderr, err := cmd.StderrPipe() + if err != nil { + return err, ErrRemoveEPKCouldNotRunRemoveHook + } + + // Start the command + err = cmd.Start() + if err != nil { + return err, ErrRemoveEPKCouldNotRunRemoveHook + } + + // Read the output + scanner := bufio.NewScanner(stderr) + scanner.Split(bufio.ScanWords) + for scanner.Scan() { + message := scanner.Text() + logger.LogFunc(Log{ + Level: "INFO", + Content: message, + }) + } + + // Close the file + err = removeScriptFile.Close() + if err != nil { + return err, ErrRemoveEPKCouldNotRunRemoveHook + } + + // Remove the EPK from the database + err = removeEPKFromDB(name) + if err != nil { + return err, ErrRemoveEPKCouldNotRemoveEPKFromDB + } + + // Remove the installed files + for _, path := range installedPaths { + // Check if there is anything in the paths not in installedPaths + // If there is, we should not remove the directory + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Check if the path is in installedPaths + for _, installedPath := range installedPaths { + if path == installedPath { + return nil + } + } + + // If it's not, return an error + return errors.New("path is not in installedPaths") + }) + if err != nil { + if errors.Is(err, errors.New("path is not in installedPaths")) { + // The path is not in installedPaths, so we should not remove it + continue + } else if !errors.Is(err, os.ErrNotExist) { + // Something else went wrong + return err, ErrRemoveEPKCouldNotRemoveFiles + } + } else { + // The path is in installedPaths, so we should remove it + err := os.RemoveAll(path) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err, ErrRemoveEPKCouldNotRemoveFiles + } else { + // The file does not exist - we must have deleted its parent directory or the user has done our job for us + continue + } + } + } + } + + return nil, nil +}