├── .github └── workflows │ ├── go-continuous.yml │ └── go.yml ├── .gitignore ├── .gitlab-ci.yml ├── LICENSE ├── Makefile ├── README.md ├── appimage-builder.yml ├── assets └── appimage-cli-tool.svg ├── cmd ├── commands │ ├── context.go │ ├── install │ │ ├── errors.go │ │ └── install.go │ ├── list.go │ ├── remove.go │ ├── search.go │ └── update │ │ ├── command.go │ │ └── update.go └── main.go ├── go.mod ├── go.sum └── internal ├── repos ├── appimagehub.go ├── errors.go ├── github.go └── repo.go └── utils ├── libappimage.go ├── registry.go ├── registry_test.go ├── signature.go └── util.go /.github/workflows/go-continuous.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Releases 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | build: 10 | name: Build 11 | runs-on: ubuntu-20.04 12 | steps: 13 | 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ^1.13 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | 23 | - name: Get dependencies 24 | run: | 25 | go get -v -t -d ./... 26 | if [ -f Gopkg.toml ]; then 27 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 28 | dep ensure 29 | fi 30 | 31 | - name: Build 32 | run: go build -o ./dist/appimage-cli-tool -v ./cmd 33 | 34 | - name: Update Release 35 | uses: johnwbyrd/update-release@v1.0.0 36 | with: 37 | # Your Github token; try \$\{\{ secrets.GITHUB_TOKEN \}\} if your build lasts less than an hour, or create your own secret token with repository access if your build requires longer than an hour. 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | # Paths to built files to be released. May be absolute or relative to \$\{\{ github.workspace \}\}. 40 | files: ./dist/appimage-cli-tool 41 | # The name of the release to be created. A reasonable looking release name will be created from the current \$\{\{ github.ref \}\} if this input is not supplied. 42 | release: continuous -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-20.04 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: go build -o ./dist/appimage-cli-tool -v ./cmd 35 | 36 | - uses: AppImageCrafters/build-appimage-action@master 37 | id: pack 38 | with: 39 | recipe: 'appimage-builder.yml' 40 | env: 41 | APPIMAGE_CLI_TOOL_VERSION: 0.1.4 42 | 43 | - name: Upload a Build Artifact 44 | uses: actions/upload-artifact@v2 45 | with: 46 | path: './*.AppImage' 47 | 48 | - name: Upload a Build Artifact 49 | uses: actions/upload-artifact@v2 50 | with: 51 | path: './dist/appimage-cli-tool' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # GoLand 18 | .idea/ 19 | bin/ 20 | build/ -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - pack 4 | - test 5 | 6 | build: 7 | image: golang 8 | stage: build 9 | script: 10 | - go get -v -t -d ./... 11 | - go build -o ./dist/appimage-cli-tool -v ./cmd 12 | - cp ./dist/appimage-cli-tool ./dist/appimage_cli_tool-$(git describe) 13 | artifacts: 14 | paths: 15 | - ./dist 16 | expire_in: 1 year 17 | 18 | distro-pack: 19 | image: appimagecrafters/appimage-builder 20 | stage: pack 21 | variables: 22 | DEBIAN_FRONTEND: noninteractive 23 | APPIMAGE_CLI_TOOL_VERSION: 0.1.4 24 | dependencies: 25 | - build 26 | before_script: 27 | - apt-get update -y 28 | - apt-get install -y checkinstall alien git 29 | script: 30 | # Pack into an AppImage 31 | - appimage-builder --skip-test --recipe=appimage-builder.yml 32 | # Pack AppImage into deb 33 | - checkinstall --install=no -y --type=debian --pkgname=appimage-cli-tool --pkgversion=${APPIMAGE_CLI_TOOL_VERSION} --backup=no --maintainer=contact@azubieta.net --pkglicense=MIT 34 | # Transform deb into rpm 35 | - alien --to-rpm appimage-cli-tool*.deb 36 | artifacts: 37 | paths: 38 | - "appimage-cli-tool*" 39 | expire_in: 1 year -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AppImage Crafters 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | get-deps: 2 | go get -v -t -d ./... 3 | build: 4 | go build -o ./dist/app -v ./app 5 | install: 6 | install ./app*.AppImage /usr/local/bin/app 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CLI AppImage Management Tool 2 | ============================ 3 | 4 | Search, install, update and remove AppImage from the comfort of your CLI. 5 | 6 | Features: 7 | - Search/Install from the appimagehub.com catalog 8 | - Install from github.com 9 | - Update using the appimage-update 10 | - Manage your local AppImage collection 11 | 12 | ## Installation 13 | 14 | ```shell script 15 | sudo wget https://github.com/AppImageCrafters/appimage-cli-tool/releases/latest/download/appimage-cli-tool -O /usr/local/bin/appimage-cli-tool; 16 | sudo chmod +x /usr/local/bin/appimage-cli-tool 17 | ``` 18 | 19 | ## Usage 20 | ```shell script 21 | Usage: appimage-cli-tool 22 | 23 | Flags: 24 | --help Show context-sensitive help. 25 | --debug Enable debug mode. 26 | 27 | Commands: 28 | search 29 | Search applications in the store. 30 | 31 | install 32 | Install an application. 33 | 34 | list 35 | List installed applications. 36 | 37 | remove 38 | Remove an application. 39 | 40 | update [ ...] 41 | Update an application. 42 | 43 | Run "app --help" for more information on a command. 44 | ``` 45 | -------------------------------------------------------------------------------- /appimage-builder.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | script: 3 | - rm AppDir -rf | true 4 | - mkdir -p AppDir/bin AppDir/usr/share/icons/hicolor/scalable/apps/ 5 | - cp ./dist/appimage-cli-tool AppDir/bin 6 | - cp assets/appimage-cli-tool.svg AppDir/usr/share/icons/hicolor/scalable/apps/ 7 | 8 | AppDir: 9 | path: ./AppDir 10 | 11 | app_info: 12 | id: org.appimage-crafters.appimage-cli-tool 13 | name: appimage-cli-tool 14 | icon: appimage-cli-tool 15 | version: !ENV ${APPIMAGE_CLI_TOOL_VERSION} 16 | exec: bin/appimage-cli-tool 17 | exec_args: $@ 18 | 19 | apt: 20 | arch: amd64 21 | sources: 22 | - sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ bionic main restricted universe multiverse' 23 | key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' 24 | - sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe multiverse' 25 | - sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse' 26 | - sourceline: 'deb http://archive.neon.kde.org/user bionic main' 27 | key_url: 'http://archive.neon.kde.org/public.key' 28 | 29 | include: 30 | - libappimage0 31 | exclude: 32 | - libpcre3 33 | - binutils 34 | - dpkg-dev 35 | - gir1.2-freedesktop 36 | - python3 37 | - libxrender1 38 | 39 | 40 | AppImage: 41 | update-information: 'gh-releases-zsync|AppImageCrafters|appimage-cli-tool|latest|appimage-cli-tool-*x86_64.AppImage.zsync' 42 | sign-key: None 43 | arch: x86_64 -------------------------------------------------------------------------------- /assets/appimage-cli-tool.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 45 | 47 | 51 | 55 | 56 | 66 | 68 | 72 | 76 | 77 | 87 | 88 | 108 | 117 | 126 | 130 | 134 | 138 | 145 | 149 | 153 | 157 | 161 | 165 | 169 | 170 | -------------------------------------------------------------------------------- /cmd/commands/context.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | type Context struct { 4 | Debug bool 5 | } 6 | -------------------------------------------------------------------------------- /cmd/commands/install/errors.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import "errors" 4 | 5 | var ApplicationInstalled = errors.New("the application is installed already") 6 | -------------------------------------------------------------------------------- /cmd/commands/install/install.go: -------------------------------------------------------------------------------- 1 | package install 2 | 3 | import ( 4 | "appimage-cli-tool/cmd/commands" 5 | "appimage-cli-tool/internal/repos" 6 | "appimage-cli-tool/internal/utils" 7 | 8 | "fmt" 9 | "os" 10 | ) 11 | 12 | type InstallCmd struct { 13 | Target string `arg name:"target" help:"Installation target." type:"string"` 14 | } 15 | 16 | func (cmd *InstallCmd) Run(*commands.Context) (err error) { 17 | if _, err := os.Stat(cmd.Target); err == nil { 18 | cmd.createDesktopIntegration(cmd.Target) 19 | return nil 20 | } 21 | 22 | repo, err := repos.ParseTarget(cmd.Target) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | release, err := repo.GetLatestRelease() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | selectedBinary, err := utils.PromptBinarySelection(release.Files) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | targetFilePath, err := utils.MakeTargetFilePath(selectedBinary) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if _, err = os.Stat(targetFilePath); err == nil { 43 | return ApplicationInstalled 44 | } 45 | 46 | err = repo.Download(selectedBinary, targetFilePath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | cmd.addToRegistry(targetFilePath, repo) 52 | 53 | cmd.createDesktopIntegration(targetFilePath) 54 | 55 | signingEntity, _ := utils.VerifySignature(targetFilePath) 56 | if signingEntity != nil { 57 | fmt.Println("AppImage signed by:") 58 | for _, v := range signingEntity.Identities { 59 | fmt.Println("\t", v.Name) 60 | } 61 | } 62 | 63 | return 64 | } 65 | 66 | func (cmd *InstallCmd) addToRegistry(targetFilePath string, repo repos.Repo) { 67 | sha1, _ := utils.GetFileSHA1(targetFilePath) 68 | updateInfo, _ := utils.ReadUpdateInfo(targetFilePath) 69 | if updateInfo == "" { 70 | updateInfo = repo.FallBackUpdateInfo() 71 | } 72 | 73 | entry := utils.RegistryEntry{ 74 | FilePath: targetFilePath, 75 | Repo: repo.Id(), 76 | FileSha1: sha1, 77 | UpdateInfo: updateInfo, 78 | } 79 | 80 | registry, _ := utils.OpenRegistry() 81 | if registry != nil { 82 | _ = registry.Add(entry) 83 | _ = registry.Close() 84 | } 85 | } 86 | 87 | func (cmd *InstallCmd) createDesktopIntegration(targetFilePath string) { 88 | libAppImage, err := utils.NewLibAppImageBindings() 89 | if err != nil { 90 | fmt.Println("Integration failed: missing libappimage.so") 91 | return 92 | } 93 | 94 | fmt.Println("Creating menu entry and mime-types integrations") 95 | err = libAppImage.Register(targetFilePath) 96 | if err != nil { 97 | fmt.Println("Integration failed: " + err.Error()) 98 | } else { 99 | fmt.Println("Integration completed") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/commands/list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "appimage-cli-tool/internal/utils" 5 | "bytes" 6 | "github.com/juju/ansiterm" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | ) 11 | 12 | type ListCmd struct { 13 | } 14 | 15 | func (r *ListCmd) Run(*Context) error { 16 | registry, err := utils.OpenRegistry() 17 | if err != nil { 18 | return err 19 | } 20 | defer registry.Close() 21 | 22 | registry.Update() 23 | var buf bytes.Buffer 24 | tabWriter := ansiterm.NewTabWriter(&buf, 20, 4, 0, ' ', 0) 25 | tabWriter.SetColorCapable(true) 26 | 27 | tabWriter.SetForeground(ansiterm.Green) 28 | _, _ = tabWriter.Write([]byte("Repo\t File Name\t SHA1\n")) 29 | _, _ = tabWriter.Write([]byte("----\t ---------\t ----\n")) 30 | 31 | tabWriter.SetForeground(ansiterm.DarkGray) 32 | 33 | var lines [][]string 34 | for fileName, v := range registry.Entries { 35 | line := []string{v.Repo, filepath.Base(fileName), v.FileSha1} 36 | lines = append(lines, line) 37 | } 38 | sort.Slice(lines, func(i int, j int) bool { 39 | return lines[i][1] < lines[j][1] 40 | }) 41 | 42 | for _, line := range lines { 43 | _, _ = tabWriter.Write([]byte(line[0] + "\t " + line[1] + "\t " + line[2] + "\n")) 44 | } 45 | _ = tabWriter.Flush() 46 | _, _ = os.Stdout.Write(buf.Bytes()) 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /cmd/commands/remove.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "appimage-cli-tool/internal/utils" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | type RemoveCmd struct { 10 | Target string `arg name:"id" help:"Installation id or file name." type:"string"` 11 | KeepFile bool `help:"Remove only the application desktop entry."` 12 | } 13 | 14 | func (cmd *RemoveCmd) Run(*Context) (err error) { 15 | registry, err := utils.OpenRegistry() 16 | if err != nil { 17 | return err 18 | } 19 | defer registry.Close() 20 | 21 | registry.Update() 22 | 23 | entry, ok := registry.Lookup(cmd.Target) 24 | if !ok { 25 | return fmt.Errorf("application not found \"" + cmd.Target + "\"") 26 | } 27 | 28 | err = removeDesktopIntegration(entry.FilePath) 29 | if err != nil { 30 | fmt.Println("Desktop integration removal failed: " + err.Error()) 31 | } 32 | 33 | if cmd.KeepFile { 34 | return nil 35 | } 36 | 37 | err = os.Remove(entry.FilePath) 38 | if err != nil { 39 | return fmt.Errorf("Unable to remove AppImage file: " + err.Error()) 40 | } 41 | fmt.Println("Application removed: " + entry.FilePath) 42 | registry.Remove(entry.FilePath) 43 | return err 44 | } 45 | 46 | func removeDesktopIntegration(filePath string) error { 47 | libAppImage, err := utils.NewLibAppImageBindings() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if libAppImage.ShallNotBeIntegrated(filePath) { 53 | return nil 54 | } 55 | 56 | return libAppImage.Unregister(filePath) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/commands/search.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "appimage-cli-tool/internal/utils" 5 | "bytes" 6 | "fmt" 7 | "github.com/juju/ansiterm" 8 | "github.com/tidwall/gjson" 9 | "os" 10 | ) 11 | 12 | type SearchCmd struct { 13 | Query string `arg name:"query" help:"Query be used in the search." type:"string"` 14 | } 15 | 16 | func (r *SearchCmd) Run(*Context) error { 17 | jsonData, err := utils.QueryUrl("https://www.pling.com/json/search/p/" + r.Query + "/s/AppImageHub.com") 18 | if err != nil { 19 | return err 20 | } 21 | 22 | jsonParser := gjson.Parse(jsonData.String()) 23 | 24 | products := jsonParser.Get(`#(title="Products").values`) 25 | 26 | var buf bytes.Buffer 27 | tabWriter := ansiterm.NewTabWriter(&buf, 20, 4, 0, ' ', 0) 28 | tabWriter.SetColorCapable(true) 29 | 30 | tabWriter.SetForeground(ansiterm.Green) 31 | _, _ = tabWriter.Write([]byte("Target\t FileName\t Category\t Publisher\n")) 32 | _, _ = tabWriter.Write([]byte("--\t ----\t --------\t ---------\n")) 33 | 34 | tabWriter.SetForeground(ansiterm.DarkGray) 35 | 36 | for _, product := range products.Array() { 37 | id := product.Get(`project_id`).String() 38 | title := product.Get(`title`).String() 39 | category := product.Get(`cat_title`).String() 40 | username := product.Get(`username`).String() 41 | 42 | fmt.Fprintf(tabWriter, "appimagehub:%s\t %s\t %s\t %s\n", id, title, category, username) 43 | } 44 | 45 | _ = tabWriter.Flush() 46 | _, _ = os.Stdout.Write(buf.Bytes()) 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /cmd/commands/update/command.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "appimage-cli-tool/cmd/commands" 8 | "appimage-cli-tool/internal/utils" 9 | ) 10 | 11 | type UpdateCmd struct { 12 | Targets []string `arg optional name:"targets" help:"Updates the target applications." type:"string"` 13 | 14 | Check bool `help:"Only check for updates."` 15 | All bool `help:"Update all applications."` 16 | } 17 | 18 | var NoUpdateInfo = errors.New("there is no update information") 19 | 20 | func (cmd *UpdateCmd) Run(*commands.Context) (err error) { 21 | if cmd.All { 22 | cmd.Targets, err = getAllTargets() 23 | if err != nil { 24 | return err 25 | } 26 | } 27 | 28 | for _, target := range cmd.Targets { 29 | entry, err := cmd.getRegistryEntry(target) 30 | if err != nil { 31 | continue 32 | } 33 | 34 | updateMethod, err := NewUpdater(entry.UpdateInfo, entry.FilePath) 35 | if err != nil { 36 | println(err.Error()) 37 | continue 38 | } 39 | 40 | fmt.Println("Looking for updates of: ", entry.FilePath) 41 | updateAvailable, err := updateMethod.Lookup() 42 | if err != nil { 43 | println(err.Error()) 44 | continue 45 | } 46 | 47 | if !updateAvailable { 48 | fmt.Println("No updates were found for: ", entry.FilePath) 49 | continue 50 | } 51 | 52 | if cmd.Check { 53 | fmt.Println("Update available for: ", entry.FilePath) 54 | continue 55 | } 56 | 57 | result, err := updateMethod.Download() 58 | if err != nil { 59 | println(err.Error()) 60 | continue 61 | } 62 | 63 | signingEntity, _ := utils.VerifySignature(result) 64 | if signingEntity != nil { 65 | fmt.Println("AppImage signed by:") 66 | for _, v := range signingEntity.Identities { 67 | fmt.Println("\t", v.Name) 68 | } 69 | } 70 | 71 | fmt.Println("Update downloaded to: " + result) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func (cmd *UpdateCmd) getRegistryEntry(target string) (utils.RegistryEntry, error) { 78 | registry, err := utils.OpenRegistry() 79 | if err != nil { 80 | return utils.RegistryEntry{}, err 81 | } 82 | defer registry.Close() 83 | 84 | entry, _ := registry.Lookup(target) 85 | 86 | if entry.UpdateInfo == "" { 87 | entry.UpdateInfo, _ = utils.ReadUpdateInfo(target) 88 | entry.FilePath = target 89 | } 90 | 91 | if entry.UpdateInfo == "" { 92 | return entry, NoUpdateInfo 93 | } else { 94 | return entry, nil 95 | } 96 | } 97 | 98 | func getAllTargets() ([]string, error) { 99 | registry, err := utils.OpenRegistry() 100 | if err != nil { 101 | return nil, err 102 | } 103 | registry.Update() 104 | 105 | var paths []string 106 | for k := range registry.Entries { 107 | paths = append(paths, k) 108 | } 109 | 110 | return paths, nil 111 | } 112 | -------------------------------------------------------------------------------- /cmd/commands/update/update.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/AppImageCrafters/appimage-update" 8 | "github.com/AppImageCrafters/appimage-update/updaters" 9 | ) 10 | 11 | func NewUpdater(updateInfoString string, appImagePath string) (update.Updater, error) { 12 | if strings.HasPrefix(updateInfoString, "zsync") { 13 | return updaters.NewZSyncUpdater(&updateInfoString, appImagePath) 14 | } 15 | 16 | if strings.HasPrefix(updateInfoString, "gh-releases-zsync") { 17 | return updaters.NewGitHubZsyncUpdater(&updateInfoString, appImagePath) 18 | } 19 | 20 | if strings.HasPrefix(updateInfoString, "gh-releases-direct") { 21 | return updaters.NewGitHubDirectUpdater(&updateInfoString, appImagePath) 22 | } 23 | 24 | if strings.HasPrefix(updateInfoString, "ocs-v1-appimagehub-direct") { 25 | return updaters.NewOCSAppImageHubDirect(&updateInfoString, appImagePath) 26 | } 27 | 28 | if strings.HasPrefix(updateInfoString, "ocs-v1-appimagehub-zsync") { 29 | return updaters.NewOCSAppImageHubZSync(&updateInfoString, appImagePath) 30 | } 31 | 32 | return nil, fmt.Errorf("Invalid updated information: ", updateInfoString) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "appimage-cli-tool/cmd/commands" 5 | "appimage-cli-tool/cmd/commands/install" 6 | "appimage-cli-tool/cmd/commands/update" 7 | "github.com/alecthomas/kong" 8 | ) 9 | 10 | var cli struct { 11 | Debug bool `help:"Enable debug mode."` 12 | 13 | Search commands.SearchCmd `cmd help:"Search applications in the store."` 14 | Install install.InstallCmd `cmd help:"Install an application."` 15 | List commands.ListCmd `cmd help:"List installed applications."` 16 | Remove commands.RemoveCmd `cmd help:"Remove an application."` 17 | Update update.UpdateCmd `cmd help:"Update an application."` 18 | } 19 | 20 | func main() { 21 | ctx := kong.Parse(&cli) 22 | // Call the Run() method of the selected parsed command. 23 | err := ctx.Run(&commands.Context{Debug: cli.Debug}) 24 | ctx.FatalIfErrorf(err) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module appimage-cli-tool 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/AppImageCrafters/appimage-update v0.1.5-0.20200727182203-945dfa160174 7 | github.com/alecthomas/kong v0.2.11 8 | github.com/antchfx/xmlquery v1.2.4 9 | github.com/google/go-github/v31 v31.0.0 10 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a 11 | github.com/manifoldco/promptui v0.7.0 12 | github.com/rainycape/dl v0.0.0-20151222075243-1b01514224a1 13 | github.com/schollz/progressbar/v3 v3.3.4 14 | github.com/stretchr/testify v1.6.1 15 | github.com/tidwall/gjson v1.6.0 16 | golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | 4d63.com/gochecknoinits v0.0.0-20200108094044-eb73b47b9fc4/go.mod h1:4o1i5aXtIF5tJFt3UD1knCVmWOXg7fLYdHVu6jeNcnM= 2 | github.com/AppImageCrafters/appimage-update v0.1.0 h1:PxgLaeRDIKTZ295Dc+9NRhIBk3FuAOjbm/gfZ6rCnQI= 3 | github.com/AppImageCrafters/appimage-update v0.1.1 h1:N9WMpsXR+1O1MhVxpZaCQi3jUXXEbyTNucWs9U5Wz2k= 4 | github.com/AppImageCrafters/appimage-update v0.1.1/go.mod h1:4STzwWhTmxLp5M1DrTRz92U9+ojZk26HwAqe2agGP0s= 5 | github.com/AppImageCrafters/appimage-update v0.1.2 h1:gNsJVVlBkdoJdWDsdyWPsPHWhR6k2EErFYyZp62Vzvw= 6 | github.com/AppImageCrafters/appimage-update v0.1.2/go.mod h1:PxSWD/Ci1Vntq5AVWym9Jwsn0fYIb1eNOzMUT0OKsd0= 7 | github.com/AppImageCrafters/appimage-update v0.1.3 h1:ywbn2s5znyGXuKgvOUmiAARArMyF1KOoUKr6UmXkL2M= 8 | github.com/AppImageCrafters/appimage-update v0.1.3/go.mod h1:ve6bfuz21ExlTbk/1Ohy1HpfVDaW4z74bQbosihVvKQ= 9 | github.com/AppImageCrafters/appimage-update v0.1.4 h1:fcksVo4uJrV5PES+WjUoc6Q3ascBKWiAZiSOIoX041Q= 10 | github.com/AppImageCrafters/appimage-update v0.1.4/go.mod h1:jmGN6RN8P4RuRwwhDekFO89ZcrfnHL3pt9ripb1/jkQ= 11 | github.com/AppImageCrafters/appimage-update v0.1.5-0.20200727182203-945dfa160174 h1:/FWPNjJ1ZBZ7id7DGVW76sQgX4sK4ZUMcnAl3ZMLKHA= 12 | github.com/AppImageCrafters/appimage-update v0.1.5-0.20200727182203-945dfa160174/go.mod h1:XXaKmAhb90XMaZi33Nb7eP2nun98qRuJ02ufiM2rx7w= 13 | github.com/AppImageCrafters/appimage-update v0.1.5 h1:0+bSRVF0sfKh9ofdqwdYogNODDQMfEsn1/gg32A0A40= 14 | github.com/AppImageCrafters/appimage-update v0.1.5/go.mod h1:jmGN6RN8P4RuRwwhDekFO89ZcrfnHL3pt9ripb1/jkQ= 15 | github.com/AppImageCrafters/libzsync-go v0.1.5 h1:4ZXW7TSU6tmCy4gmugDDFW6aFmV/mkooHq8UkjcFyfY= 16 | github.com/AppImageCrafters/libzsync-go v0.1.5/go.mod h1:lF+WsCvezwqAfTa/icnXvmGsCPeq/zDpFGSEE/cw/AQ= 17 | github.com/AppImageCrafters/zsync v0.1.0 h1:4dkA2pAXZUuWUNryaqcO6vO3oGDz7jIWhleM9CZ6wKg= 18 | github.com/AppImageCrafters/zsync v0.1.0/go.mod h1:BTERniiRlHxk1jlUQkMZHU8OyQMWSbl/DJFrqvd3uaU= 19 | github.com/AppImageCrafters/zsync v0.1.1 h1:uhHcq81ExzEIl/FEuvFqfQ13jnERsQQaBp3KJzAOm+U= 20 | github.com/AppImageCrafters/zsync v0.1.1/go.mod h1:BTERniiRlHxk1jlUQkMZHU8OyQMWSbl/DJFrqvd3uaU= 21 | github.com/AppImageCrafters/zsync v0.1.4 h1:auVtVzj+3qII5WXBVPIuTq7YIHCmr9vlWJJEvkVloaA= 22 | github.com/AppImageCrafters/zsync v0.1.4/go.mod h1:GlIuJzFA+iid38KFqzSka8TVDzTLW0u2w4NzvPmeYg0= 23 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 24 | github.com/alecthomas/kong v0.2.9 h1:WGuTS/N2/NQ/9LymVqpr1ifZ4EEkQPvwFHqZs6ak5IU= 25 | github.com/alecthomas/kong v0.2.9/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 26 | github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= 27 | github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 28 | github.com/antchfx/xmlquery v1.2.4 h1:T/SH1bYdzdjTMoz2RgsfVKbM5uWh3gjDYYepFqQmFv4= 29 | github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM= 30 | github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0= 31 | github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= 32 | github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= 33 | github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= 34 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 35 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 36 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 37 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 38 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 39 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 40 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 41 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 42 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 43 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 45 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/diskfs/go-diskfs v1.0.1-0.20200223082018-e1e7ed3e8059 h1:cTbkIihDv30GGFQ/TFFfwxXSnDC01b+dP5XwPR0+81A= 47 | github.com/diskfs/go-diskfs v1.0.1-0.20200223082018-e1e7ed3e8059/go.mod h1:l5/91JbSThbOvz9ns03j6OKpc1t4gQXb+0c8ZtV5Kzs= 48 | github.com/diskfs/go-diskfs v1.1.1 h1:rMjLpaydtXGVZb7mdkRGK1+//30i76nKAit89zUzeaI= 49 | github.com/diskfs/go-diskfs v1.1.1/go.mod h1:afUPxxu+x1snp4aCY2bKR0CoZ/YFJewV3X2UEr2nPZE= 50 | github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8= 51 | github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 52 | github.com/glycerine/rbuf v0.0.0-20190314090850-75b78581bebe h1:S7HF/JKUdDrsd66htKdBOt/t3WvhU3l8EXe0U3WxEDA= 53 | github.com/glycerine/rbuf v0.0.0-20190314090850-75b78581bebe/go.mod h1:BOGkN1CszB3i4g9xn96RH4t5uXnxJjnC5/RWJ1Wx7GM= 54 | github.com/golang/dl v0.0.0-20200601221412-a954fa24b3e5 h1:yKUHMhCLATw18FJiv9NN7Edh/Yq4XN8fPmRg7aq5/Qw= 55 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 56 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 57 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 59 | github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= 60 | github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= 61 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 62 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 63 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 64 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 65 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 66 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= 67 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 68 | github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= 69 | github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb/go.mod h1:82TxjOpWQiPmywlbIaB2ZkqJoSYJdLGPgAJDvM3PbKc= 70 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s= 71 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 72 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 73 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 74 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 75 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 76 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 77 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 78 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 79 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 80 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 81 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 82 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 83 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 84 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 85 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 86 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 87 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 88 | github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= 89 | github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= 90 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 91 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 92 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 93 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 94 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 95 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 96 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 97 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 98 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 99 | github.com/mibk/dupl v1.0.0/go.mod h1:pCr4pNxxIbFGvtyCOi0c7LVjmV6duhKWV+ex5vh38ME= 100 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 101 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 102 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 103 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 104 | github.com/petar/GoLLRB v0.0.0-20190514000832-33fb24c13b99/go.mod h1:HUpKUBZnpzkdx0kD/+Yfuft+uD3zHGtXF/XJB14TUr4= 105 | github.com/pierrec/lz4 v2.3.0+incompatible h1:CZzRn4Ut9GbUkHlQ7jqBXeZQV41ZSKWFc302ZU6lUTk= 106 | github.com/pierrec/lz4 v2.3.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 107 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 108 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 109 | github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw= 110 | github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= 111 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 112 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 113 | github.com/rainycape/dl v0.0.0-20151222075243-1b01514224a1 h1:XZlja+DeIOJeEPWAfM9M5wko0keB9qgRfuuWbry6VBQ= 114 | github.com/rainycape/dl v0.0.0-20151222075243-1b01514224a1/go.mod h1:lh74SQgfeEuNq74dKWzDLuVB+/ntX5c4g1oDyL4GkGg= 115 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 116 | github.com/schollz/progressbar v1.0.0 h1:gbyFReLHDkZo8mxy/dLWMr+Mpb1MokGJ1FqCiqacjZM= 117 | github.com/schollz/progressbar/v3 v3.3.3 h1:woop83iT9IwNMhawXBgHTlAAOwUj4Nnr1RvX2LkkJTs= 118 | github.com/schollz/progressbar/v3 v3.3.3/go.mod h1:N/820QRS3ua9DhrVnLShsNgAEKNYFd89Cf5syXfqeyQ= 119 | github.com/schollz/progressbar/v3 v3.3.4 h1:nMinx+JaEm/zJz4cEyClQeAw5rsYSB5th3xv+5lV6Vg= 120 | github.com/schollz/progressbar/v3 v3.3.4/go.mod h1:Rp5lZwpgtYmlvmGo1FyDwXMqagyRBQYSDwzlP9QDu84= 121 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 122 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 123 | github.com/smallnest/ringbuffer v0.0.0-20200331140504-3a38e8060b89/go.mod h1:wIpUJ8WEUx959cAgIwpDuHUcE7aexxxYNcvXUT08L90= 124 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 125 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 126 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 127 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 128 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 129 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 130 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 131 | github.com/stripe/safesql v0.2.0/go.mod h1:q7b2n0JmzM1mVGfcYpanfVb2j23cXZeWFxcILPn3JV4= 132 | github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= 133 | github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 134 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= 135 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 136 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 137 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 138 | github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= 139 | github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= 140 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 141 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 142 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 143 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 144 | golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= 145 | golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 146 | golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= 147 | golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 148 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 149 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 150 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 151 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 152 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 153 | golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI= 154 | golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 155 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 156 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 158 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 159 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 160 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 163 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 164 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 166 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= 168 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= 170 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 172 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 173 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 174 | golang.org/x/tools v0.0.0-20200102200121-6de373a2766c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 175 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 177 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 179 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 181 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 182 | gopkg.in/djherbis/times.v1 v1.2.0 h1:UCvDKl1L/fmBygl2Y7hubXCnY7t4Yj46ZrBFNUipFbM= 183 | gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= 184 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 185 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8 h1:jL/vaozO53FMfZLySWM+4nulF3gQEC6q5jH90LPomDo= 187 | gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 188 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 189 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 190 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 191 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= 192 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= 193 | -------------------------------------------------------------------------------- /internal/repos/appimagehub.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "appimage-cli-tool/internal/utils" 5 | "github.com/antchfx/xmlquery" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type AppImageHubRepo struct { 11 | ContentId string 12 | } 13 | 14 | func NewAppImageHubRepo(target string) (Repo, error) { 15 | if strings.HasPrefix(target, "https://www.appimagehub.com/p/") { 16 | target = strings.Replace(target, "https://www.appimagehub.com/p/", "appimagehub:", 1) 17 | } 18 | 19 | if !strings.HasPrefix(target, "appimagehub:") { 20 | return nil, InvalidTargetFormat 21 | } 22 | 23 | repo := &AppImageHubRepo{} 24 | repo.ContentId = target[12:] 25 | 26 | return repo, nil 27 | } 28 | 29 | func (a AppImageHubRepo) Id() string { 30 | return "appimagehub:" + a.ContentId 31 | } 32 | 33 | func (a AppImageHubRepo) GetLatestRelease() (*Release, error) { 34 | doc, err := xmlquery.LoadURL("https://www.appimagehub.com/ocs/v1/content/data/" + a.ContentId) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | var downloadLinks []utils.BinaryUrl 40 | for i := 1; i < 100; i++ { 41 | idx := strconv.Itoa(i) 42 | link, err := xmlquery.Query(doc, "//ocs/data/content/downloadlink"+idx+"/text()") 43 | if err != nil { 44 | return nil, err 45 | } 46 | name, err := xmlquery.Query(doc, "//ocs/data/content/downloadname"+idx+"/text()") 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if link == nil { 52 | break 53 | } 54 | 55 | downloadLink := utils.BinaryUrl{ 56 | FileName: name.Data, 57 | Url: link.Data, 58 | } 59 | 60 | if strings.HasSuffix(downloadLink.FileName, ".AppImage") || 61 | strings.HasSuffix(downloadLink.FileName, ".appimage") { 62 | downloadLinks = append(downloadLinks, downloadLink) 63 | } 64 | } 65 | 66 | if len(downloadLinks) > 0 { 67 | return &Release{ 68 | "latest", 69 | downloadLinks, 70 | }, nil 71 | } else { 72 | return nil, NoAppImageBinariesFound 73 | } 74 | } 75 | 76 | func (a AppImageHubRepo) Download(binaryUrl *utils.BinaryUrl, targetPath string) (err error) { 77 | err = utils.DownloadAppImage(binaryUrl.Url, targetPath) 78 | return 79 | } 80 | 81 | func (a AppImageHubRepo) FallBackUpdateInfo() string { 82 | return "ocs-v1-appimagehub-zsync|www.appimagehub.com/ocs/v1|" + a.ContentId + "|*.AppImage" 83 | } 84 | -------------------------------------------------------------------------------- /internal/repos/errors.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import "errors" 4 | 5 | var InvalidTargetFormat = errors.New("invalid target format") 6 | 7 | var NoAppImageBinariesFound = errors.New("no AppImage found in releases") 8 | -------------------------------------------------------------------------------- /internal/repos/github.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "appimage-cli-tool/internal/utils" 5 | "context" 6 | "strings" 7 | "github.com/google/go-github/v31/github" 8 | ) 9 | 10 | type GitHubRepo struct { 11 | User string 12 | Project string 13 | Release string 14 | File string 15 | } 16 | 17 | func NewGitHubRepo(target string) (repo Repo, err error) { 18 | if strings.HasPrefix(target, "https://github.com/") { 19 | target = strings.Replace(target, "https://github.com/", "github:", 1) 20 | } 21 | 22 | if !strings.HasPrefix(target, "github:") { 23 | return repo, InvalidTargetFormat 24 | } 25 | 26 | repo = &GitHubRepo{} 27 | 28 | targetParts := strings.Split(target[7:], "/") 29 | targetPartsLen := len(targetParts) 30 | if targetPartsLen < 2 { 31 | return repo, InvalidTargetFormat 32 | } 33 | 34 | ghSource := GitHubRepo{ 35 | User: targetParts[0], 36 | Project: targetParts[1], 37 | } 38 | 39 | if targetPartsLen > 2 { 40 | ghSource.Release = targetParts[2] 41 | } 42 | 43 | if targetPartsLen > 3 { 44 | ghSource.File = targetParts[3] 45 | } 46 | 47 | return &ghSource, nil 48 | } 49 | 50 | func (g GitHubRepo) Id() string { 51 | id := "github:" + g.User + "/" + g.Project 52 | 53 | return id 54 | } 55 | 56 | func (g GitHubRepo) GetLatestRelease() (*Release, error) { 57 | var downloadLinks []utils.BinaryUrl 58 | 59 | client := github.NewClient(nil) 60 | releases, _, err := client.Repositories.ListReleases(context.Background(), g.User, g.Project, nil) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | for _, release := range releases { 66 | if *release.Draft == true { 67 | continue 68 | } 69 | 70 | for _, asset := range release.Assets { 71 | if strings.HasSuffix(*asset.Name, ".AppImage") { 72 | downloadLinks = append(downloadLinks, utils.BinaryUrl{ 73 | FileName: *asset.Name, 74 | Url: *asset.BrowserDownloadURL, 75 | }) 76 | } 77 | } 78 | 79 | if len(downloadLinks) > 0 { 80 | return &Release{ 81 | *release.TagName, 82 | downloadLinks, 83 | }, nil 84 | } 85 | } 86 | 87 | return nil, NoAppImageBinariesFound 88 | } 89 | 90 | func (g GitHubRepo) Download(binaryUrl *utils.BinaryUrl, targetPath string) (err error) { 91 | err = utils.DownloadAppImage(binaryUrl.Url, targetPath) 92 | return 93 | } 94 | 95 | func (g GitHubRepo) FallBackUpdateInfo() string { 96 | updateInfo := "gh-releases-direct|" + g.User + "|" + g.Project 97 | if g.Release == "" { 98 | updateInfo += "|latest" 99 | } else { 100 | updateInfo += "|" + g.Release 101 | } 102 | 103 | if g.File == "" { 104 | updateInfo += "|*.AppImage" 105 | } else { 106 | updateInfo += "|" + g.File 107 | } 108 | 109 | return updateInfo 110 | } 111 | -------------------------------------------------------------------------------- /internal/repos/repo.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "appimage-cli-tool/internal/utils" 5 | ) 6 | 7 | type Release struct { 8 | Tag string 9 | Files []utils.BinaryUrl 10 | } 11 | 12 | type Repo interface { 13 | Id() string 14 | GetLatestRelease() (*Release, error) 15 | Download(binaryUrl *utils.BinaryUrl, targetPath string) error 16 | FallBackUpdateInfo() string 17 | } 18 | 19 | func ParseTarget(target string) (Repo, error) { 20 | repo, err := NewGitHubRepo(target) 21 | if err == nil { 22 | return repo, nil 23 | } 24 | 25 | repo, err = NewAppImageHubRepo(target) 26 | if err == nil { 27 | return repo, nil 28 | } 29 | 30 | return nil, InvalidTargetFormat 31 | } 32 | -------------------------------------------------------------------------------- /internal/utils/libappimage.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // #include 4 | // #include 5 | import "C" 6 | 7 | import ( 8 | "fmt" 9 | "github.com/rainycape/dl" 10 | ) 11 | 12 | type libAppImageBind struct { 13 | lib *dl.DL 14 | 15 | appimage_shall_not_be_integrated func(path *C.char) int 16 | appimage_register_in_system func(path *C.char, verbose int) int 17 | appimage_unregister_in_system func(path *C.char, verbose int) int 18 | } 19 | 20 | type LibAppImage interface { 21 | Register(filePath string) error 22 | Unregister(filePath string) error 23 | ShallNotBeIntegrated(filePath string) bool 24 | Close() 25 | } 26 | 27 | func loadLibAppImage() (*dl.DL, error) { 28 | lib, err := dl.Open("libappimage.so.1.0", 0) 29 | if err == nil { 30 | return lib, nil 31 | } 32 | 33 | lib, err = dl.Open("libappimage.so", 0) 34 | if err == nil { 35 | return lib, nil 36 | } 37 | 38 | return nil, fmt.Errorf("libappimage not found, desktop integration is disabled") 39 | } 40 | 41 | func NewLibAppImageBindings() (LibAppImage, error) { 42 | bindings := libAppImageBind{} 43 | var err error 44 | bindings.lib, err = loadLibAppImage() 45 | 46 | err = bindings.lib.Sym("appimage_shall_not_be_integrated", &bindings.appimage_shall_not_be_integrated) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | err = bindings.lib.Sym("appimage_unregister_in_system", &bindings.appimage_unregister_in_system) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | err = bindings.lib.Sym("appimage_register_in_system", &bindings.appimage_register_in_system) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return &bindings, nil 62 | } 63 | 64 | func (bind *libAppImageBind) Register(filePath string) error { 65 | if bind.appimage_register_in_system(C.CString(filePath), 1) != 0 { 66 | return fmt.Errorf("unregister failed") 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (bind *libAppImageBind) ShallNotBeIntegrated(filePath string) bool { 73 | return bind.appimage_shall_not_be_integrated(C.CString(filePath)) != 0 74 | } 75 | 76 | func (bind *libAppImageBind) Unregister(filePath string) error { 77 | if bind.appimage_unregister_in_system(C.CString(filePath), 1) != 0 { 78 | return fmt.Errorf("unregister failed") 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (bind *libAppImageBind) Close() { 85 | _ = bind.lib.Close() 86 | } 87 | -------------------------------------------------------------------------------- /internal/utils/registry.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | updateUtils "github.com/AppImageCrafters/appimage-update/util" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 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 | } 21 | 22 | type Registry struct { 23 | Entries map[string]RegistryEntry 24 | } 25 | 26 | func OpenRegistry() (registry *Registry, err error) { 27 | path, err := makeRegistryFilePath() 28 | if err != nil { 29 | return 30 | } 31 | 32 | data, err := ioutil.ReadFile(path) 33 | if err != nil { 34 | return &Registry{Entries: map[string]RegistryEntry{}}, nil 35 | } 36 | 37 | err = json.Unmarshal(data, ®istry) 38 | if err != nil { 39 | return 40 | } 41 | 42 | return 43 | } 44 | 45 | func (registry *Registry) Close() error { 46 | path, err := makeRegistryFilePath() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | blob, err := json.Marshal(registry) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | err = ioutil.WriteFile(path, blob, 0666) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (registry *Registry) Add(entry RegistryEntry) error { 65 | registry.Entries[entry.FilePath] = entry 66 | return nil 67 | } 68 | 69 | func (registry *Registry) Remove(filePath string) { 70 | delete(registry.Entries, filePath) 71 | } 72 | 73 | func (registry *Registry) Update() { 74 | applicationsDir, err := MakeApplicationsDirPath() 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | 79 | files, err := ioutil.ReadDir(applicationsDir) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | for _, f := range files { 85 | if strings.HasSuffix(f.Name(), ".AppImage") { 86 | filePath := filepath.Join(applicationsDir, f.Name()) 87 | _, ok := registry.Entries[filePath] 88 | if !ok { 89 | entry := registry.createEntryFromFile(filePath) 90 | _ = registry.Add(entry) 91 | } 92 | } 93 | } 94 | 95 | for filePath, _ := range registry.Entries { 96 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 97 | registry.Remove(filePath) 98 | } 99 | } 100 | } 101 | 102 | func (registry *Registry) addFile(filePath string) { 103 | entry := registry.createEntryFromFile(filePath) 104 | _ = registry.Add(entry) 105 | } 106 | 107 | func (registry *Registry) createEntryFromFile(filePath string) RegistryEntry { 108 | fileSha1, _ := GetFileSHA1(filePath) 109 | updateInfo, _ := updateUtils.ReadUpdateInfo(filePath) 110 | entry := RegistryEntry{ 111 | Repo: "", 112 | FileSha1: fileSha1, 113 | AppName: "", 114 | AppVersion: "", 115 | FilePath: filePath, 116 | UpdateInfo: updateInfo, 117 | } 118 | return entry 119 | } 120 | 121 | func (registry *Registry) Lookup(target string) (RegistryEntry, bool) { 122 | applicationsDir, _ := MakeApplicationsDirPath() 123 | possibleFullPath := filepath.Join(applicationsDir, target) 124 | 125 | for _, entry := range registry.Entries { 126 | if entry.FileSha1 == target || entry.FilePath == target || 127 | entry.FilePath == possibleFullPath || entry.Repo == target { 128 | return entry, true 129 | } 130 | } 131 | 132 | if IsAppImageFile(target) { 133 | entry := registry.createEntryFromFile(target) 134 | _ = registry.Add(entry) 135 | 136 | return entry, true 137 | } else { 138 | if IsAppImageFile(possibleFullPath) { 139 | entry := registry.createEntryFromFile(target) 140 | _ = registry.Add(entry) 141 | 142 | return entry, true 143 | } 144 | } 145 | 146 | return RegistryEntry{}, false 147 | } 148 | 149 | func makeRegistryFilePath() (string, error) { 150 | applicationsPath, err := MakeApplicationsDirPath() 151 | if err != nil { 152 | return "", err 153 | } 154 | 155 | filePath := filepath.Join(applicationsPath, ".registry.json") 156 | return filePath, nil 157 | } 158 | -------------------------------------------------------------------------------- /internal/utils/registry_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestOpenRegistry(t *testing.T) { 9 | registry, err := OpenRegistry() 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | 14 | err = registry.Close() 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | } 19 | 20 | func TestRegistry_Remove(t *testing.T) { 21 | registry, err := OpenRegistry() 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | registry.Remove("AppImageUpdate-x86_64-old.AppImage") 27 | _, ok := registry.Entries["AppImageUpdate-x86_64-old.AppImage"] 28 | assert.False(t, ok) 29 | 30 | err = registry.Close() 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/utils/signature.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "debug/elf" 7 | "encoding/hex" 8 | "fmt" 9 | "golang.org/x/crypto/openpgp" 10 | "io" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | func VerifySignature(target string) (result *openpgp.Entity, err error) { 16 | key, err := readElfSection(target, ".sig_key") 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | signature, err := readElfSection(target, ".sha256_sig") 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | file, err := newAppImagePreSignatureReader(target) 27 | if err != nil { 28 | return 29 | } 30 | 31 | sha256Hash := sha256.New() 32 | _, err = io.Copy(sha256Hash, file) 33 | 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | verification_target := hex.EncodeToString(sha256Hash.Sum(nil)) 39 | 40 | keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | entity, err := openpgp.CheckArmoredDetachedSignature(keyring, strings.NewReader(verification_target), bytes.NewReader(signature)) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return entity, nil 51 | } 52 | 53 | func readElfSection(appImagePath string, sectionName string) ([]byte, error) { 54 | elfFile, err := elf.Open(appImagePath) 55 | if err != nil { 56 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error()) 57 | } 58 | 59 | section := elfFile.Section(sectionName) 60 | if section == nil { 61 | return nil, fmt.Errorf("missing " + sectionName + " section on target elf") 62 | } 63 | sectionData, err := section.Data() 64 | 65 | if err != nil { 66 | return nil, fmt.Errorf("Unable to parse " + sectionName + " section: " + err.Error()) 67 | } 68 | 69 | str_end := bytes.Index(sectionData, []byte("\000")) 70 | if str_end == -1 || str_end == 0 { 71 | return nil, nil 72 | } 73 | 74 | return sectionData[:str_end], nil 75 | } 76 | 77 | func ReadSignature(appImagePath string) ([]byte, error) { 78 | elfFile, err := elf.Open(appImagePath) 79 | if err != nil { 80 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error()) 81 | } 82 | 83 | updInfo := elfFile.Section(".sha256_sig") 84 | if updInfo == nil { 85 | panic("Missing .sha256_sig section on target elf ") 86 | } 87 | sectionData, err := updInfo.Data() 88 | 89 | if err != nil { 90 | panic("Unable to parse .sha256_sig section: " + err.Error()) 91 | } 92 | 93 | str_end := bytes.Index(sectionData, []byte("\000")) 94 | if str_end == -1 || str_end == 0 { 95 | return nil, fmt.Errorf("No update information found in: " + appImagePath) 96 | } 97 | 98 | return sectionData[:str_end], nil 99 | } 100 | 101 | type appImagePreSignatureReader struct { 102 | keySectionOffset uint64 103 | keySectionSize uint64 104 | 105 | sigSectionOffset uint64 106 | sigSectionSize uint64 107 | 108 | offset uint64 109 | file *os.File 110 | } 111 | 112 | func newAppImagePreSignatureReader(target string) (*appImagePreSignatureReader, error) { 113 | elfFile, err := elf.Open(target) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | key := elfFile.Section(".sig_key") 119 | if key == nil { 120 | return nil, fmt.Errorf("Missing .sig_key section") 121 | } 122 | 123 | signature := elfFile.Section(".sha256_sig") 124 | if signature == nil { 125 | return nil, fmt.Errorf("Missing .sha256_sig section") 126 | } 127 | 128 | file, err := os.Open(target) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return &appImagePreSignatureReader{ 134 | offset: 0, 135 | file: file, 136 | keySectionOffset: key.Offset, 137 | keySectionSize: key.Size, 138 | sigSectionOffset: signature.Offset, 139 | sigSectionSize: signature.Size, 140 | }, nil 141 | } 142 | 143 | func (reader *appImagePreSignatureReader) Read(p []byte) (n int, err error) { 144 | n, err = reader.file.Read(p) 145 | if err != nil { 146 | return 147 | } 148 | 149 | oldOffset := reader.offset 150 | reader.offset += uint64(n) 151 | 152 | if reader.keySectionOffset >= oldOffset && reader.keySectionOffset < reader.offset { 153 | start := reader.keySectionOffset - oldOffset 154 | for i := start; i < uint64(n) && (i-start) < reader.keySectionSize; i++ { 155 | p[i] = 0 156 | } 157 | } 158 | 159 | if reader.sigSectionOffset >= oldOffset && reader.sigSectionOffset < reader.offset { 160 | start := reader.sigSectionOffset - oldOffset 161 | for i := start; i < uint64(n) && (i-start) < reader.sigSectionSize; i++ { 162 | p[i] = 0 163 | } 164 | } 165 | 166 | return n, err 167 | } 168 | -------------------------------------------------------------------------------- /internal/utils/util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // #include 4 | // #include 5 | import "C" 6 | 7 | import ( 8 | "bytes" 9 | "crypto/sha1" 10 | "debug/elf" 11 | "encoding/hex" 12 | "fmt" 13 | "github.com/manifoldco/promptui" 14 | "github.com/schollz/progressbar/v3" 15 | "io" 16 | "net/http" 17 | "os" 18 | "os/signal" 19 | "os/user" 20 | "path/filepath" 21 | "strings" 22 | ) 23 | 24 | func MakeApplicationsDirPath() (string, error) { 25 | usr, err := user.Current() 26 | if err != nil { 27 | return "", err 28 | } 29 | 30 | applicationsPath := filepath.Join(usr.HomeDir, "Applications") 31 | err = os.MkdirAll(applicationsPath, os.ModePerm) 32 | if err != nil { 33 | return "", err 34 | } 35 | return applicationsPath, nil 36 | } 37 | 38 | func QueryUrl(url string) (bytes.Buffer, error) { 39 | resp, err := http.Get(url) 40 | if err != nil { 41 | return bytes.Buffer{}, err 42 | } 43 | defer resp.Body.Close() 44 | 45 | var data bytes.Buffer 46 | _, err = io.Copy(&data, resp.Body) 47 | if err != nil { 48 | return bytes.Buffer{}, err 49 | } 50 | 51 | return data, nil 52 | } 53 | 54 | func DownloadAppImage(url string, filePath string) error { 55 | output, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755) 56 | if err != nil { 57 | return err 58 | } 59 | defer output.Close() 60 | 61 | resp, err := http.Get(url) 62 | if err != nil { 63 | return err 64 | } 65 | defer resp.Body.Close() 66 | 67 | bar := progressbar.DefaultBytes( 68 | resp.ContentLength, 69 | "Downloading", 70 | ) 71 | 72 | go func() { 73 | sigchan := make(chan os.Signal, 1) 74 | signal.Notify(sigchan, os.Interrupt) 75 | <-sigchan 76 | 77 | _ = resp.Body.Close() 78 | _ = output.Close() 79 | _ = os.Remove(filePath) 80 | 81 | os.Exit(0) 82 | }() 83 | 84 | _, err = io.Copy(io.MultiWriter(output, bar), resp.Body) 85 | return err 86 | } 87 | 88 | func UrlToTarget(target string) (string, error) { 89 | if strings.HasPrefix(target, "https://github.com/") { 90 | target, err := resolveGithubProjectTarget(target) 91 | return target, err 92 | } 93 | 94 | return target, nil 95 | } 96 | 97 | func resolveGithubProjectTarget(target string) (string, error) { 98 | target = target[19:] 99 | target_parts := strings.Split(target, "/") 100 | 101 | if len(target_parts) < 2 { 102 | return "", fmt.Errorf("missing github owner or project") 103 | } 104 | 105 | return "github:" + target_parts[0] + "/" + target_parts[1], nil 106 | } 107 | 108 | type BinaryUrl struct { 109 | FileName string 110 | Url string 111 | } 112 | 113 | func PromptBinarySelection(downloadLinks []BinaryUrl) (result *BinaryUrl, err error) { 114 | if len(downloadLinks) == 1 { 115 | return &downloadLinks[0], nil 116 | } 117 | 118 | prompt := promptui.Select{ 119 | Label: "Select binary to download", 120 | Items: downloadLinks, 121 | Templates: &promptui.SelectTemplates{ 122 | Label: " {{ .FileName }}", 123 | Active: "\U00002705 {{ .FileName }}", 124 | Inactive: " {{ .FileName }}", 125 | Selected: "\U00002705 {{ .FileName }}"}, 126 | } 127 | 128 | i, _, err := prompt.Run() 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return &downloadLinks[i], nil 134 | } 135 | 136 | func MakeTargetFilePath(link *BinaryUrl) (string, error) { 137 | applicationsPath, err := MakeApplicationsDirPath() 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | filePath := filepath.Join(applicationsPath, link.FileName) 143 | return filePath, nil 144 | } 145 | 146 | func ReadUpdateInfo(appImagePath string) (string, error) { 147 | elfFile, err := elf.Open(appImagePath) 148 | if err != nil { 149 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error()) 150 | } 151 | 152 | updInfo := elfFile.Section(".upd_info") 153 | if updInfo == nil { 154 | panic("Missing update section on target elf ") 155 | } 156 | sectionData, err := updInfo.Data() 157 | 158 | if err != nil { 159 | panic("Unable to parse update section: " + err.Error()) 160 | } 161 | 162 | str_end := bytes.Index(sectionData, []byte("\000")) 163 | if str_end == -1 || str_end == 0 { 164 | return "", fmt.Errorf("No update information found in: " + appImagePath) 165 | } 166 | 167 | update_info := string(sectionData[:str_end]) 168 | return update_info, nil 169 | } 170 | 171 | func GetFileSHA1(filePath string) (string, error) { 172 | file, err := os.Open(filePath) 173 | if err != nil { 174 | return "", err 175 | } 176 | 177 | sha1Checksum := sha1.New() 178 | _, err = io.Copy(sha1Checksum, file) 179 | if err != nil { 180 | return "", err 181 | } 182 | return hex.EncodeToString(sha1Checksum.Sum(nil)), nil 183 | } 184 | 185 | func IsAppImageFile(filePath string) bool { 186 | file, err := os.Open(filePath) 187 | if err != nil { 188 | return false 189 | } 190 | 191 | return isAppImage1File(file) || isAppImage2File(file) 192 | } 193 | 194 | func isAppImage2File(file *os.File) bool { 195 | return isElfFile(file) && fileHasBytesAt(file, []byte{0x41, 0x49, 0x02}, 8) 196 | } 197 | 198 | func isAppImage1File(file *os.File) bool { 199 | return isISO9660(file) || fileHasBytesAt(file, []byte{0x41, 0x49, 0x01}, 8) 200 | } 201 | 202 | func isElfFile(file *os.File) bool { 203 | return fileHasBytesAt(file, []byte{0x7f, 0x45, 0x4c, 0x46}, 0) 204 | } 205 | 206 | func isISO9660(file *os.File) bool { 207 | for _, offset := range []int64{32769, 34817, 36865} { 208 | if fileHasBytesAt(file, []byte{'C', 'D', '0', '0', '1'}, offset) { 209 | return true 210 | } 211 | } 212 | 213 | return false 214 | } 215 | 216 | func fileHasBytesAt(file *os.File, expectedBytes []byte, offset int64) bool { 217 | readBytes := make([]byte, len(expectedBytes)) 218 | _, _ = file.Seek(offset, 0) 219 | _, _ = file.Read(readBytes) 220 | 221 | return bytes.Compare(readBytes, expectedBytes) == 0 222 | } 223 | --------------------------------------------------------------------------------