1132 lines
37 KiB
Go
1132 lines
37 KiB
Go
package lib
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"archive/tar"
|
|
"crypto/ed25519"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"math/big"
|
|
"net/http"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"github.com/Masterminds/semver"
|
|
"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
|
|
type RemoteEPK struct {
|
|
Name string
|
|
Author string
|
|
Description string
|
|
Version semver.Version
|
|
Architecture string
|
|
CompressedSize int64
|
|
Dependencies []string
|
|
Path string
|
|
Arch string
|
|
EPKHash uint64
|
|
}
|
|
|
|
// Repository is a struct that contains the repository information
|
|
type Repository struct {
|
|
Name string
|
|
URL string
|
|
Owner string
|
|
Description string
|
|
}
|
|
|
|
// SpecialFiles is a struct that contains the special files that are not to be deleted or replaced
|
|
type SpecialFiles struct {
|
|
NoDelete []string
|
|
NoReplace []string
|
|
}
|
|
|
|
// Metadata is a struct that contains the metadata of the package
|
|
type Metadata struct {
|
|
Name string
|
|
Description string
|
|
LongDescription string
|
|
Version semver.Version
|
|
Author string
|
|
License string
|
|
Architecture string
|
|
Dependencies []string
|
|
SpecialFiles SpecialFiles
|
|
Size int64
|
|
DecompressedSize big.Int
|
|
}
|
|
|
|
// EPKPreMap is a struct that contains the metadata of the EPK
|
|
type EPKPreMap struct {
|
|
Name string
|
|
Author string
|
|
Architecture 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
|
|
type PotentiallyNullEPKPreMap struct {
|
|
EPKPreMap *EPKPreMap
|
|
Null bool
|
|
}
|
|
|
|
// Log is a struct that contains the log information
|
|
type Log struct {
|
|
Level string
|
|
Content string
|
|
Prompt bool
|
|
PlaySound bool
|
|
Progress *big.Int
|
|
Total *big.Int
|
|
Overwrite bool
|
|
}
|
|
|
|
// StreamOrBytes is a struct that contains either a stream or bytes, allowing optimising for memory or speed
|
|
type StreamOrBytes struct {
|
|
Stream *os.File
|
|
Bytes []byte
|
|
IsStream bool
|
|
}
|
|
|
|
// Logger is a struct that contains the functions and properties of the logger
|
|
type Logger struct {
|
|
LogFunc func(Log) string
|
|
PromptSupported bool
|
|
ProgressSupported bool
|
|
}
|
|
|
|
// Epk is a struct that contains the metadata and the tar archive of the EPK
|
|
type Epk struct {
|
|
Metadata Metadata
|
|
TarArchive []byte
|
|
}
|
|
|
|
// interfaceToStringSlice converts an interface slice to a string slice
|
|
func interfaceToStringSlice(interfaceSlice []interface{}, interfaceName string) ([]string, error) {
|
|
// Yes, it's meant to be empty and not nil: JSON arrays are empty, not nil
|
|
//goland:noinspection GoPreferNilSlice
|
|
stringSlice := []string{}
|
|
for _, interfaceValue := range interfaceSlice {
|
|
stringValue, ok := interfaceValue.(string)
|
|
if !ok {
|
|
return nil, errors.New(interfaceName + " are not strings")
|
|
}
|
|
stringSlice = append(stringSlice, stringValue)
|
|
}
|
|
|
|
return stringSlice, nil
|
|
}
|
|
|
|
// ByteToFingerprint converts a byte slice to an Eon fingerprint, which is similar to a legacy-style OpenSSH fingerprint
|
|
func ByteToFingerprint(input []byte) string {
|
|
xxHashWriter := xxhash.New()
|
|
_, _ = xxHashWriter.Write(input)
|
|
inputString := hex.EncodeToString(xxhash.New().Sum(nil))
|
|
var result []string
|
|
var previousChar rune
|
|
for index, char := range inputString {
|
|
if index%2 == 0 && index != 0 {
|
|
result = append(result, string(previousChar)+string(char))
|
|
} else {
|
|
previousChar = char
|
|
}
|
|
}
|
|
return strings.Join(result, ":")
|
|
}
|
|
|
|
// 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 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")
|
|
|
|
// PreMapEPK maps enough data to create the display summary of an EPK
|
|
func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) {
|
|
// First, we need to check if it even is a EPK by checking the first 3 magic bytes
|
|
if epkBytes.IsStream {
|
|
var magicBytes = make([]byte, 3)
|
|
_, err := epkBytes.Stream.ReadAt(magicBytes, 0)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
|
|
}
|
|
if string(magicBytes) != "epk" {
|
|
return EPKPreMap{}, nil, ErrPreMapEPKNotEPKError
|
|
}
|
|
} else {
|
|
if string(epkBytes.Bytes[0:3]) != "epk" {
|
|
return EPKPreMap{}, nil, ErrPreMapEPKNotEPKError
|
|
}
|
|
}
|
|
|
|
// Let's determine the endian-ness of the EPK via the 3rd byte
|
|
var littleEndian bool
|
|
if epkBytes.IsStream {
|
|
var littleEndianByte = make([]byte, 1)
|
|
_, err := epkBytes.Stream.ReadAt(littleEndianByte, 3)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
|
|
}
|
|
if littleEndianByte[0] == 0x6C {
|
|
littleEndian = true
|
|
} else if littleEndianByte[0] == 0x62 {
|
|
littleEndian = false
|
|
} else {
|
|
return EPKPreMap{}, nil, ErrPreMapEPKInvalidEndianError
|
|
}
|
|
} else {
|
|
if epkBytes.Bytes[3] == 0x6C {
|
|
littleEndian = true
|
|
} else if epkBytes.Bytes[3] == 0x62 {
|
|
littleEndian = false
|
|
} else {
|
|
return EPKPreMap{}, nil, ErrPreMapEPKInvalidEndianError
|
|
}
|
|
}
|
|
|
|
// Now we can get the offsets of the tar archive
|
|
var tarArchiveOffset int64
|
|
if epkBytes.IsStream {
|
|
var tarArchiveOffsetBytes = make([]byte, 8)
|
|
_, err := epkBytes.Stream.ReadAt(tarArchiveOffsetBytes, 4)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
|
|
}
|
|
if littleEndian {
|
|
tarArchiveOffset = int64(binary.LittleEndian.Uint64(tarArchiveOffsetBytes))
|
|
} else {
|
|
tarArchiveOffset = int64(binary.BigEndian.Uint64(tarArchiveOffsetBytes))
|
|
}
|
|
} else {
|
|
if littleEndian {
|
|
tarArchiveOffset = int64(binary.LittleEndian.Uint64(epkBytes.Bytes[4:12]))
|
|
} else {
|
|
tarArchiveOffset = int64(binary.BigEndian.Uint64(epkBytes.Bytes[4:12]))
|
|
}
|
|
}
|
|
|
|
// We don't need to validate the signature yet. We will do that when we map the full EPK, because it means
|
|
// we have to read the entire thing, which is a waste of resources, since we only need the metadata.
|
|
|
|
// Let's map the display data
|
|
var displayDataMap map[string]interface{}
|
|
if epkBytes.IsStream {
|
|
var metadataBuffer = make([]byte, tarArchiveOffset-ConstMapEPKMetadataOffset)
|
|
_, err := epkBytes.Stream.ReadAt(metadataBuffer, ConstMapEPKMetadataOffset)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
|
|
}
|
|
err = json.Unmarshal(metadataBuffer, &displayDataMap)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKCouldNotParseJSONError
|
|
}
|
|
} else {
|
|
err := json.Unmarshal(epkBytes.Bytes[ConstMapEPKMetadataOffset:tarArchiveOffset], &displayDataMap)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKCouldNotParseJSONError
|
|
}
|
|
}
|
|
|
|
// Declare the parsedDisplayData object
|
|
var parsedDisplayData EPKPreMap
|
|
|
|
// Add some of our data so that the full EPK can be mapped with less effort
|
|
parsedDisplayData.MetadataMap = displayDataMap
|
|
parsedDisplayData.IsLittleEndian = littleEndian
|
|
parsedDisplayData.Size = epkSize
|
|
parsedDisplayData.TarOffset = tarArchiveOffset
|
|
|
|
// Map the display data
|
|
var ok bool
|
|
// Set the size
|
|
sizeBigInt, ok := displayDataMap["size"].(string)
|
|
if !ok {
|
|
return EPKPreMap{}, errors.New("size is not a string"), ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
parsedDisplayData.DecompressedSize.SetString(sizeBigInt, 10)
|
|
// Set the name, author, version, arch, and dependencies
|
|
parsedDisplayData.Name, ok = displayDataMap["name"].(string)
|
|
if !ok {
|
|
return EPKPreMap{}, errors.New("name is not a string"), ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
parsedDisplayData.Author, ok = displayDataMap["author"].(string)
|
|
if !ok {
|
|
return EPKPreMap{}, errors.New("author is not a string"), ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
versionString, ok := displayDataMap["version"].(string)
|
|
if !ok {
|
|
return EPKPreMap{}, errors.New("version is not a string"), ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
versionPointer, err := semver.NewVersion(versionString)
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
parsedDisplayData.Version = *versionPointer
|
|
parsedDisplayData.Architecture, ok = displayDataMap["arch"].(string)
|
|
if !ok {
|
|
return EPKPreMap{}, errors.New("arch is not a string"), ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
dependencies, ok := displayDataMap["deps"].([]interface{})
|
|
if !ok {
|
|
return EPKPreMap{}, errors.New("dependencies is not an array"), ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
parsedDisplayData.Dependencies, err = interfaceToStringSlice(dependencies, "dependencies")
|
|
if err != nil {
|
|
return EPKPreMap{}, err, ErrPreMapEPKCouldNotMapJSONError
|
|
}
|
|
|
|
return parsedDisplayData, nil, nil
|
|
}
|
|
|
|
func handlePublicKeyCheck(exists bool, matchingAuthor bool, matchingFingerprint bool, publicKey []byte, author string, addFingerprintToDB func([]byte, string, bool) error, logger *Logger) error {
|
|
if !exists {
|
|
if logger.PromptSupported {
|
|
response := logger.LogFunc(Log{
|
|
Level: "WARN",
|
|
Content: "Public key not found in database.\nthe public key fingerprint is: " + author + " " + ByteToFingerprint(publicKey) +
|
|
"\nWould you like to trust this key (y/n)?",
|
|
Prompt: true,
|
|
})
|
|
if strings.ToLower(response) == "y" {
|
|
err := addFingerprintToDB(publicKey, author, false)
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Public key added to database.",
|
|
})
|
|
}
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Installation cancelled.",
|
|
})
|
|
}
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "FATAL",
|
|
Content: "Public key not found in database.\nthe public key fingerprint is:" + author + " " + ByteToFingerprint(publicKey) +
|
|
"\nSince non-interactive mode is enabled, the installation will not proceed.",
|
|
})
|
|
}
|
|
} else if !matchingAuthor {
|
|
if logger.PromptSupported {
|
|
response := logger.LogFunc(Log{
|
|
Level: "WARN",
|
|
Content: "Public key does not match the author.\nthe public key fingerprint is: " + author + " " + ByteToFingerprint(publicKey) +
|
|
"\nWould you like to replace the key (y/n)?",
|
|
Prompt: true,
|
|
})
|
|
if strings.ToLower(response) == "y" {
|
|
err := addFingerprintToDB(publicKey, author, true)
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Public key replaced in database.",
|
|
})
|
|
}
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "FATAL",
|
|
Content: "Installation cancelled.",
|
|
})
|
|
}
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "FATAL",
|
|
Content: "Public key does not match the author.\nThe public key is :" + author + " " + ByteToFingerprint(publicKey) +
|
|
"\nSince non-interactive mode is enabled, the installation will not proceed.",
|
|
})
|
|
}
|
|
} else if !matchingFingerprint {
|
|
if logger.PromptSupported {
|
|
response := logger.LogFunc(Log{
|
|
Level: "WARN",
|
|
Content: "Public key fingerprint does not match the author.\nThe public key is :" + author + " " + ByteToFingerprint(publicKey) +
|
|
"\nThis may be a security risk. To replace the key, type \"Yes, do as I say!\". Otherwise, type anything else.",
|
|
Prompt: true,
|
|
})
|
|
if response == "Yes, do as I say!" {
|
|
err := addFingerprintToDB(publicKey, author, true)
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Public key replaced in database.",
|
|
})
|
|
}
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "FATAL",
|
|
Content: "Installation cancelled.",
|
|
})
|
|
}
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "FATAL",
|
|
Content: "Public key fingerprint does not match the author.\nthe public key fingerprint is: " + author + " " + ByteToFingerprint(publicKey) +
|
|
"\nSince non-interactive mode is enabled, the installation will not proceed.",
|
|
})
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
// 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) {
|
|
// We define the signature and public key bytes here so that we can read them later
|
|
signature := make([]byte, 64)
|
|
publicKey := make([]byte, 32)
|
|
|
|
if epkBytes.IsStream {
|
|
// Before we continue, check if the signature is valid
|
|
// To get the signature, we read from the 12th byte to the 76th byte
|
|
_, err := epkBytes.Stream.ReadAt(signature, 12)
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError
|
|
}
|
|
|
|
// To get the public key, we read from the 76th byte to the 108th byte
|
|
_, err = epkBytes.Stream.ReadAt(publicKey, 76)
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError
|
|
}
|
|
} else {
|
|
// Make signature and public key the optimised bytes
|
|
signature = epkBytes.Bytes[12:76]
|
|
publicKey = epkBytes.Bytes[76:108]
|
|
}
|
|
|
|
// Let's check for the public key in the database
|
|
exists, matchingAuthor, matchingFingerprint, err := checkFingerprintInDB(publicKey, preMap.Author)
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataFailedToGetFingerprintError
|
|
} else {
|
|
err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, preMap.Author, addFingerprintToDB, logger)
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataFailedToAddFingerprintError
|
|
}
|
|
}
|
|
|
|
// We need to create a new xxHash instance
|
|
xxHash := xxhash.New()
|
|
if epkBytes.IsStream {
|
|
// Now we can verify the signature. First, we need to take the checksum of the metadata
|
|
// Seeking is better than using ReadAt because it allows us to not have to load the entire file into memory
|
|
_, err = epkBytes.Stream.Seek(ConstMapEPKMetadataOffset, io.SeekStart)
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataFailedToJumpError
|
|
}
|
|
|
|
// Streaming bytes to the hash is more memory efficient
|
|
_, err = epkBytes.Stream.WriteTo(xxHash)
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError
|
|
}
|
|
// 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
|
|
}
|
|
} else {
|
|
// We now verify the signature in one go without streaming
|
|
if !ed25519.Verify(publicKey, xxHash.Sum(epkBytes.Bytes[ConstMapEPKMetadataOffset:]), signature) {
|
|
return &Metadata{}, nil, ErrFullyMapMetadataInvalidSignatureError
|
|
}
|
|
}
|
|
|
|
// Great, the EPK is valid. Let's map the metadata.
|
|
// We use the metadata map provided by PreMapEPK to reduce the amount of work needed to map the EPK
|
|
|
|
// First, map SpecialFiles
|
|
var parsedSpecialFiles SpecialFiles
|
|
specialFilesMap, ok := preMap.MetadataMap["specialFiles"].(map[string]interface{})
|
|
if !ok {
|
|
return &Metadata{}, errors.New("specialFiles is not an object"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
noDelete, ok := specialFilesMap["noDelete"].([]interface{})
|
|
if !ok {
|
|
return &Metadata{}, errors.New("noDelete is not an array"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
parsedSpecialFiles.NoDelete, err = interfaceToStringSlice(noDelete, "noDelete")
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
noReplace, ok := specialFilesMap["noReplace"].([]interface{})
|
|
if !ok {
|
|
return &Metadata{}, errors.New("noReplace is not an array"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
parsedSpecialFiles.NoReplace, err = interfaceToStringSlice(noReplace, "noReplace")
|
|
if err != nil {
|
|
return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
|
|
// Declare the parsedMetadata object
|
|
var parsedMetadata Metadata
|
|
|
|
// Append parsedSpecialFiles to parsedMetadata
|
|
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
|
|
|
|
// Map the metadata
|
|
parsedMetadata.Description, ok = preMap.MetadataMap["desc"].(string)
|
|
if !ok {
|
|
return &Metadata{}, errors.New("description is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
parsedMetadata.LongDescription, ok = preMap.MetadataMap["longDesc"].(string)
|
|
if !ok {
|
|
return &Metadata{}, errors.New("longDesc is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
parsedMetadata.Author, ok = preMap.MetadataMap["author"].(string)
|
|
if !ok {
|
|
return &Metadata{}, errors.New("author is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
parsedMetadata.License, ok = preMap.MetadataMap["license"].(string)
|
|
if !ok {
|
|
return &Metadata{}, errors.New("license is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
decompressedSizeString, ok := preMap.MetadataMap["size"].(string)
|
|
if !ok {
|
|
return &Metadata{}, errors.New("size is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
|
|
}
|
|
parsedMetadata.DecompressedSize.SetString(decompressedSizeString, 10)
|
|
|
|
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")
|
|
|
|
// InstallEPK installs an EPK file
|
|
func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, addEPKToDB func(metadata *Metadata, removeScript []byte, dependency bool, hasRemoveScript bool, size int64) error, logger *Logger) (string, error, error) {
|
|
// Create the temporary directory
|
|
tempDir, err := os.MkdirTemp("/tmp", "eon-install-")
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotCreateTempDirError
|
|
}
|
|
|
|
var zStandardReader *zstd.Decoder
|
|
if epkBytes.IsStream {
|
|
// Seek to the correct position in the EPK
|
|
_, err = epkBytes.Stream.Seek(preMap.TarOffset, io.SeekStart)
|
|
if err != nil {
|
|
return "", err, nil
|
|
}
|
|
|
|
// Create a ZStandard reader reading from the EPK
|
|
zStandardReader, err = zstd.NewReader(epkBytes.Stream)
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError
|
|
}
|
|
} 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
|
|
}
|
|
}
|
|
|
|
// Create a tar reader reading from the ZStandard reader
|
|
tarReader := tar.NewReader(zStandardReader)
|
|
|
|
// Create a goroutine to see how much of the decompressed size we have decompressed
|
|
var written big.Int
|
|
stop := make(chan bool)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
if logger.ProgressSupported {
|
|
logger.LogFunc(Log{
|
|
Level: "PROGRESS",
|
|
Progress: &written,
|
|
Total: &metadata.DecompressedSize,
|
|
Overwrite: true,
|
|
})
|
|
} else {
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Decompressed " + humanize.Bytes(uint64(written.Int64())) + " of " + humanize.Bytes(uint64(metadata.DecompressedSize.Int64())),
|
|
Prompt: false,
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Iterate through the tar archive
|
|
for {
|
|
// Read the next header
|
|
header, err := tarReader.Next()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
switch {
|
|
// If we are done, break
|
|
case err == io.EOF:
|
|
break
|
|
|
|
// If there was an error, return the error
|
|
case err != nil:
|
|
return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError
|
|
|
|
// This should never happen, but if it does, we should just continue
|
|
case header == nil:
|
|
continue
|
|
}
|
|
|
|
// Get the target path
|
|
var target string
|
|
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)
|
|
} else {
|
|
return tempDir, errors.New("invalid path in EPK: " + header.Name), ErrInstallEPKCouldNotDecompressTarArchiveError
|
|
}
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
// Check if the directory exists
|
|
_, 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 err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotCreateDirError
|
|
}
|
|
} else {
|
|
return tempDir, err, ErrInstallEPKCouldNotStatDirError
|
|
}
|
|
} else {
|
|
// If it does exist, don't touch it
|
|
continue
|
|
}
|
|
case tar.TypeReg:
|
|
// Check if the file has anywhere to go
|
|
_, 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 err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotCreateDirError
|
|
}
|
|
} else {
|
|
return tempDir, err, ErrInstallEPKCouldNotStatDirError
|
|
}
|
|
}
|
|
|
|
// Check if the file already exists
|
|
_, err = os.Stat(target)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// 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
|
|
}
|
|
|
|
writtenFile, err := io.Copy(file, tarReader)
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError
|
|
}
|
|
|
|
written.Add(&written, big.NewInt(writtenFile))
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotCloseTarReaderError
|
|
}
|
|
} else {
|
|
return tempDir, err, ErrInstallEPKCouldNotStatFileError
|
|
}
|
|
} else {
|
|
// See if it's an upgrade or not
|
|
if preMap.IsUpgrade {
|
|
// Check if it's a special file
|
|
for _, file := range metadata.SpecialFiles.NoReplace {
|
|
if strings.TrimSuffix(target, "/") == strings.TrimSuffix(file, "/") {
|
|
// This file is a special file and should not be replaced
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
zStandardReader.Close()
|
|
|
|
// Now let's run the hooks
|
|
if preMap.IsUpgrade {
|
|
_, err := os.Stat(filepath.Join(tempDir, "hooks", "upgrade.sh"))
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return tempDir, err, ErrInstallEPKCouldNotStatHookError
|
|
}
|
|
} 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
|
|
}
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotRunHookError
|
|
}
|
|
scanner := bufio.NewScanner(stderr)
|
|
scanner.Split(bufio.ScanWords)
|
|
for scanner.Scan() {
|
|
message := scanner.Text()
|
|
fmt.Println(message)
|
|
}
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotRunHookError
|
|
}
|
|
}
|
|
} else {
|
|
_, err := os.Stat(filepath.Join(tempDir, "hooks", "install.sh"))
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return tempDir, err, ErrInstallEPKCouldNotStatHookError
|
|
}
|
|
} 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
|
|
}
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotRunHookError
|
|
}
|
|
scanner := bufio.NewScanner(stderr)
|
|
scanner.Split(bufio.ScanWords)
|
|
for scanner.Scan() {
|
|
message := scanner.Text()
|
|
fmt.Println(message)
|
|
}
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotRunHookError
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
err := addEPKToDB(metadata, []byte{}, false, false, metadata.Size)
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError
|
|
}
|
|
} else {
|
|
return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError
|
|
}
|
|
} else {
|
|
err := addEPKToDB(metadata, file, false, true, metadata.Size)
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError
|
|
}
|
|
}
|
|
|
|
// Remove the temporary directory
|
|
err = os.RemoveAll(tempDir)
|
|
if err != nil {
|
|
return tempDir, err, ErrInstallEPKCouldNotRemoveTempDirError
|
|
}
|
|
|
|
stop <- true
|
|
logger.LogFunc(Log{
|
|
Level: "PROGRESS",
|
|
Progress: big.NewInt(1),
|
|
Total: big.NewInt(1),
|
|
Overwrite: true,
|
|
})
|
|
|
|
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")
|
|
|
|
func AddRepository(url string, addRepositoryToDB func(repository Repository) error, getFingerprintFromDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, addRemotePackageToDB func(string, RemoteEPK) error, logger *Logger) (error, error) {
|
|
// First, fetch range 0-3 of /repository.erf
|
|
// Then, check if the first 3 bytes are "eon"
|
|
|
|
// Create the request
|
|
magicRequest, err := http.NewRequest("GET", url+"/repository.erf", nil)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotCreateRequestError
|
|
}
|
|
|
|
// Add the range header
|
|
magicRequest.Header.Add("Range", "bytes=0-3")
|
|
|
|
// Send the request
|
|
magicResponse, err := http.DefaultClient.Do(magicRequest)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotSendRequestError
|
|
}
|
|
|
|
// Check if the status code is 206
|
|
var hasEntireFile bool
|
|
if magicResponse.StatusCode != 206 {
|
|
if magicResponse.StatusCode == 200 {
|
|
// This web server does not support range requests, meaning we now have the entire file.
|
|
// Mark it as such.
|
|
hasEntireFile = true
|
|
} else {
|
|
return errors.New("status code " + strconv.Itoa(magicResponse.StatusCode)), ErrAddRepositoryUnexpectedStatusCodeError
|
|
}
|
|
}
|
|
|
|
// Check the magic bytes
|
|
var magicBytes = make([]byte, 3)
|
|
_, err = magicResponse.Body.Read(magicBytes)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotReadResponseError
|
|
}
|
|
|
|
// Check if the magic bytes are "eon"
|
|
if string(magicBytes) != "eon" {
|
|
return nil, ErrAddRepositoryInvalidMagicError
|
|
}
|
|
|
|
// Great. We either confirmed the repository is an Eon repository or we have the entire file. Both are good.
|
|
var fullFetch *http.Response
|
|
if !hasEntireFile {
|
|
// Download the rest of the file
|
|
var err error
|
|
fullFetch, err = http.Get(url + "/repository.erf")
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotSendRequestError
|
|
}
|
|
} else {
|
|
fullFetch = magicResponse
|
|
}
|
|
|
|
// Now we get the contents of the file
|
|
contents, err := io.ReadAll(fullFetch.Body)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotReadResponseError
|
|
}
|
|
|
|
// Verify the file's signature
|
|
|
|
// Unmarshal the repository metadata, which is NOT the same as the EPK metadata
|
|
var repositoryMetadata map[string]interface{}
|
|
// We use a decoder instead of unmarshal here because we need to use JSON numbers: float64 is not enough
|
|
var jsonDecoder = json.NewDecoder(bytes.NewReader(contents[99:]))
|
|
jsonDecoder.UseNumber()
|
|
err = jsonDecoder.Decode(&repositoryMetadata)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryUnmarshalMetadataError
|
|
}
|
|
|
|
// Get the public key and signature
|
|
signature := contents[3:67]
|
|
publicKey := contents[67:99]
|
|
|
|
// Look for the public key in the database
|
|
exists, matchingAuthor, matchingFingerprint, err := getFingerprintFromDB(publicKey, repositoryMetadata["author"].(string))
|
|
if err != nil {
|
|
return err, ErrAddRepositoryFailedToGetFingerprintError
|
|
} else {
|
|
err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, repositoryMetadata["author"].(string), addFingerprintToDB, logger)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryFailedToAddFingerprintError
|
|
}
|
|
}
|
|
|
|
// We need to create a new xxHash instance
|
|
xxHash := xxhash.New()
|
|
_, err = xxHash.Write(contents[99:])
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotHashError
|
|
}
|
|
|
|
// Verify the signature
|
|
if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) {
|
|
return errors.New("invalid signature"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
// Now we can create the repository object
|
|
var repository Repository
|
|
var ok bool
|
|
repository.URL = url
|
|
repository.Name, ok = repositoryMetadata["name"].(string)
|
|
if !ok {
|
|
return errors.New("name is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
repository.Description, ok = repositoryMetadata["desc"].(string)
|
|
if !ok {
|
|
return errors.New("desc is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
repository.Owner, ok = repositoryMetadata["author"].(string)
|
|
if !ok {
|
|
return errors.New("author is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
// First check if the directory exists
|
|
_, err = os.Stat("/var/lib/eon/repositories")
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// If the directory does not exist, create it
|
|
err = os.MkdirAll("/var/lib/eon/repositories", 0755)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryCannotCreateDirError
|
|
}
|
|
} else {
|
|
return err, ErrAddRepositoryCannotStatDirError
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var remoteEPKs []RemoteEPK
|
|
for _, epk := range packageList {
|
|
epk, ok := epk.(map[string]interface{})
|
|
if !ok {
|
|
return errors.New("package is not an object"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
name, ok := epk["name"].(string)
|
|
if !ok {
|
|
return errors.New("package name is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
author, ok := epk["author"].(string)
|
|
if !ok {
|
|
return errors.New("package author is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
arch, ok := epk["arch"].(string)
|
|
if !ok {
|
|
return errors.New("package arch is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
versionString, ok := epk["version"].(string)
|
|
if !ok {
|
|
return errors.New("package version is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
versionPointer, err := semver.NewVersion(versionString)
|
|
if err != nil {
|
|
return errors.New("package version is not a valid semver version"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
sizeString, ok := epk["size"].(string)
|
|
if !ok {
|
|
return errors.New("package size is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
size, err := strconv.ParseInt(sizeString, 10, 64)
|
|
if err != nil {
|
|
return errors.New("package size is not a number"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
dependenciesInterface, ok := epk["deps"].([]interface{})
|
|
if !ok {
|
|
return errors.New("package dependencies is not an array"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
dependencies, err := interfaceToStringSlice(dependenciesInterface, "dependencies")
|
|
if err != nil {
|
|
return err, ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
hashJsonNumber, ok := epk["hash"].(json.Number)
|
|
if !ok {
|
|
return errors.New("package hash is not a number"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
fmt.Println(hash)
|
|
|
|
path, ok := epk["path"].(string)
|
|
if !ok {
|
|
return errors.New("package path is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
description, ok := epk["desc"].(string)
|
|
if !ok {
|
|
return errors.New("package description is not a string"), ErrAddRepositoryInvalidMetadataError
|
|
}
|
|
|
|
remoteEPKs = append(remoteEPKs, RemoteEPK{
|
|
Name: name,
|
|
Author: author,
|
|
Description: description,
|
|
Version: *versionPointer,
|
|
Architecture: arch,
|
|
CompressedSize: size,
|
|
Dependencies: dependencies,
|
|
Path: path,
|
|
Arch: arch,
|
|
EPKHash: hash,
|
|
})
|
|
if err != nil {
|
|
return err, ErrAddRepositoryFailedToAddPackageError
|
|
}
|
|
}
|
|
|
|
// We add packages afterward so that if there is an error, we don't have to remove the packages
|
|
for _, epk := range remoteEPKs {
|
|
err := addRemotePackageToDB(repository.Name, epk)
|
|
if err != nil {
|
|
return err, ErrAddRepositoryFailedToAddPackageError
|
|
}
|
|
}
|
|
|
|
// Add the repository to the database
|
|
err = addRepositoryToDB(repository)
|
|
if err != nil {
|
|
if err.(*sqlite.Error).Code() == 2067 {
|
|
return nil, ErrAddRepositoryRepositoryAlreadyExistsError
|
|
} else {
|
|
return err, ErrAddRepositoryFailedToAddRepositoryError
|
|
}
|
|
}
|
|
|
|
// Alright, we're done here.
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Added repository " + repository.Name + " to the database.",
|
|
})
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
var ErrRemoveRepositoryDoesNotExistError = errors.New("repository does not exist")
|
|
var ErrRemoveRepositoryCannotStatRepositoryError = errors.New("cannot stat repository")
|
|
var ErrRemoveRepositoryCannotRemoveRepositoryError = errors.New("cannot remove repository")
|
|
var ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError = errors.New("failed to remove repository from database")
|
|
|
|
func RemoveRepository(repository string, removeRepositoryFromDB func(repository string) error, logger *Logger) (error, error) {
|
|
// First check if the file exists
|
|
_, err := os.Stat(filepath.Join("/var/lib/eon/repositories/", repository+".json"))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, ErrRemoveRepositoryDoesNotExistError
|
|
} else {
|
|
return err, ErrRemoveRepositoryCannotStatRepositoryError
|
|
}
|
|
}
|
|
|
|
// Remove the file
|
|
err = os.Remove(filepath.Join("/var/lib/eon/repositories/", repository+".json"))
|
|
if err != nil {
|
|
return err, ErrRemoveRepositoryCannotRemoveRepositoryError
|
|
}
|
|
|
|
// Purge the download cache
|
|
err = os.RemoveAll(filepath.Join("/var/cache/eon/repositories/", repository))
|
|
if err != nil {
|
|
return err, ErrRemoveRepositoryCannotRemoveRepositoryError
|
|
}
|
|
|
|
// Remove the repository from the database
|
|
err = removeRepositoryFromDB(repository)
|
|
if err != nil {
|
|
return err, ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError
|
|
}
|
|
|
|
// Alright, we're done here.
|
|
logger.LogFunc(Log{
|
|
Level: "INFO",
|
|
Content: "Removed repository " + repository + " from the database.",
|
|
})
|
|
|
|
return nil, nil
|
|
}
|