🚀 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:
2026-03-10 03:41:41 +03:00
commit d763b0b652
13 changed files with 565 additions and 0 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
.git
Dockerfile
docker-compose.yml

227
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,6 @@
package constants
const (
IPAToolPath = "ipatool"
GlobalFlags = "--non-interactive --format json"
)

36
docker/Dockerfile Normal file
View 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
View 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

Binary file not shown.

5
go.mod Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}