├── .github
└── workflows
│ └── release.yaml
├── .gitignore
├── .idea
├── .gitignore
├── R5ReloadedInstaller.iml
└── modules.xml
├── README.md
├── cmd
└── r5_installer
│ ├── interaction.go
│ ├── main.go
│ ├── processes.go
│ └── startup.go
├── go.mod
├── go.sum
├── internal
└── download
│ └── github.go
└── pkg
├── download
├── download.go
└── writecounter.go
├── progress
└── utils.go
├── util
├── util.go
└── zip.go
└── validation
└── validation.go
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | release:
3 | types: [created]
4 |
5 | jobs:
6 | releases-matrix:
7 | name: Release Go Binary
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | # build and publish in parallel: linux/386, linux/amd64, linux/arm64, windows/386, windows/amd64, darwin/amd64, darwin/arm64
12 | goos: [ windows ]
13 | goarch: [amd64]
14 | exclude:
15 | - goarch: arm64
16 | goos: windows
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: wangyoucao577/go-release-action@v1.30
21 | with:
22 | github_token: ${{ secrets.GITHUB_TOKEN }}
23 | goos: ${{ matrix.goos }}
24 | goarch: ${{ matrix.goarch }}
25 | goversion: "https://dl.google.com/go/go1.19.1.linux-amd64.tar.gz"
26 | project_path: "./cmd/r5_installer"
27 | binary_name: "r5util"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | main.exe
3 |
4 | r5_installer.exe
5 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/R5ReloadedInstaller.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # R5ReloadedInstaller
2 |
3 | ## What
4 |
5 | This application assists with downloading and extracting files required for using [R5Reloaded](https://github.com/Mauler125/r5sdk).
6 |
7 | ### Current Functionality/Supported Downloads
8 | 1. [R5sdk](https://github.com/Mauler125/r5sdk) + [scripts_r5](https://github.com/Mauler125/scripts_r5)
9 | 2. [FlowState AimTrainer + Scripts](https://github.com/ColombianGuy/r5_aimtrainer)
10 | 3. Troubleshooting option to delete scripts prior to downloading current versions
11 |
12 | ## How to use this application
13 |
14 | 1. Download the latest release of this repository from: https://github.com/M1kep/R5ReloadedInstaller/releases/latest
15 | 2. Unzip the `.exe` file into the R5Reloaded directory. This should be the same directory as the `r5apex.exe` file.
16 | 3. Double-click the `r5util.exe` and follow the prompts in the terminal window.
--------------------------------------------------------------------------------
/cmd/r5_installer/interaction.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/AlecAivazis/survey/v2"
6 | )
7 |
8 | func gatherRunOptions(options []string) (selectedOptions []string, err error) {
9 | prompt := &survey.MultiSelect{
10 | Message: "Please select from the following options",
11 | Options: options,
12 | Default: []int{
13 | 0,
14 | 1,
15 | },
16 | }
17 |
18 | //_ = dialog.Raise("Use the arrow keys(navigation) and spacebar(toggle selection) in console to continue")
19 | err = survey.AskOne(prompt, &selectedOptions)
20 | if err != nil {
21 | err = fmt.Errorf("error while gathering options from user: %v", err)
22 | }
23 |
24 | return
25 | }
26 |
--------------------------------------------------------------------------------
/cmd/r5_installer/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "R5ReloadedInstaller/pkg/util"
5 | "R5ReloadedInstaller/pkg/validation"
6 | "fmt"
7 | "github.com/google/go-github/v47/github"
8 | "github.com/gosuri/uiprogress"
9 | "github.com/pkg/browser"
10 | "github.com/rs/zerolog"
11 | "github.com/tawesoft/golib/v2/dialog"
12 | "golang.org/x/sync/errgroup"
13 | "os"
14 | "path/filepath"
15 | "strings"
16 | )
17 |
18 | func main() {
19 | VERSION := "v0.15.1"
20 | var r5Folder string
21 | ghClient := github.NewClient(nil)
22 |
23 | r5Folder, err := getValidatedR5Folder()
24 | if err != nil {
25 | util.LogErrorWithDialog(err)
26 | return
27 | }
28 |
29 | if validation.IsLauncherFileLocked(r5Folder) {
30 | _ = dialog.Raise("Please close the R5 Launcher before running.")
31 | return
32 | }
33 |
34 | cacheDir, err := initializeDirectories(r5Folder)
35 | if err != nil {
36 | util.LogErrorWithDialog(err)
37 | return
38 | }
39 |
40 | logFile, err := os.Create(filepath.Join(cacheDir, "logfile.txt"))
41 | if err != nil {
42 | util.LogErrorWithDialog(fmt.Errorf("error creating logging file: %v", err))
43 | return
44 | }
45 | defer logFile.Close()
46 |
47 | fileLogger := zerolog.New(logFile).With().Logger()
48 |
49 | shouldExit, msg, err := checkForUpdate(ghClient, cacheDir, VERSION)
50 | if msg != "" {
51 | fmt.Println(msg)
52 | }
53 |
54 | if err != nil {
55 | fmt.Println(err)
56 | }
57 |
58 | if shouldExit {
59 | if strings.HasPrefix(msg, "New major version") {
60 | err := browser.OpenURL("https://github.com/M1kep/R5ReloadedInstaller/releases/latest")
61 | if err != nil {
62 | fileLogger.Error().Err(fmt.Errorf("error opening browser to latest release: %v", err)).Msg("error")
63 | _ = dialog.Error("Error opening browser to latest release. Please manually update from https://github.com/M1kep/R5ReloadedInstaller/releases/latest")
64 | return
65 | }
66 | }
67 | _ = dialog.Raise("Exiting due to update check.")
68 | return
69 | }
70 |
71 | //type optionConfig struct {
72 | // UIOption string
73 | // UIPriority int
74 | // RunPriority int
75 | //}
76 | //var options []optionConfig
77 | //options = append(options, optionConfig{"SDK", 50, 300})
78 | //options = append(options, optionConfig{"Latest Flowstate Scripts", 100, 300})
79 | //options = append(options, optionConfig{
80 | // "(Troubleshooting) Clean Scripts - Deletes 'platform/scripts' prior to extracting",
81 | // 1000,
82 | // 50,
83 | //})
84 | selectedOptions, err := gatherRunOptions([]string{
85 | "SDK",
86 | "Latest Flowstate Scripts",
87 | "(Troubleshooting) Clean Scripts - Deletes 'platform/scripts' prior to extracting",
88 | "(DEV) Latest r5_scripts",
89 | "SDK(Include Pre-Releases)",
90 | })
91 | if err != nil {
92 | fileLogger.Error().Err(fmt.Errorf("error gathering run options")).Msg("error")
93 | util.LogErrorWithDialog(fmt.Errorf("error gathering run options"))
94 | return
95 | }
96 |
97 | uiprogress.Start()
98 | errGroup := new(errgroup.Group)
99 |
100 | if util.Contains(selectedOptions, "(Troubleshooting) Clean Scripts - Deletes 'platform/scripts' prior to extracting") {
101 | err := os.RemoveAll(filepath.Join(r5Folder, "platform/scripts"))
102 | if err != nil {
103 | fileLogger.Error().Err(fmt.Errorf("error removing 'platform/scripts' folder: %v", err)).Msg("error")
104 | util.LogErrorWithDialog(fmt.Errorf("error removing 'platform/scripts' folder: %v", err))
105 | return
106 | }
107 | }
108 |
109 | if util.Contains(selectedOptions, "SDK") {
110 | err := ProcessSDK(
111 | ghClient,
112 | errGroup,
113 | cacheDir,
114 | r5Folder,
115 | false,
116 | )
117 |
118 | if err != nil {
119 | fileLogger.Error().Err(err).Msg("error")
120 | util.LogErrorWithDialog(err)
121 | return
122 | }
123 | }
124 |
125 | if util.Contains(selectedOptions, "SDK(Include Pre-Releases)") {
126 | err := ProcessSDK(
127 | ghClient,
128 | errGroup,
129 | cacheDir,
130 | r5Folder,
131 | true,
132 | )
133 |
134 | if err != nil {
135 | fileLogger.Error().Err(err).Msg("error")
136 | util.LogErrorWithDialog(err)
137 | return
138 | }
139 | }
140 |
141 | if util.Contains(selectedOptions, "(DEV) Latest r5_scripts") {
142 | err := ProcessLatestR5Scripts(
143 | ghClient,
144 | errGroup,
145 | cacheDir,
146 | r5Folder,
147 | )
148 |
149 | if err != nil {
150 | fileLogger.Error().Err(err).Msg("error")
151 | util.LogErrorWithDialog(err)
152 | return
153 | }
154 | }
155 |
156 | if util.Contains(selectedOptions, "Latest Flowstate Scripts") {
157 | err := ProcessFlowstate(
158 | ghClient,
159 | errGroup,
160 | cacheDir,
161 | r5Folder,
162 | )
163 |
164 | if err != nil {
165 | fileLogger.Error().Err(err).Msg("error")
166 | util.LogErrorWithDialog(err)
167 | return
168 | }
169 | }
170 |
171 | _ = dialog.Raise("Success. Confirm to close terminal.")
172 | return
173 | }
174 |
--------------------------------------------------------------------------------
/cmd/r5_installer/processes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "R5ReloadedInstaller/internal/download"
5 | "R5ReloadedInstaller/pkg/util"
6 | "fmt"
7 | "github.com/google/go-github/v47/github"
8 | "golang.org/x/sync/errgroup"
9 | "path/filepath"
10 | )
11 |
12 | func ProcessSDK(ghClient *github.Client, errGroup *errgroup.Group, cacheDir string, r5Folder string, includePreReleases bool) error {
13 | // Download SDK Release
14 | sdkOutputPath, err := download.StartLatestRepoReleaseDownload(
15 | ghClient,
16 | errGroup,
17 | "Downloading SDK",
18 | cacheDir,
19 | "sdk-depot",
20 | "depot.zip",
21 | "Mauler125",
22 | "r5sdk",
23 | includePreReleases,
24 | )
25 | if err != nil {
26 | return fmt.Errorf("error starting download of sdk release: %v", err)
27 | }
28 |
29 | if err := errGroup.Wait(); err != nil {
30 | return fmt.Errorf("error encountered while performing SDK download: %v", err)
31 | }
32 |
33 | // Unzip SDK into R5Folder
34 | err = util.UnzipFile(sdkOutputPath, r5Folder, false, "Extracting SDK")
35 | if err != nil {
36 | return fmt.Errorf("error unzipping sdk: %v", err)
37 | }
38 |
39 | return nil
40 | }
41 |
42 | func ProcessLatestR5Scripts(ghClient *github.Client, errGroup *errgroup.Group, cacheDir string, r5Folder string) error {
43 | // Download scripts_r5
44 | scriptsRepoContentsOutput, err := download.StartLatestRepoContentsDownload(
45 | ghClient,
46 | errGroup,
47 | "Downloading Scripts",
48 | cacheDir,
49 | "scripts",
50 | "Mauler125",
51 | "scripts_r5",
52 | )
53 | if err != nil {
54 | return fmt.Errorf("error starting download of scripts: %v", err)
55 | }
56 |
57 | if err := errGroup.Wait(); err != nil {
58 | return fmt.Errorf("error encountered while performing r5_scripts download: %v", err)
59 | }
60 |
61 | // Unzip Scripts into platform/scripts
62 | err = util.UnzipFile(scriptsRepoContentsOutput, filepath.Join(r5Folder, "platform/scripts"), true, "Extracting scripts")
63 | if err != nil {
64 | return fmt.Errorf("error unzipping scripts: %v", err)
65 | }
66 |
67 | return nil
68 | }
69 |
70 | func ProcessFlowstate(ghClient *github.Client, errGroup *errgroup.Group, cacheDir string, r5Folder string) error {
71 | flowstateReleaseOutput, err := download.StartLatestRepoReleaseDownload(
72 | ghClient,
73 | errGroup,
74 | "Downloading FlowState Required Files",
75 | cacheDir,
76 | "flowstate-deps",
77 | "Flowstate.-.Required.Files.zip",
78 | "ColombianGuy",
79 | "r5_flowstate",
80 | false,
81 | )
82 | if err != nil {
83 | return fmt.Errorf("error starting download of Flowstate release: %v", err)
84 | }
85 |
86 | // Download Aim trainer contents
87 | flowstateScriptsOutput, err := download.StartLatestRepoContentsDownload(
88 | ghClient,
89 | errGroup,
90 | "Downloading Latest Flowstate Scripts",
91 | cacheDir,
92 | "scripts",
93 | "ColombianGuy",
94 | "r5_flowstate",
95 | )
96 | if err != nil {
97 | return fmt.Errorf("error starting download of Flowstate scripts: %v", err)
98 | }
99 |
100 | if err := errGroup.Wait(); err != nil {
101 | return fmt.Errorf("error encountered while performing Flowstate downloads: %v", err)
102 | }
103 |
104 | // Unzip Flowstate deps into R5Folder
105 | err = util.UnzipFile(flowstateReleaseOutput, r5Folder, false, "Extracting Flowstate Deps")
106 | if err != nil {
107 | return fmt.Errorf("error unzipping Flowstate deps: %v", err)
108 | }
109 |
110 | //Unzip Flowstate Scripts into platform/scripts
111 | err = util.UnzipFile(flowstateScriptsOutput, filepath.Join(r5Folder, "platform/scripts"), true, "Extracting Flowstate Scripts")
112 | if err != nil {
113 | return fmt.Errorf("error unzipping Flowstate scripts: %v", err)
114 | }
115 |
116 | return nil
117 | }
118 |
--------------------------------------------------------------------------------
/cmd/r5_installer/startup.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "R5ReloadedInstaller/pkg/validation"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "github.com/google/go-github/v47/github"
9 | "github.com/tawesoft/golib/v2/dialog"
10 | "golang.org/x/mod/semver"
11 | "os"
12 | "path/filepath"
13 | "time"
14 | )
15 |
16 | func getValidatedR5Folder() (validatedFolder string, err error) {
17 | isRunningInR5Folder, err := validation.IsRunningInR5Folder()
18 | if err != nil {
19 | return "", err
20 | }
21 |
22 | if isRunningInR5Folder {
23 | validatedFolder, err := os.Getwd()
24 | if err != nil {
25 | return "", fmt.Errorf("error retrieving current directory while validating r5Path: %v", err)
26 | }
27 |
28 | return validatedFolder, nil
29 | }
30 |
31 | // Check CLI argument
32 | if !(len(os.Args) >= 2) {
33 | _ = dialog.Raise("Please move the R5RInstaller into your R5 Directory")
34 | return "", fmt.Errorf("not running from r5Folder and no argument provided")
35 | }
36 |
37 | pathFromArgs := os.Args[1]
38 | isPathR5Folder, err := validation.IsR5Folder(pathFromArgs)
39 | if err != nil {
40 | return "", err
41 | }
42 | if isPathR5Folder {
43 | validatedFolder = pathFromArgs
44 | } else {
45 | _ = dialog.Raise("Please move the R5RInstaller into your R5 Directory or pass correct path via arguments")
46 | return "", fmt.Errorf("not running from r5Folder and provided path argument is invalid")
47 | }
48 |
49 | return validatedFolder, nil
50 | }
51 |
52 | func initializeDirectories(r5Folder string) (cacheDir string, err error) {
53 | cacheDir = filepath.Join(r5Folder, "R5InstallerDirectory/cache")
54 |
55 | err = os.MkdirAll(cacheDir, 0777)
56 | if err != nil {
57 | err = fmt.Errorf("error initializing installer directory %s: %v", cacheDir, err)
58 | return
59 | }
60 |
61 | return
62 | }
63 |
64 | type UpdateCheckDetails struct {
65 | LastUpdateCheck time.Time
66 | LastRetrievedReleaseTag string
67 | }
68 |
69 | func checkForUpdate(ghClient *github.Client, cacheDir string, currentVersion string) (shouldExit bool, message string, err error) {
70 | repoOwner := "M1kep"
71 |
72 | repoName := "R5ReloadedInstaller"
73 | updateCheckDetailsFromDisk, err := loadUpdateCheckDetails(cacheDir)
74 | if err != nil {
75 | if !os.IsNotExist(err) {
76 | return true, "Error loading update details from disk", fmt.Errorf("error encountered loading update check details: %v", err)
77 | }
78 | }
79 |
80 | useUpdateDetailsCache := false
81 | // Update check should not happen within 10 minutes of each other
82 | nextUpdateCheckAt := updateCheckDetailsFromDisk.LastUpdateCheck.Add(time.Minute * 10)
83 | if time.Now().Before(nextUpdateCheckAt) {
84 | timeTillNextCheck := nextUpdateCheckAt.Sub(time.Now())
85 | if updateCheckDetailsFromDisk.LastRetrievedReleaseTag != "" {
86 | useUpdateDetailsCache = true
87 | } else {
88 | // If we don't have a cached tag, and the last update check was within 10 minutes, don't continue.
89 | return false, fmt.Sprintf("INFO: Last update check may have failed, waiting %s to check again.", timeTillNextCheck), nil
90 | }
91 | }
92 |
93 | if !semver.IsValid(currentVersion) {
94 | return true, "Current version is invalid", fmt.Errorf("invalid current version provided '%s'", currentVersion)
95 | }
96 |
97 | newUpdateCheckDetails := UpdateCheckDetails{}
98 | var latestVersionTag string
99 | if useUpdateDetailsCache {
100 | latestVersionTag = updateCheckDetailsFromDisk.LastRetrievedReleaseTag
101 | newUpdateCheckDetails.LastUpdateCheck = updateCheckDetailsFromDisk.LastUpdateCheck
102 |
103 | // If a new version was downloaded within the udpatecheck delay window(10 minutes)
104 | // Then we should use and persist the current version
105 | if semver.Compare(currentVersion, latestVersionTag) > 0 {
106 | latestVersionTag = currentVersion
107 | }
108 | } else {
109 | newUpdateCheckDetails.LastUpdateCheck = time.Now()
110 | repoReleases, _, err := ghClient.Repositories.ListReleases(context.Background(), repoOwner, repoName, &github.ListOptions{})
111 | if err != nil {
112 | saveDetailsErr := saveUpdateDetails(cacheDir, newUpdateCheckDetails)
113 | if saveDetailsErr != nil {
114 | return false, "", err
115 | }
116 |
117 | return false, "", fmt.Errorf("error listing releases for %s/%s: %v", repoOwner, repoName, err)
118 | }
119 |
120 | latestVersionTag = *(repoReleases[0].TagName)
121 | }
122 |
123 | if !semver.IsValid(latestVersionTag) {
124 | err := saveUpdateDetails(cacheDir, newUpdateCheckDetails)
125 | if err != nil {
126 | return false, "", err
127 | }
128 |
129 | return false, "", fmt.Errorf("invalid version from GitHub release '%s'", latestVersionTag)
130 | }
131 |
132 | newUpdateCheckDetails.LastRetrievedReleaseTag = latestVersionTag
133 | err = saveUpdateDetails(cacheDir, newUpdateCheckDetails)
134 | if err != nil {
135 | return false, "", err
136 | }
137 |
138 | if semver.Compare(currentVersion, latestVersionTag) < 0 {
139 | if semver.Major(latestVersionTag) > semver.Major(currentVersion) {
140 | return true, "New major version available. Browser will open to the following link after closing: https://github.com/M1kep/R5ReloadedInstaller/releases/latest", nil
141 | }
142 |
143 | return false, "New minor update is available. Consider downloading the latest release from https://github.com/M1kep/R5ReloadedInstaller/releases/latest", nil
144 | }
145 | return false, "", nil
146 | }
147 |
148 | func saveUpdateDetails(cacheDir string, details UpdateCheckDetails) error {
149 | jsonOut, err := json.Marshal(details)
150 | if err != nil {
151 | return fmt.Errorf("error marshalling details: %v", err)
152 | }
153 |
154 | err = os.WriteFile(filepath.Join(cacheDir, "updateCheckDetails.json"), jsonOut, 0777)
155 | if err != nil {
156 | return fmt.Errorf("error writing update details to disk: %v", err)
157 | }
158 |
159 | return nil
160 | }
161 |
162 | func loadUpdateCheckDetails(cacheDir string) (UpdateCheckDetails, error) {
163 | fileBytes, err := os.ReadFile(filepath.Join(cacheDir, "updateCheckDetails.json"))
164 | if err != nil {
165 | if os.IsNotExist(err) {
166 | return UpdateCheckDetails{}, err
167 | } else {
168 | return UpdateCheckDetails{}, fmt.Errorf("error reading update details from disk: %v", err)
169 | }
170 | }
171 |
172 | details := UpdateCheckDetails{}
173 | err = json.Unmarshal(fileBytes, &details)
174 | if err != nil {
175 | return UpdateCheckDetails{}, fmt.Errorf("error unmarshalling update details: %v", err)
176 | }
177 |
178 | return details, nil
179 | }
180 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module R5ReloadedInstaller
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/AlecAivazis/survey/v2 v2.3.6
7 | github.com/google/go-github/v47 v47.0.0
8 | github.com/gosuri/uiprogress v0.0.1
9 | github.com/rs/zerolog v1.28.0
10 | github.com/tawesoft/golib/v2 v2.1.0
11 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
12 | golang.org/x/sync v0.0.0-20220907140024-f12130a52804
13 | )
14 |
15 | require (
16 | github.com/alessio/shellescape v1.4.1 // indirect
17 | github.com/google/go-querystring v1.1.0 // indirect
18 | github.com/gosuri/uilive v0.0.4 // indirect
19 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
20 | github.com/mattn/go-colorable v0.1.12 // indirect
21 | github.com/mattn/go-isatty v0.0.16 // indirect
22 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
23 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
24 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
25 | golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect
26 | golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
27 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
28 | golang.org/x/text v0.3.7 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
2 | github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
3 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
4 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
5 | github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
6 | github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
7 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
8 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
9 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
14 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
15 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
16 | github.com/google/go-github/v47 v47.0.0 h1:eQap5bIRZibukP0VhngWgpuM0zhY4xntqOzn6DhdkE4=
17 | github.com/google/go-github/v47 v47.0.0/go.mod h1:DRjdvizXE876j0YOZwInB1ESpOcU/xFBClNiQLSdorE=
18 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
19 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
20 | github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
21 | github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
22 | github.com/gosuri/uiprogress v0.0.1 h1:0kpv/XY/qTmFWl/SkaJykZXrBBzwwadmW8fRb7RJSxw=
23 | github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0=
24 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
25 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
26 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
27 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
28 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
29 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
30 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
31 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
32 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
33 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
35 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
36 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
37 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
38 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
39 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
42 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
43 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
44 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
46 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
47 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
48 | github.com/tawesoft/golib/v2 v2.1.0 h1:Xc84d+0KWaKik1Y4Ydqt3eNSIfJAglMq1/3X87xKFDY=
49 | github.com/tawesoft/golib/v2 v2.1.0/go.mod h1:SR4iyLNwV6fMYmKXDrVFXibIrduOeIMmqwB4szmVNuk=
50 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
51 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
52 | golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
53 | golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
54 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
55 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
56 | golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
57 | golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
58 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
59 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
60 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
61 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
62 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
63 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
64 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
65 | golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
66 | golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
67 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
68 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
69 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
70 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
71 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
72 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
73 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
77 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
78 |
--------------------------------------------------------------------------------
/internal/download/github.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "R5ReloadedInstaller/pkg/download"
5 | "context"
6 | "fmt"
7 | "github.com/google/go-github/v47/github"
8 | "golang.org/x/sync/errgroup"
9 | "path/filepath"
10 | )
11 |
12 | func StartLatestRepoReleaseDownload(ghClient *github.Client, eg *errgroup.Group, progressMessage string, cacheDirectory string, cacheName string, releaseFileName string, repoOwner string, repoName string, includePreReleases bool) (outputPath string, err error) {
13 | repoReleases, _, err := ghClient.Repositories.ListReleases(context.Background(), repoOwner, repoName, &github.ListOptions{})
14 | if err != nil {
15 | return "", fmt.Errorf("error listing releases for %s/%s: %v", repoOwner, repoName, err)
16 | }
17 |
18 | releaseToDownload := repoReleases[0]
19 | if !includePreReleases {
20 | for _, repo := range repoReleases {
21 | if !*repo.Prerelease {
22 | releaseToDownload = repo
23 | break
24 | }
25 | }
26 | }
27 |
28 | downloadUrl := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", repoOwner, repoName, *releaseToDownload.TagName, releaseFileName)
29 | sdkOutputPath := filepath.Join(cacheDirectory, fmt.Sprintf("%s_%s.zip", cacheName, *releaseToDownload.TagName))
30 |
31 | eg.Go(func() error {
32 | err := download.DownloadFile(sdkOutputPath, downloadUrl, progressMessage)
33 | if err != nil {
34 | return fmt.Errorf("error downloading release(%s) for %s/%s: %v", *releaseToDownload.TagName, repoOwner, repoName, err)
35 | }
36 | return nil
37 | })
38 |
39 | return sdkOutputPath, nil
40 | }
41 |
42 | func StartLatestRepoContentsDownload(ghClient *github.Client, eg *errgroup.Group, progressMessage string, cacheDirectory string, cacheName string, repoOwner string, repoName string) (outputPath string, err error) {
43 | ghRepo, _, err := ghClient.Repositories.Get(context.Background(), repoOwner, repoName)
44 | if err != nil {
45 | return "", fmt.Errorf("error retrieving repo info for %s/%s: %v", repoOwner, repoName, err)
46 | }
47 |
48 | repoCommits, _, err := ghClient.Repositories.ListCommits(context.Background(), repoOwner, repoName, &github.CommitsListOptions{
49 | ListOptions: github.ListOptions{
50 | PerPage: 1,
51 | },
52 | })
53 | if err != nil {
54 | return "", fmt.Errorf("error retrieving commits for %s/%s: %v", repoOwner, repoName, err)
55 | }
56 | latestCommitShortSHA := (*repoCommits[0].SHA)[0:7]
57 | downloadUrl := fmt.Sprintf("https://api.github.com/repos/%s/%s/zipball/%s", repoOwner, repoName, *ghRepo.DefaultBranch)
58 | outputPath = filepath.Join(cacheDirectory, fmt.Sprintf("%s-%s_%s.zip", cacheName, *ghRepo.DefaultBranch, latestCommitShortSHA))
59 |
60 | eg.Go(func() error {
61 | err := download.DownloadFile(outputPath, downloadUrl, progressMessage)
62 | if err != nil {
63 | return fmt.Errorf("error downloading repo contents from %s/%s for commit %s: %v", repoOwner, repoName, latestCommitShortSHA, err)
64 | }
65 | return nil
66 | })
67 |
68 | return outputPath, nil
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/download/download.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "R5ReloadedInstaller/pkg/progress"
5 | "R5ReloadedInstaller/pkg/util"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | )
11 |
12 | // Built off of https://golang.doc.xuwenliang.com/download-a-file-with-progress/
13 |
14 | // DownloadFile will download a url to a local file. It's efficient because it will
15 | // write as it downloads and not load the whole file into memory. We pass an io.TeeReader
16 | // into Copy() to report progress on the download.
17 | func DownloadFile(filepath string, url string, downloadMessage string) error {
18 | out, err := os.Create(filepath + ".tmp")
19 | if err != nil {
20 | return fmt.Errorf("error creating tmp file for download: %v", err)
21 | }
22 |
23 | contentLength, err := util.GetContentLengthFromURL(url)
24 | if err != nil {
25 | return fmt.Errorf("error retrieving content length for file download: %v", err)
26 | }
27 |
28 | pb := progress.NewProgressBarWithMessage(downloadMessage, contentLength)
29 | // Get the data
30 | resp, err := http.Get(url)
31 | defer resp.Body.Close()
32 | if err != nil {
33 | return fmt.Errorf("GET request for %s while performing file download failed: %v", url, err)
34 | }
35 |
36 | // Create our progress reporter and pass it to be used alongside our writer
37 | writeTracker := &WriteTracker{
38 | Pb: pb,
39 | ContentLength: contentLength,
40 | }
41 | _, err = io.Copy(out, io.TeeReader(resp.Body, writeTracker))
42 | if err != nil {
43 | return fmt.Errorf("error while downloading file from %s: %v", url, err)
44 | }
45 |
46 | // If the content-length was 0, the progress bar needs to be manually incremented to indicate completion
47 | if contentLength == 0 {
48 | pb.Incr()
49 | }
50 | out.Close()
51 |
52 | err = os.Rename(filepath+".tmp", filepath)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/download/writecounter.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "fmt"
5 | "github.com/gosuri/uiprogress"
6 | )
7 |
8 | // WriteTracker counts the number of bytes written to it. It implements to the io.Writer
9 | // interface and we can pass this into io.TeeReader() which will report progress on each
10 | // write cycle.
11 | type WriteTracker struct {
12 | Pb *uiprogress.Bar
13 | Total int
14 | ContentLength int
15 |
16 | nextUpdate int
17 | }
18 |
19 | func (wt *WriteTracker) Write(p []byte) (int, error) {
20 | n := len(p)
21 | wt.Total += n
22 |
23 | if (wt.Total > wt.nextUpdate || wt.Total == wt.ContentLength) && wt.ContentLength != 0 {
24 | updateIncrementSize := wt.ContentLength / 100
25 | err := wt.UpdateProgress()
26 | if err != nil {
27 | return n, err
28 | }
29 |
30 | wt.nextUpdate = wt.nextUpdate + updateIncrementSize
31 | }
32 | return n, nil
33 | }
34 |
35 | func (wt *WriteTracker) UpdateProgress() error {
36 | if wt.ContentLength != 0 {
37 | err := wt.Pb.Set(wt.Total)
38 | if err != nil {
39 | return fmt.Errorf("error updating progress bar in writetracker: %v", err)
40 | }
41 | }
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/progress/utils.go:
--------------------------------------------------------------------------------
1 | package progress
2 |
3 | import (
4 | "github.com/gosuri/uiprogress"
5 | )
6 |
7 | // NewProgressBarWithMessage Will create a progress bar with the provided message
8 | // Progress bars total will be set to 1 if maxProgress is 0
9 | func NewProgressBarWithMessage(message string, maxProgress int) *uiprogress.Bar {
10 | var pb *uiprogress.Bar
11 | if maxProgress == 0 {
12 | pb = uiprogress.AddBar(1).AppendCompleted()
13 | } else {
14 | pb = uiprogress.AddBar(maxProgress).AppendCompleted()
15 | }
16 | pb.PrependFunc(func(b *uiprogress.Bar) string {
17 | return message
18 | })
19 |
20 | return pb
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/tawesoft/golib/v2/dialog"
7 | "net/http"
8 | "os"
9 | "strconv"
10 | )
11 |
12 | func Exists(fileOrDirName string) (bool, error) {
13 | if _, err := os.Stat(fileOrDirName); errors.Is(err, os.ErrNotExist) {
14 | return false, nil
15 | } else if err != nil {
16 | return false, err
17 | } else {
18 | return true, nil
19 | }
20 | }
21 |
22 | func Contains[T comparable](elems []T, v T) bool {
23 | for _, s := range elems {
24 | if v == s {
25 | return true
26 | }
27 | }
28 | return false
29 | }
30 |
31 | func GetContentLengthFromURL(url string) (contentLength int, err error) {
32 | headResp, err := http.Head(url)
33 | if err != nil {
34 | return 0, fmt.Errorf("HEAD request for %s failed: %v", url, err)
35 | }
36 |
37 | contentLengthHeader := headResp.Header.Get("Content-Length")
38 | if contentLengthHeader == "" {
39 | return 0, nil
40 | }
41 |
42 | contentLength, err = strconv.Atoi(contentLengthHeader)
43 | if err != nil {
44 | return 0, fmt.Errorf("failed to convert \"%s\" to int: %v", contentLengthHeader, err)
45 | }
46 | return contentLength, nil
47 | }
48 |
49 | func LogErrorWithDialog(err error) {
50 | fmt.Println(err)
51 | _ = dialog.Error("Program encountered error. See console for logs.")
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/util/zip.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "archive/zip"
5 | "fmt"
6 | "github.com/gosuri/uiprogress"
7 | "github.com/tawesoft/golib/v2/dialog"
8 | "io"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | )
13 |
14 | func UnzipFile(zipFile string, destinationPath string, stripFirstFolder bool, progressMessage string) error {
15 | archive, err := zip.OpenReader(zipFile)
16 | if err != nil {
17 | return fmt.Errorf("error opening zipfile '%s': %v", zipFile, err)
18 | }
19 | defer archive.Close()
20 |
21 | pb := uiprogress.AddBar(len(archive.File)).AppendCompleted()
22 | pb.PrependFunc(func(b *uiprogress.Bar) string {
23 | return progressMessage
24 | })
25 |
26 | for _, f := range archive.File {
27 | var filePath string
28 | if stripFirstFolder {
29 | fNameRemovedFirstPath := strings.Split(f.Name, "/")[1:]
30 | if fNameRemovedFirstPath[0] == "" {
31 | pb.Incr()
32 | continue
33 | }
34 | filePath = filepath.Join(destinationPath, strings.Join(fNameRemovedFirstPath, string(os.PathSeparator)))
35 | } else {
36 | filePath = filepath.Join(destinationPath, f.Name)
37 | }
38 |
39 | if !strings.HasPrefix(filePath, filepath.Clean(destinationPath)+string(os.PathSeparator)) {
40 | return fmt.Errorf("invalid file path '%s' while extracting '%s' from zip '%s'", filePath, f.Name, zipFile)
41 | }
42 |
43 | if f.FileInfo().IsDir() {
44 | err := os.MkdirAll(filePath, os.ModePerm)
45 | if err != nil {
46 | return fmt.Errorf("error creating directory '%s' while extracting zip '%s': %v", filePath, zipFile, err)
47 | }
48 | pb.Incr()
49 | continue
50 | }
51 |
52 | if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
53 | if err != nil {
54 | return fmt.Errorf("error creating directory '%s' while extracting '%s' from zip '%s': %v", filepath.Dir(filePath), f.Name, zipFile, err)
55 | }
56 | }
57 |
58 | err = func() error {
59 | dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
60 | if err != nil {
61 | if strings.HasSuffix(filePath, "materials\\correction\\mp_rr_desertlands_mu1_hdr.raw_hdr") {
62 | return nil
63 | }
64 |
65 | fileInfo, fInfoErr := os.Stat(filePath)
66 | if fInfoErr != nil {
67 | return fmt.Errorf("error opening destination file while extracting '%s' from zip '%s': %v", filePath, zipFile, err)
68 | }
69 |
70 | fileMode := fileInfo.Mode()
71 | if fileMode != 292 {
72 | _ = dialog.Warning("Failed to unzip release, file is not read-only. Confirm torrent is no longer seeding and the launcher is not started")
73 | }
74 | return fmt.Errorf("error opening destination file(with permissions %s) while extracting '%s' from zip '%s': %v", fileInfo.Mode(), filePath, zipFile, err)
75 | }
76 | defer dstFile.Close()
77 |
78 | fileInArchive, err := f.Open()
79 | if err != nil {
80 | return fmt.Errorf("error opening file '%s' in zip '%s': %v", f.Name, zipFile, err)
81 | }
82 | defer fileInArchive.Close()
83 |
84 | if _, err := io.Copy(dstFile, fileInArchive); err != nil {
85 | return fmt.Errorf("error extracting file '%s' from zip '%s' to '%s': %v", f.Name, zipFile, dstFile.Name(), err)
86 | }
87 | pb.Incr()
88 | return nil
89 | }()
90 | if err != nil {
91 | return err
92 | }
93 | }
94 | return nil
95 | }
96 |
--------------------------------------------------------------------------------
/pkg/validation/validation.go:
--------------------------------------------------------------------------------
1 | package validation
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | func IsR5Folder(path string) (bool, error) {
10 | filesInDir, err := os.ReadDir(path)
11 | if err != nil {
12 | return false, fmt.Errorf("error while reading path '%s': %v", path, err)
13 | }
14 |
15 | for _, file := range filesInDir {
16 | if file.Name() == "r5apex.exe" {
17 | return true, nil
18 | }
19 | }
20 |
21 | return false, nil
22 | }
23 |
24 | func IsRunningInR5Folder() (bool, error) {
25 | path, err := os.Getwd()
26 | if err != nil {
27 | return false, fmt.Errorf("error retrieving current directory while validating r5Path: %v", err)
28 | }
29 |
30 | return IsR5Folder(path)
31 | }
32 |
33 | func IsLauncherFileLocked(path string) bool {
34 | launcherPath := filepath.Join(path, "launcher.exe")
35 | file, err := os.OpenFile(launcherPath, os.O_WRONLY, 0777)
36 | defer file.Close()
37 | if err != nil {
38 | if os.IsNotExist(err) {
39 | return false
40 | }
41 | return true
42 | }
43 |
44 | return false
45 | }
46 |
--------------------------------------------------------------------------------