├── .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 | --------------------------------------------------------------------------------