5 | import "C"
6 |
7 | import (
8 | "os"
9 | "fmt"
10 | "os/user"
11 | "net/url"
12 | "strings"
13 | "path/filepath"
14 | "github.com/manifoldco/promptui"
15 | )
16 |
17 | type BinaryUrl struct {
18 | FileName string
19 | Url string
20 | }
21 |
22 | type AppImageInfo struct {
23 | IsTerminalApp bool
24 | AppImageType int
25 | }
26 |
27 | // Get the user/repo from a github url
28 | func GetUserRepoFromUrl(gitHubUrl string) (string, error) {
29 | urlParsed, err := url.ParseRequestURI(gitHubUrl)
30 | if err != nil {
31 | return "", err
32 | }
33 |
34 | if urlParsed.Host != "github.com" {
35 | return "", fmt.Errorf("invalid github url")
36 | }
37 |
38 | splitPaths := strings.Split(urlParsed.EscapedPath(), "/")
39 |
40 | if len(splitPaths) < 3 {
41 | return "", fmt.Errorf("invalid github url")
42 | }
43 |
44 | return splitPaths[1] + "/" + splitPaths[2], nil
45 | }
46 |
47 | // Get the Applications directory absolute path
48 | func MakeApplicationsDirPath() (string, error) {
49 | usr, err := user.Current()
50 | if err != nil {
51 | return "", err
52 | }
53 |
54 | applicationsPath := filepath.Join(usr.HomeDir, "Applications")
55 | err = os.MkdirAll(applicationsPath, os.ModePerm)
56 | if err != nil {
57 | return "", err
58 | }
59 | return applicationsPath, nil
60 | }
61 |
62 | // Get the file path of a file in applications folder
63 | func MakeTargetFilePath(link *BinaryUrl) (string, error) {
64 | applicationsPath, err := MakeApplicationsDirPath()
65 | if err != nil {
66 | return "", err
67 | }
68 |
69 | filePath := filepath.Join(applicationsPath, link.FileName)
70 | return filePath, nil
71 | }
72 |
73 | // Make file path from a file in run-cache directory inside Applications directory
74 | func MakeTempFilePath(link *BinaryUrl) (string, error) {
75 | applicationsPath, err := MakeTempAppDirPath()
76 | if err != nil {
77 | return "", err
78 | }
79 |
80 | filePath := filepath.Join(applicationsPath, link.FileName)
81 | return filePath, nil
82 | }
83 |
84 | // Make folder run-cache inside Applications dir and return it's path
85 | func MakeTempAppDirPath() (string, error) {
86 | TempApplicationDirPath, err := MakeApplicationsDirPath()
87 | if err != nil {
88 | return "", err
89 | }
90 |
91 | TempApplicationDirPath = filepath.Join(TempApplicationDirPath, "run-cache")
92 | err = os.MkdirAll(TempApplicationDirPath, os.ModePerm)
93 | if err != nil {
94 | return "", err
95 | }
96 |
97 | return TempApplicationDirPath, nil
98 | }
99 |
100 | // List appimages to select from
101 | func PromptBinarySelection(downloadLinks []BinaryUrl) (result *BinaryUrl, err error) {
102 | if len(downloadLinks) == 1 {
103 | return &downloadLinks[0], nil
104 | }
105 |
106 | prompt := promptui.Select{
107 | Label: "Select an AppImage to install",
108 | Items: downloadLinks,
109 | Templates: &promptui.SelectTemplates{
110 | Label: " {{ .FileName }}",
111 | Active: "\U00002713 {{ .FileName }}",
112 | Inactive: " {{ .FileName }}",
113 | Selected: "\U00002713 {{ .FileName }}"},
114 | }
115 |
116 | i, _, err := prompt.Run()
117 | if err != nil {
118 | return nil, err
119 | }
120 |
121 | return &downloadLinks[i], nil
122 | }
123 |
124 | // read the update info embeded into the appimage file
125 | // func ReadUpdateInfo(appImagePath string) (string, error) {
126 | // elfFile, err := elf.Open(appImagePath)
127 | // if err != nil {
128 | // panic("Unable to open target: \"" + appImagePath + "\"." + err.Error())
129 | // }
130 |
131 | // updInfo := elfFile.Section(".upd_info")
132 | // if updInfo == nil {
133 | // panic("Missing update section on target elf ")
134 | // }
135 | // sectionData, err := updInfo.Data()
136 |
137 | // if err != nil {
138 | // panic("Unable to parse update section: " + err.Error())
139 | // }
140 |
141 | // str_end := bytes.Index(sectionData, []byte("\000"))
142 | // if str_end == -1 || str_end == 0 {
143 | // return "", fmt.Errorf("No update information found in: " + appImagePath)
144 | // }
145 |
146 | // update_info := string(sectionData[:str_end])
147 | // return update_info, nil
148 | // }
149 |
--------------------------------------------------------------------------------
/src/helpers/utils/registry.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "log"
6 | "strings"
7 | "io/ioutil"
8 | "encoding/json"
9 | "path/filepath"
10 | updateUtils "github.com/pegvin/appimage-update/util"
11 | )
12 |
13 | type RegistryEntry struct {
14 | Repo string
15 | FileSha1 string
16 | // AppName string
17 | // AppVersion string
18 | FilePath string
19 | UpdateInfo string
20 | TagName string
21 | IsTerminalApp bool
22 | AppImageType int
23 | }
24 |
25 | type Registry struct {
26 | Entries map[string]RegistryEntry
27 | }
28 |
29 | // Function to open a registry entry
30 | func OpenRegistry() (registry *Registry, err error) {
31 | path, err := makeRegistryFilePath() // Get the full path to .registry.json
32 | if err != nil {
33 | return
34 | }
35 |
36 | data, err := ioutil.ReadFile(path) // Read file
37 | if err != nil {
38 | return &Registry{Entries: map[string]RegistryEntry{}}, nil // If some error occured return a new empty registry
39 | }
40 |
41 | err = json.Unmarshal(data, ®istry) // Parse JSON data into the struct
42 | if err != nil {
43 | return
44 | }
45 |
46 | return
47 | }
48 |
49 | // Function to close a registry entry
50 | func (registry *Registry) Close() error {
51 | path, err := makeRegistryFilePath() // Get full path to .registry.json
52 | if err != nil {
53 | return err
54 | }
55 |
56 | blob, err := json.Marshal(registry) // Convert registry struct into a blob
57 | if err != nil {
58 | return err
59 | }
60 |
61 | err = ioutil.WriteFile(path, blob, 0666) // write the blob file with 666 permissions
62 | if err != nil {
63 | return err
64 | }
65 |
66 | return nil
67 | }
68 |
69 | // Add a entry to registry
70 | func (registry *Registry) Add(entry RegistryEntry) error {
71 | registry.Entries[entry.FilePath] = entry
72 | return nil
73 | }
74 |
75 | // Remove a entry from registry
76 | func (registry *Registry) Remove(filePath string) {
77 | delete(registry.Entries, filePath)
78 | }
79 |
80 | // Update registry entry
81 | func (registry *Registry) Update() {
82 | applicationsDir, err := MakeApplicationsDirPath() // Applications folder full path
83 | if err != nil {
84 | log.Fatal(err)
85 | }
86 |
87 | files, err := ioutil.ReadDir(applicationsDir) // Read all the files in the folder
88 | if err != nil {
89 | log.Fatal(err)
90 | }
91 |
92 | // Filter out all the appimage files and put them into registry
93 | for _, f := range files {
94 | if strings.HasSuffix(strings.ToLower(f.Name()), ".appimage") {
95 | filePath := filepath.Join(applicationsDir, f.Name())
96 | _, ok := registry.Entries[filePath]
97 | if !ok {
98 | entry := registry.createEntryFromFile(filePath)
99 | _ = registry.Add(entry)
100 | }
101 | }
102 | }
103 |
104 | // Remove deleted/non-existent files from registry
105 | for filePath := range registry.Entries {
106 | if _, err := os.Stat(filePath); os.IsNotExist(err) {
107 | registry.Remove(filePath)
108 | }
109 | }
110 | }
111 |
112 | // Create a new entry in the registry from a appimage file
113 | func (registry *Registry) createEntryFromFile(filePath string) RegistryEntry {
114 | fileSha1, _ := GetFileSHA1(filePath)
115 | updateInfo, _ := updateUtils.ReadUpdateInfo(filePath)
116 | entry := RegistryEntry{
117 | Repo: "",
118 | TagName: "",
119 | FileSha1: fileSha1,
120 | // AppName: "",
121 | // AppVersion: "",
122 | FilePath: filePath,
123 | UpdateInfo: updateInfo,
124 | }
125 | return entry
126 | }
127 |
128 | // Lookup a entry in the registry
129 | func (registry *Registry) Lookup(target string) (RegistryEntry, bool) {
130 | applicationsDir, _ := MakeApplicationsDirPath()
131 | possibleFullPath := filepath.Join(applicationsDir, target)
132 |
133 | for _, entry := range registry.Entries {
134 | if entry.FileSha1 == target || entry.FilePath == target ||
135 | entry.FilePath == possibleFullPath || entry.Repo == target {
136 | return entry, true
137 | }
138 | }
139 |
140 | if IsAppImageFile(target) {
141 | entry := registry.createEntryFromFile(target)
142 | _ = registry.Add(entry)
143 |
144 | return entry, true
145 | } else {
146 | if IsAppImageFile(possibleFullPath) {
147 | entry := registry.createEntryFromFile(target)
148 | _ = registry.Add(entry)
149 |
150 | return entry, true
151 | }
152 | }
153 |
154 | return RegistryEntry{}, false
155 | }
156 |
157 | // makes the registry file path
158 | func makeRegistryFilePath() (string, error) {
159 | applicationsPath, err := MakeApplicationsDirPath()
160 | if err != nil {
161 | return "", err
162 | }
163 |
164 | filePath := filepath.Join(applicationsPath, ".registry.json")
165 | return filePath, nil
166 | }
167 |
--------------------------------------------------------------------------------
/src/helpers/repos/github.go:
--------------------------------------------------------------------------------
1 | package repos
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | "bread/src/helpers/utils"
8 | "github.com/google/go-github/v31/github"
9 | )
10 |
11 | // Struct containing GitHub Repo Details
12 | type GitHubRepo struct {
13 | User string
14 | Project string
15 | Release string
16 | File string
17 | TagName string
18 | UserRepo string
19 | }
20 |
21 | // Parses string to a github repo information, and returns a object and error (if any)
22 | func NewGitHubRepo(target string, tagName string) (appInfo Application, err error) {
23 | appInfo = &GitHubRepo{}
24 | ghSource := GitHubRepo{}
25 |
26 | // parse the target as a github url and get the user/repo from it
27 | userRepo, err := utils.GetUserRepoFromUrl(target)
28 | if err == nil { // If successfull return the information
29 | userRepoSplitted := strings.Split(userRepo, "/")
30 | ghSource = GitHubRepo{
31 | User: userRepoSplitted[0],
32 | Project: userRepoSplitted[1],
33 | TagName: tagName,
34 | UserRepo: userRepoSplitted[0] + "/" + userRepoSplitted[1],
35 | }
36 | return &ghSource, nil
37 | } else {
38 | // Take the `user/repo` and split `user` and `repo`
39 | targetParts := strings.Split(target, "/")
40 |
41 | // If input is not in format of `user/repo` assume `user` and `repo` are same
42 | if len(targetParts) < 2 {
43 | ghSource = GitHubRepo{
44 | User: targetParts[0],
45 | Project: targetParts[0],
46 | TagName: tagName,
47 | UserRepo: targetParts[0] + "/" + targetParts[0],
48 | }
49 | } else {
50 | ghSource = GitHubRepo{
51 | User: targetParts[0],
52 | Project: targetParts[1],
53 | TagName: tagName,
54 | UserRepo: targetParts[0] + "/" + targetParts[1],
55 | }
56 | }
57 |
58 | return &ghSource, nil
59 | }
60 | }
61 |
62 | // Get the github user/repo from the repo information
63 | func (g GitHubRepo) Id() string {
64 | return g.UserRepo
65 | }
66 |
67 | // Gets the latest/specified tagged release from github
68 | func (g GitHubRepo) GetLatestRelease(NoPreRelease bool) (*Release, error) {
69 | client := github.NewClient(nil) // Client For Interacting with github api
70 |
71 | // Get all the releases from the target
72 | releases, _, err := client.Repositories.ListReleases(context.Background(), g.User, g.Project, nil)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | if g.TagName != "" {
78 | releaseWithTagName := getReleaseFromTagName(releases, g.TagName)
79 |
80 | if releaseWithTagName != nil {
81 | appimageFiles := getAppImageFilesFromRelease(releaseWithTagName)
82 | if len(appimageFiles) > 0 {
83 | return &Release{
84 | *releaseWithTagName.TagName,
85 | appimageFiles,
86 | }, nil
87 | }
88 | }
89 | }
90 |
91 | // Filter out files which are not AppImage
92 | for _, release := range releases {
93 | if *release.Draft {
94 | continue
95 | }
96 | if *release.Prerelease && NoPreRelease {
97 | continue
98 | }
99 |
100 | downloadLinks := getAppImageFilesFromRelease(release)
101 | if len(downloadLinks) > 0 {
102 | return &Release{
103 | *release.TagName,
104 | downloadLinks,
105 | }, nil
106 | }
107 | }
108 |
109 | return nil, NoAppImageBinariesFound
110 | }
111 |
112 | // Download appimage from remote
113 | func (g GitHubRepo) Download(binaryUrl *utils.BinaryUrl, targetPath string) (err error) {
114 | err = utils.DownloadFile(binaryUrl.Url, targetPath, 0755, "Downloading")
115 | return err
116 | }
117 |
118 | // Generate a fallback update information for a appimage
119 | func (g GitHubRepo) FallBackUpdateInfo() string {
120 | updateInfo := "gh-releases-direct|" + g.User + "|" + g.Project
121 | if g.Release == "" {
122 | updateInfo += "|latest"
123 | } else {
124 | updateInfo += "|" + g.Release
125 | }
126 |
127 | if g.File == "" {
128 | updateInfo += "|*.AppImage"
129 | } else {
130 | updateInfo += "|" + g.File
131 | }
132 |
133 | return updateInfo
134 | }
135 |
136 | // Gets All The AppImage Files from a github release
137 | func getAppImageFilesFromRelease(release *github.RepositoryRelease) ([]utils.BinaryUrl) {
138 | var downloadLinks []utils.BinaryUrl // Contains Download Links
139 |
140 | for _, asset := range release.Assets {
141 | if strings.HasSuffix(strings.ToLower(*asset.Name), ".appimage") {
142 | downloadLinks = append(downloadLinks, utils.BinaryUrl{
143 | FileName: *asset.Name,
144 | Url: *asset.BrowserDownloadURL,
145 | })
146 | }
147 | }
148 |
149 | return downloadLinks
150 | }
151 |
152 | // Gets Release From A Particular Tag Name
153 | func getReleaseFromTagName(releases []*github.RepositoryRelease, tagName string) (*github.RepositoryRelease) {
154 | for _, release := range releases {
155 | if *release.Draft { continue }
156 | if tagName == *release.TagName {
157 | return release
158 | }
159 | }
160 | return nil
161 | }
162 |
--------------------------------------------------------------------------------
/src/helpers/utils/signature.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io"
5 | "os"
6 | "fmt"
7 | "bytes"
8 | "strings"
9 | "debug/elf"
10 | "crypto/sha1"
11 | "encoding/hex"
12 | "crypto/sha256"
13 | "github.com/ProtonMail/go-crypto/openpgp"
14 | )
15 |
16 | // Function to verify signature
17 | func VerifySignature(target string) (result *openpgp.Entity, err error) {
18 | key, err := readElfSection(target, ".sig_key")
19 | if err != nil {
20 | return nil, err
21 | }
22 |
23 | signature, err := readElfSection(target, ".sha256_sig")
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | file, err := newAppImagePreSignatureReader(target)
29 | if err != nil {
30 | return
31 | }
32 |
33 | sha256Hash := sha256.New()
34 | _, err = io.Copy(sha256Hash, file)
35 |
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | verification_target := hex.EncodeToString(sha256Hash.Sum(nil))
41 |
42 | keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key))
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | entity, err := openpgp.CheckArmoredDetachedSignature(keyring, strings.NewReader(verification_target), bytes.NewReader(signature), nil)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | return entity, nil
53 | }
54 |
55 | // Function which reads a particular section in the appimage (elf)
56 | func readElfSection(appImagePath string, sectionName string) ([]byte, error) {
57 | elfFile, err := elf.Open(appImagePath)
58 | if err != nil {
59 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error())
60 | }
61 |
62 | section := elfFile.Section(sectionName)
63 | if section == nil {
64 | return nil, fmt.Errorf("missing " + sectionName + " section on target elf")
65 | }
66 | sectionData, err := section.Data()
67 |
68 | if err != nil {
69 | return nil, fmt.Errorf("Unable to parse " + sectionName + " section: " + err.Error())
70 | }
71 |
72 | str_end := bytes.Index(sectionData, []byte("\000"))
73 | if str_end == -1 || str_end == 0 {
74 | return nil, nil
75 | }
76 |
77 | return sectionData[:str_end], nil
78 | }
79 |
80 | // Function which reads the appimage signature
81 | func ReadSignature(appImagePath string) ([]byte, error) {
82 | elfFile, err := elf.Open(appImagePath)
83 | if err != nil {
84 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error())
85 | }
86 |
87 | updInfo := elfFile.Section(".sha256_sig")
88 | if updInfo == nil {
89 | panic("Missing .sha256_sig section on target elf ")
90 | }
91 | sectionData, err := updInfo.Data()
92 |
93 | if err != nil {
94 | panic("Unable to parse .sha256_sig section: " + err.Error())
95 | }
96 |
97 | str_end := bytes.Index(sectionData, []byte("\000"))
98 | if str_end == -1 || str_end == 0 {
99 | return nil, fmt.Errorf("No update information found in: " + appImagePath)
100 | }
101 |
102 | return sectionData[:str_end], nil
103 | }
104 |
105 | type appImagePreSignatureReader struct {
106 | keySectionOffset uint64
107 | keySectionSize uint64
108 |
109 | sigSectionOffset uint64
110 | sigSectionSize uint64
111 |
112 | offset uint64
113 | file *os.File
114 | }
115 |
116 | func newAppImagePreSignatureReader(target string) (*appImagePreSignatureReader, error) {
117 | elfFile, err := elf.Open(target)
118 | if err != nil {
119 | return nil, err
120 | }
121 |
122 | key := elfFile.Section(".sig_key")
123 | if key == nil {
124 | return nil, fmt.Errorf("missing .sig_key section")
125 | }
126 |
127 | signature := elfFile.Section(".sha256_sig")
128 | if signature == nil {
129 | return nil, fmt.Errorf("missing .sha256_sig section")
130 | }
131 |
132 | file, err := os.Open(target)
133 | if err != nil {
134 | return nil, err
135 | }
136 |
137 | return &appImagePreSignatureReader{
138 | offset: 0,
139 | file: file,
140 | keySectionOffset: key.Offset,
141 | keySectionSize: key.Size,
142 | sigSectionOffset: signature.Offset,
143 | sigSectionSize: signature.Size,
144 | }, nil
145 | }
146 |
147 | func (reader *appImagePreSignatureReader) Read(p []byte) (n int, err error) {
148 | n, err = reader.file.Read(p)
149 | if err != nil {
150 | return
151 | }
152 |
153 | oldOffset := reader.offset
154 | reader.offset += uint64(n)
155 |
156 | if reader.keySectionOffset >= oldOffset && reader.keySectionOffset < reader.offset {
157 | start := reader.keySectionOffset - oldOffset
158 | for i := start; i < uint64(n) && (i-start) < reader.keySectionSize; i++ {
159 | p[i] = 0
160 | }
161 | }
162 |
163 | if reader.sigSectionOffset >= oldOffset && reader.sigSectionOffset < reader.offset {
164 | start := reader.sigSectionOffset - oldOffset
165 | for i := start; i < uint64(n) && (i-start) < reader.sigSectionSize; i++ {
166 | p[i] = 0
167 | }
168 | }
169 |
170 | return n, err
171 | }
172 |
173 | // Show signature of a given file
174 | func ShowSignature(filePath string) (error) {
175 | signingEntity, err := VerifySignature(filePath)
176 | if err != nil {
177 | return err
178 | }
179 | if signingEntity != nil {
180 | fmt.Println("AppImage signed by:")
181 | for _, v := range signingEntity.Identities {
182 | fmt.Println("\t", v.Name)
183 | }
184 | }
185 | return nil
186 | }
187 |
188 | // Get SHA1 Hash of a file
189 | func GetFileSHA1(filePath string) (string, error) {
190 | file, err := os.Open(filePath)
191 | if err != nil {
192 | return "", err
193 | }
194 |
195 | sha1Checksum := sha1.New()
196 | _, err = io.Copy(sha1Checksum, file)
197 | if err != nil {
198 | return "", err
199 | }
200 | return hex.EncodeToString(sha1Checksum.Sum(nil)), nil
201 | }
202 |
--------------------------------------------------------------------------------
/src/commands/update.go:
--------------------------------------------------------------------------------
1 | package commands
2 |
3 | import (
4 | "bread/src/helpers/repos"
5 | "bread/src/helpers/utils"
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | type UpdateCmd struct {
13 | Targets []string `arg:"" optional:"" name:"targets" help:"Update the target/all applications." type:"string"`
14 |
15 | Check bool `short:"c" help:"Only check for updates."`
16 | All bool `short:"a" help:"Update/check all applications."`
17 | NoPreRelease bool `short:"n" help:"Disable pre-releases." default:"false"`
18 | }
19 |
20 | // Function Which Will Be Executed When `update` is called.
21 | func (cmd *UpdateCmd) Run() (err error) {
22 | // Variable which will hold if any app was updated.
23 | var howManyUpdates int = 0
24 |
25 | if cmd.All { // if `update all`
26 | cmd.Targets, err = getAllTargets() // Load all the application info into targets
27 | if err != nil {
28 | return err
29 | }
30 | }
31 |
32 | if len(cmd.Targets) == 0 {
33 | if cmd.All {
34 | fmt.Println("No Application Installed")
35 | } else {
36 | fmt.Println("No Application Specified To Update")
37 | }
38 | return nil
39 | }
40 |
41 | fmt.Println("Checking For Updates")
42 | for _, target := range cmd.Targets {
43 | if len(strings.Split(target, "/")) < 2 {
44 | target = strings.ToLower(target + "/" + target)
45 | } else if len(strings.Split(target, "/")) == 2 {
46 | target = strings.ToLower(target)
47 | }
48 |
49 | entry, err := cmd.getRegistryEntry(target)
50 | if err != nil {
51 | continue
52 | }
53 |
54 | repo, err := repos.ParseTarget(target, "")
55 |
56 | if err != nil {
57 | return err
58 | }
59 |
60 | release, err := repo.GetLatestRelease(cmd.NoPreRelease)
61 | if err != nil {
62 | fmt.Println(target, "\U00002192", err)
63 | continue
64 | // return err
65 | }
66 |
67 | if release.Tag == entry.TagName {
68 | continue
69 | }
70 |
71 | if cmd.Check {
72 | fmt.Println(target, "\U00002192", release.Tag)
73 | howManyUpdates++
74 | continue
75 | }
76 |
77 | fmt.Println("Updating: " + target + "#" + entry.TagName + " \U00002192 " + target + "#" + release.Tag)
78 |
79 | var selectedBinary *utils.BinaryUrl;
80 | for fileIndex := range release.Files {
81 | if filepath.Base(entry.FilePath) == release.Files[fileIndex].FileName {
82 | selectedBinary = &release.Files[fileIndex]
83 | break
84 | }
85 | }
86 |
87 | if selectedBinary == nil {
88 | // Show A Prompt To Select A AppImage File.
89 | selectedBinary, err = utils.PromptBinarySelection(release.Files)
90 | if err != nil {
91 | return err
92 | }
93 | }
94 |
95 | // Make A FilePath Out Of The AppImage Name
96 | targetFilePath, err := utils.MakeTargetFilePath(selectedBinary)
97 | if err != nil {
98 | return err
99 | }
100 |
101 | // Download The AppImage
102 | err = repo.Download(selectedBinary, targetFilePath)
103 | if err != nil {
104 | return err
105 | }
106 |
107 | // Integrated The AppImage To Desktop
108 | err = utils.CreateDesktopIntegration(targetFilePath)
109 | if err != nil {
110 | os.Remove(targetFilePath)
111 | return err
112 | }
113 |
114 | registry, err := utils.OpenRegistry()
115 | if err != nil {
116 | return err
117 | }
118 |
119 | // De-Integrate old app from desktop
120 | err = utils.RemoveDesktopIntegration(entry.FilePath)
121 | if err != nil {
122 | os.Remove(targetFilePath) // If error, remove the newly downloaded appimage.
123 | return err
124 | } else {
125 | err = os.Remove(entry.FilePath) // Remove the old appimage
126 | if err != nil {
127 | fmt.Println("Cannot Remove The Old AppImage.\n", err.Error())
128 | }
129 | }
130 |
131 | registry.Remove(entry.FilePath) // Remove Old File From Registry
132 |
133 | sha1hash, _ := utils.GetFileSHA1(targetFilePath)
134 | appImageInfo, _ := utils.GetAppImageInfo(targetFilePath)
135 | err = registry.Add(utils.RegistryEntry{
136 | Repo: target,
137 | FilePath: targetFilePath,
138 | FileSha1: sha1hash,
139 | TagName: release.Tag,
140 | IsTerminalApp: appImageInfo.IsTerminalApp,
141 | AppImageType: appImageInfo.AppImageType,
142 | })
143 |
144 | if err != nil {
145 | return err
146 | }
147 |
148 | err = registry.Close()
149 | if err != nil {
150 | return err
151 | }
152 |
153 | // Print Signature Info If Exist.
154 | utils.ShowSignature(targetFilePath)
155 |
156 | // Remove the old file
157 | os.Remove(entry.FilePath)
158 |
159 | // utils.ShowSignature(result)
160 | fmt.Println("Updated: " + target)
161 | howManyUpdates++
162 | }
163 |
164 | if cmd.Check {
165 | if howManyUpdates == 0 {
166 | fmt.Println("No Updates Found!")
167 | } else {
168 | fmt.Println("Update Available For", howManyUpdates, "Application(s)")
169 | }
170 | } else {
171 | if howManyUpdates == 0 {
172 | fmt.Println("No Updates Found!")
173 | } else {
174 | fmt.Println("Updated", howManyUpdates, "Application(s)")
175 | }
176 | }
177 |
178 | return nil
179 | }
180 |
181 | // Get a application from registry
182 | func (cmd *UpdateCmd) getRegistryEntry(target string) (utils.RegistryEntry, error) {
183 | registry, err := utils.OpenRegistry()
184 | if err != nil {
185 | return utils.RegistryEntry{}, err
186 | }
187 | defer registry.Close()
188 |
189 | entry, _ := registry.Lookup(target)
190 |
191 | return entry, nil
192 | }
193 |
194 | // Get all the applications from the registry
195 | func getAllTargets() ([]string, error) {
196 | registry, err := utils.OpenRegistry()
197 | if err != nil {
198 | return nil, err
199 | }
200 | registry.Update()
201 |
202 | var repos []string
203 | for k := range registry.Entries {
204 | entry, _ := registry.Lookup(k)
205 | repos = append(repos, entry.Repo)
206 | }
207 |
208 | return repos, nil
209 | }
210 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Bread 
3 |
4 | Install, update, remove & run AppImage from GitHub using your CLI. (Fork of [AppImage ClI Tool](https://github.com/AppImageCrafters/appimage-cli-tool))
5 |
6 | ## Features
7 | - Install from the GitHub Releases
8 | - Automatically Integrate App To Desktop When Installing/Updating
9 | - Run Applications From Remote Without Installing Them
10 | - Update with ease
11 |
12 | ## Getting Started
13 |
14 | ### Installation
15 |
16 |
17 | Arch Linux & it's Derivatives
18 |
19 | you can use this step if your distribution does provide libappimage v1.0.0 or greater, which is the case on Arch Linux & it's Derivatives, kaOS, KDE Neon, Parabola Linux
20 | install libappimage dependency
21 | pacman -S libappimage
22 | then install bread
23 | sudo curl -L https://github.com/pegvin/bread/releases/download/v0.7.2/bread-0.7.2-x86_64 -o /usr/local/bin/bread && sudo chmod +x /usr/local/bin/bread
24 |
25 |
26 |
27 | Debian & it's Derivatives
28 |
29 | you can use this step if your distribution doesn't provide libappimage v1.0.0 or greater, which is the case on Debian & it's derivatives
30 | get the appimage containing libappimage v1.0.3
31 | sudo curl -L https://github.com/pegvin/bread/releases/download/v0.7.2/bread-0.7.2-x86_64.AppImage -o /usr/local/bin/bread && sudo chmod +x /usr/local/bin/bread
32 |
33 |
34 | ***Any version of libappimage will work with bread but it is recommended to use v1.0.0 or greater, You can also Refer to this [list](https://repology.org/project/libappimage/versions) to check what version of libappimage your Distribution provides.***
35 |
36 | ---
37 |
38 | ## Removal
39 |
40 | Just Remove the binary
41 | ```bash
42 | sudo rm -v /usr/local/bin/bread
43 | ```
44 |
45 | **NOTE** this won't delete the app you've installed.
46 |
47 | ---
48 |
49 | ## Usage
50 |
51 |
52 | NOTE
53 |
54 | Often there are many times when the GitHub user and repo both are same, for example libresprite, so in this case you can just specify single name like this bread install libresprite, this works with all the commands
55 |
56 |
57 |
58 | Install a application
59 |
60 | To install an Application from GitHub you can use the install command where user is the github repo owner and repo is the repository name
61 | bread install user/repo
62 | To install an application from a different Tag name you can specify the tag name too
63 | bread install user/repo tagname
64 |
65 |
66 |
67 | Run a application from remote
68 |
69 | If you want to run a application from remote without installing it you can use the run command
70 | bread run user/repo
71 | You can pass CLI arguments to the application too like this
72 | bread run user/repo -- --arg1 --arg2
73 | You can clear the download cache using clean command bread clean, Since all the applications you run from remote are cached so that it isn't downloaded everytime
74 |
75 |
76 |
77 | Remove a application
78 |
79 | you can remove a installed application using the remove command
80 | bread remove user/repo
81 |
82 |
83 |
84 | Update a applicationn
85 |
86 | You can update a application using the update command
87 | bread update user/repo
88 |
89 | if you just want to check if update is available you can use the --check flag
90 | bread update user/repo --check
91 |
92 | if you want to update all the applications you can use the --all flag
93 | bread update --all
94 |
95 | the --check & --all flag can be used together
96 | bread update --all --check
97 |
98 | the -n or --no-pre-release flag can be used to disable updates for pre-releases.
99 | bread update --no-pre-release
100 |
101 |
102 |
103 | Search for an application
104 |
105 | You can search for a application from the AppImage API
106 | bread search "Your search text"
107 |
108 |
109 |
110 | List all the installed application
111 |
112 | You can list all the installed applications using list command
113 | bread list
114 | If you also want to see the SHA1 Hashes of the applications listed, you can pass the -s or --show-sha1 flag
115 | bread list --show-sha1
116 | If you want to see the GitHub release tag name -t or --show-tag flag
117 | bread list --show-tag
118 |
119 |
120 | ---
121 |
122 | ### Bugs
123 | - Icons not showing in menus until there's a system reboot
124 | - Update Command Crashing
125 |
126 | ### Limits
127 | - Bread uses GitHub API to get information about a repository and it's release, but without authentication GitHub API limits the request per hour.
128 |
129 | ---
130 |
131 | ## Tested On:
132 | - Ubuntu 20.04 - by me
133 | - Debian 11 - by me
134 | - Manjaro Linux - by me
135 | - Arch Linux - by [my brother](https://github.com/idno34)
136 |
137 | ---
138 |
139 | ## File/Folder Layout
140 | Bread installs all the applications inside the `Applications` directory in your Linux Home Directory `~`, inside this directory there can be also a directory named `run-cache` which contains all the appimages you've run from the remote via the `bread run` command.
141 |
142 | In the `Applications` there is also a file named `.registry.json` which contains information related to the installed applications!
143 | In the `Applications` directory there is also a file named `.AppImageFeed.json` which is AppImage Catalog From [AppImage API](https://appimage.github.io/feed.json)
144 |
145 | ---
146 | ## Related:
147 | - [Zap - :zap: Delightful AppImage package manager ](https://github.com/srevinsaju/zap)
148 | - [A AppImage Manager Written in Shell](https://github.com/ivan-hc/AM-Application-Manager)
149 | - [The Original Tool Which Bread is Based On](https://github.com/AppImageCrafters/appimage-cli-tool)
150 |
151 | ---
152 |
153 | ## Building From Source
154 |
155 | Make Sure You Have Go version 1.18.x & [AppImage Builder](https://appimage-builder.readthedocs.io/en/latest/) Installed.
156 |
157 | Get The Repository Via Git:
158 |
159 | ```bash
160 | git clone https://github.com/pegvin/bread
161 | ```
162 |
163 | Go Inside The Source Code Directory & Get All The Dependencies:
164 |
165 | ```bash
166 | cd bread
167 | go mod tidy
168 | ```
169 |
170 | Make The Build Script Executable And Run It
171 |
172 | ```bash
173 | chmod +x ./make
174 | ./make --prod
175 | ```
176 |
177 | And To Build The AppImage Run
178 |
179 | ```bash
180 | ./make appimage
181 | ```
182 |
183 | ---
184 | ## Build Script
185 | The `make` bash script can build your go code, make appimage out of it, and clean the left over stuff including the genrated builds.
186 |
187 | #### Building in Development Mode
188 | This will build the go code into a binary inside the `build` folder
189 | ```
190 | ./make
191 | ```
192 |
193 | #### Building in Production Mode
194 | Building for production requires passing `--prod` flag which will enable some compiler options resulting in a small build size.
195 | ```
196 | ./make --prod
197 | ```
198 |
199 | #### Building the AppImage
200 | Bread requires libappimage0 for integrating your apps to desktop, which is done via libappimage, to make End user's life easier we package the libappimage with bread and that's why we build the binaries into AppImages so that user doesn't need to install anything.
201 |
202 | To make a appimage out the pre built binaries
203 | ```
204 | ./make appimage
205 | ```
206 |
207 | #### Get Dependency
208 | To install the dependencies require to build go binary
209 | ```
210 | ./make get-deps
211 | ```
212 |
213 | ---
214 |
215 | ## Todo
216 | - [ ] Switch To Some Other Language Since Go Module System is Shit
217 | - [ ] Improve UI
218 | - [x] Make AppImages Runnable From Remote Without Installing (Done in v0.3.6)
219 | - [x] Work On Reducing Binary Sizes (Reduced From 11.1MB to 3.1MB)
220 | - [ ] Add 32 Bit Builds (Currently not possible since [DL](https://github.com/rainycape/dl) dependency is not available for 32 bit machines)
221 | - [ ] Add Auto Updater Which Can Update The Bread Itself
222 | - [x] Add `--version` To Get The Version (Done in v0.2.2)
223 | - [x] Mirrors:
224 | - :heavy_multiplication_x: I Would Like To Introduce Concept Of Mirror Lists Which Contain The List Of AppImages With The Download URL, tho currently i am not working on it but in future i might.
225 | - [x] I am dropping this idea, tho i've added a search command which can search for appimages from a central server API
226 |
227 | ---
228 |
229 | # Thanks
230 |
--------------------------------------------------------------------------------