Add a new Go client implementation for interacting with the IPAGo server. The client includes functionality for authentication, searching for apps, and downloading apps. The client is structured to handle errors and retries, and includes progress reporting for downloads. The client is added to the .gitignore file and the README.md file is updated with installation instructions. The server-side code is also updated to include the Content-Length header for download progress reporting.
112 lines
2.7 KiB
Go
112 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"ipa/IPAGo/models"
|
|
"ipa/IPAGo/wrapper"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
|
)
|
|
|
|
const (
|
|
version = "1.0.1"
|
|
)
|
|
|
|
func main() {
|
|
port := flag.Int("port", 8888, "Port to listen on")
|
|
flag.Parse()
|
|
|
|
mux := http.NewServeMux()
|
|
config := huma.DefaultConfig("IPAGo", version)
|
|
config.CreateHooks = nil
|
|
api := humago.New(mux, config)
|
|
|
|
huma.Get(
|
|
api, "/auth/login",
|
|
func(ctx context.Context, input *models.AuthInput) (*models.AuthOutput, error) {
|
|
output := &models.AuthOutput{}
|
|
output.Body.Success = wrapper.Auth(input.User, input.Pswd, input.Code)
|
|
return output, nil
|
|
})
|
|
|
|
huma.Get(
|
|
api, "/auth/info",
|
|
func(ctx context.Context, input *models.AuthInfoInput) (*models.AuthInfoOutput, error) {
|
|
output := &models.AuthInfoOutput{}
|
|
output.Body.Success = wrapper.AuthInfo(input.User, input.Pswd)
|
|
return output, nil
|
|
})
|
|
|
|
huma.Get(
|
|
api, "/search",
|
|
func(ctx context.Context, input *models.SearchInput) (*models.SearchOutput, error) {
|
|
output := &models.SearchOutput{}
|
|
output.Body.Apps = []wrapper.App{}
|
|
output.Body.Success = false
|
|
|
|
if input.Limit == 0 {
|
|
input.Limit = 1
|
|
}
|
|
if input.Query == "" {
|
|
return output, nil
|
|
}
|
|
|
|
apps, err := wrapper.Search(input.Query, input.Limit)
|
|
if err != nil {
|
|
return output, nil
|
|
}
|
|
|
|
output.Body.Success = true
|
|
output.Body.Apps = apps
|
|
return output, nil
|
|
})
|
|
|
|
huma.Get(
|
|
api, "/download",
|
|
func(ctx context.Context, input *models.DownloadInput) (*models.DownloadOutput, error) {
|
|
outputPath := filepath.Join(os.TempDir(), fmt.Sprintf("%s.ipa", time.Now().Format("20060102150405")))
|
|
output := &models.DownloadOutput{}
|
|
if input.BundleID == "" {
|
|
return nil, errors.New("bundleID is empty")
|
|
}
|
|
success := wrapper.Download(input.BundleID, outputPath)
|
|
if !success {
|
|
return nil, errors.New("failed to download")
|
|
}
|
|
output.ContentDisposition = fmt.Sprintf("attachment; filename=%s", filepath.Base(outputPath))
|
|
output.ContentType = "application/octet-stream"
|
|
output.Body = func(hctx huma.Context) {
|
|
file, err := os.Open(outputPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
file.Close()
|
|
os.Remove(outputPath)
|
|
fmt.Println("File removed after download")
|
|
}()
|
|
|
|
// Set Content-Length header so the client can show progress
|
|
if info, err := file.Stat(); err == nil {
|
|
hctx.SetHeader("Content-Length", fmt.Sprintf("%d", info.Size()))
|
|
}
|
|
|
|
_, _ = io.Copy(hctx.BodyWriter(), file)
|
|
}
|
|
return output, nil
|
|
})
|
|
|
|
addr := fmt.Sprintf(":%d", *port)
|
|
fmt.Printf("Server is running on %s\n", addr)
|
|
http.ListenAndServe(addr, mux)
|
|
}
|