🚀 chore: add initial project setup with docker, go modules, and basic api structure
Add docker configuration for building and running the application, including a dockerfile and docker-compose.yml. Set up go modules with dependencies and create the basic api structure with main.go, models, and wrapper files. Include .dockerignore and .gitignore files to manage ignored files and directories. Add constants for ipatool path and global flags.
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
.git
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
227
.gitignore
vendored
Normal file
227
.gitignore
vendored
Normal file
@ -0,0 +1,227 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
6
constants/constants.go
Normal file
6
constants/constants.go
Normal file
@ -0,0 +1,6 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
IPAToolPath = "ipatool"
|
||||
GlobalFlags = "--non-interactive --format json"
|
||||
)
|
||||
36
docker/Dockerfile
Normal file
36
docker/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
||||
# --- ЭТАП 1: Сборка ---
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файлы зависимостей
|
||||
# Это делается отдельно, чтобы Docker кэшировал слои с модулями
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Собираем бинарный файл
|
||||
# CGO_ENABLED=0 нужен для статической линковки (чтобы работало в пустом образе)
|
||||
# GOOS=linux гарантирует сборку под Linux
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server main.go
|
||||
|
||||
# --- ЭТАП 2: Запуск ---
|
||||
FROM alpine:latest
|
||||
|
||||
# Устанавливаем часовой пояс и корневые сертификаты (важно для HTTPS запросов)
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем только скомпилированный файл из предыдущего этапа
|
||||
COPY --from=builder /app/server .
|
||||
COPY --from=builder /app/ipatool .
|
||||
|
||||
# Открываем порт (тот же, что в коде Huma)
|
||||
EXPOSE 8888
|
||||
|
||||
# Запускаем приложение
|
||||
CMD ["./server"]
|
||||
20
docker/docker-compose.yml
Normal file
20
docker/docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: "3.8"
|
||||
name: ${CONTAINER_NAME}
|
||||
|
||||
services:
|
||||
ipago:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
container_name: ipago-${CONTAINER_NAME}
|
||||
hostname: ipago
|
||||
environment:
|
||||
- CONTAINER_NAME=${CONTAINER_NAME}
|
||||
- PORT=${PORT}
|
||||
ports:
|
||||
- "${PORT}:8000"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ipago
|
||||
BIN
docker/ipatool
Executable file
BIN
docker/ipatool
Executable file
Binary file not shown.
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module ipa/IPAGo
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/danielgtaylor/huma/v2 v2.37.2
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/danielgtaylor/huma/v2 v2.37.2 h1:Nf9vjy2sxBJFaupPlthXL/Hy2+LurfVbaKHmCMEI7xE=
|
||||
github.com/danielgtaylor/huma/v2 v2.37.2/go.mod h1:95S04G/lExFRYlBkKaBaZm9lVmxRmqX9f2CgoOZ11AM=
|
||||
101
main.go
Normal file
101
main.go
Normal file
@ -0,0 +1,101 @@
|
||||
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 file.Close()
|
||||
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)
|
||||
}
|
||||
24
models/auth.go
Normal file
24
models/auth.go
Normal file
@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
type AuthInput struct {
|
||||
User string `query:"user" doc:"Имя пользователя" minLength:"3" example:"Ivan"`
|
||||
Pswd string `query:"pswd" doc:"Пароль" minLength:"8" example:"12345678"`
|
||||
Code string `query:"code" doc:"Код двухфакторной авторизации" minLength:"6" example:"123456"`
|
||||
}
|
||||
|
||||
type AuthOutput struct {
|
||||
Body struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
}
|
||||
|
||||
type AuthInfoInput struct {
|
||||
User string `query:"user" doc:"Имя пользователя" minLength:"3" example:"Ivan"`
|
||||
Pswd string `query:"pswd" doc:"Пароль" minLength:"8" example:"12345678"`
|
||||
}
|
||||
|
||||
type AuthInfoOutput struct {
|
||||
Body struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
}
|
||||
13
models/download.go
Normal file
13
models/download.go
Normal file
@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import "github.com/danielgtaylor/huma/v2"
|
||||
|
||||
type DownloadInput struct {
|
||||
BundleID string `query:"bundleID" doc:"ID приложения" minLength:"3" example:"com.zhiliaoapp.musically"`
|
||||
}
|
||||
|
||||
type DownloadOutput struct {
|
||||
Body func(huma.Context)
|
||||
ContentDisposition string `header:"Content-Disposition"`
|
||||
ContentType string `header:"Content-Type"`
|
||||
}
|
||||
15
models/search.go
Normal file
15
models/search.go
Normal file
@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
import "ipa/IPAGo/wrapper"
|
||||
|
||||
type SearchInput struct {
|
||||
Query string `query:"query" doc:"Поисковый запрос" minLength:"3" example:"TikTok"`
|
||||
Limit int `query:"limit" doc:"Количество результатов" min:"1" max:"100" example:"10"`
|
||||
}
|
||||
|
||||
type SearchOutput struct {
|
||||
Body struct {
|
||||
Success bool `json:"success"`
|
||||
Apps []wrapper.App `json:"apps"`
|
||||
}
|
||||
}
|
||||
113
wrapper/wrapper.go
Normal file
113
wrapper/wrapper.go
Normal file
@ -0,0 +1,113 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ipa/IPAGo/constants"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Auth(user, pswd, code string) bool {
|
||||
command := ""
|
||||
if code == "" {
|
||||
command = fmt.Sprintf("auth login -e %s -p %s", user, pswd)
|
||||
} else {
|
||||
command = fmt.Sprintf("auth login -e %s -p %s --auth-code %s", user, pswd, code)
|
||||
}
|
||||
command += " " + constants.GlobalFlags
|
||||
out, err := run(command)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
return false
|
||||
}
|
||||
result, ok := out["success"].(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func AuthInfo(user, pswd string) bool {
|
||||
command := fmt.Sprintf("%s auth info", constants.GlobalFlags)
|
||||
out, err := run(command)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
result, ok := out["success"].(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type App struct {
|
||||
BundleID string `json:"bundleID"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func Search(query string, limit int) ([]App, error) {
|
||||
command := fmt.Sprintf("%s search %s -l %d", constants.GlobalFlags, query, limit)
|
||||
out, err := run(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apps, ok := out["apps"].([]any)
|
||||
fmt.Println("apps", apps)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get apps")
|
||||
}
|
||||
appsList := make([]App, len(apps))
|
||||
for i, app := range apps {
|
||||
app, ok := app.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get app")
|
||||
}
|
||||
appsList[i] = App{
|
||||
BundleID: app["bundleID"].(string),
|
||||
Version: app["version"].(string),
|
||||
}
|
||||
}
|
||||
return appsList, nil
|
||||
}
|
||||
|
||||
func Download(bundleID string, outputPath string) bool {
|
||||
command := fmt.Sprintf(
|
||||
"download -b %s -o %s %s",
|
||||
bundleID, outputPath, constants.GlobalFlags,
|
||||
)
|
||||
out, err := run(command)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
result, ok := out["success"].(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func run(command string) (map[string]any, error) {
|
||||
fmt.Println("command", command)
|
||||
cmd := exec.Command(constants.IPAToolPath, strings.Split(command, " ")...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Println(string(out))
|
||||
return nil, err
|
||||
}
|
||||
if cmd.ProcessState.ExitCode() != 0 {
|
||||
fmt.Println(string(out))
|
||||
return nil, fmt.Errorf(
|
||||
"command failed with exit code %d: %s",
|
||||
cmd.ProcessState.ExitCode(), string(out),
|
||||
)
|
||||
}
|
||||
var result map[string]any
|
||||
err = json.Unmarshal(out, &result)
|
||||
if err != nil {
|
||||
fmt.Println(string(out))
|
||||
return nil, fmt.Errorf("failed to unmarshal output: %s", err)
|
||||
}
|
||||
fmt.Println("result", result)
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user