package qbittorrent import ( "bytes" "context" "errors" "fmt" "io" "log/slog" "net/http" "os" "os/exec" "path" "runtime" "time" "github.com/google/go-github/v63/github" "golang.org/x/sys/cpu" ) const ( repoOwner = "userdocs" repoName = "qbittorrent-nox-static" ) func runQBittorrent(binPath string, profileDir string, port int, stdout, stderr io.Writer) (*os.Process, error) { err := os.Chmod(binPath, 0755) if err != nil { return nil, err } cmd := exec.Command(binPath, fmt.Sprintf("--profile=%s", profileDir), fmt.Sprintf("--webui-port=%d", port)) cmd.Stdin = bytes.NewReader([]byte("y\n")) cmd.Stdout = stdout cmd.Stderr = stderr err = cmd.Start() if err != nil { return nil, err } return cmd.Process, nil } func downloadLatestQbitRelease(ctx context.Context, binPath string) error { client := github.NewClient(nil) rel, _, err := client.Repositories.GetLatestRelease(ctx, repoOwner, repoName) if err != nil { return err } arch := "" switch runtime.GOARCH { case "amd64": arch = "x86_64" case "arm": arch = "armhf" // this is a safe version, go does not distinguish between armv6 and armv7 if cpu.ARM.HasNEON { arch = "armv7" } case "arm64": arch = "aarch64" } if arch == "" { return errors.New("unsupported architecture") } binName := arch + "-qbittorrent-nox" var targetRelease *github.ReleaseAsset for _, v := range rel.Assets { if v.GetName() == binName { targetRelease = v break } } if targetRelease == nil { return fmt.Errorf("target asset %s not found", binName) } downloadUrl := targetRelease.GetBrowserDownloadURL() if downloadUrl == "" { return errors.New("download url is empty") } err = os.MkdirAll(path.Dir(binPath), 0755) if err != nil { return err } slog.InfoContext(ctx, "downloading latest qbittorrent-nox release", slog.String("url", downloadUrl)) return downloadFile(ctx, binPath, downloadUrl) } func downloadFile(ctx context.Context, filepath string, webUrl string) error { if stat, err := os.Stat(filepath); err == nil { resp, err := http.Head(webUrl) if err != nil { return err } defer resp.Body.Close() var lastModified time.Time lastModifiedHeader := resp.Header.Get("Last-Modified") if lastModifiedHeader != "" { lastModified, err = time.Parse(http.TimeFormat, lastModifiedHeader) if err != nil { return err } } if resp.ContentLength == stat.Size() && lastModified.Before(stat.ModTime()) { slog.InfoContext(ctx, "there is already newest version of the file", slog.String("filepath", filepath)) return nil } } // Create the file out, err := os.Create(filepath) if err != nil { return err } defer out.Close() req, err := http.NewRequestWithContext(ctx, http.MethodGet, webUrl, nil) if err != nil { return err } // Get the data resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() // Check server response if resp.StatusCode != http.StatusOK { return fmt.Errorf("bad status: %s", resp.Status) } // Writer the body to file _, err = io.Copy(out, resp.Body) if err != nil { return err } return nil }