feat: add a platform-aware apps download page
ci / api / api (push) Successful in 48s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m10s
ci / db / db (push) Successful in 57s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m1s
ci / desktop / rust (desktop proxy) (push) Successful in 5m37s
ci / ts / ts (packages + web) (push) Successful in 4m10s
release / release (версия и заметки) (push) Successful in 11s
release / android (APK в релиз) (push) Successful in 4m22s

Serve desktop and Android builds from the latest Gitea release on /download, and fix the desktop compile and CI flakes that blocked v1.3.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mrleo1nidandCursor committed 2026-10-22 00:10:03 +03:00
1 parent efe78d7e07
commit 597067689a
30 files changed
+1180 -395

No files matched your search

+500 -270
View File
@@ -28,6 +28,14 @@ const (
BearerAuthScopes = "bearerAuth.Scopes"
)
// Defines values for AppClient.
const (
AppClientAndroid AppClient = "android"
AppClientLinux AppClient = "linux"
AppClientMacos AppClient = "macos"
AppClientWindows AppClient = "windows"
)
// Defines values for AuthTokensTokenType.
const (
Bearer AuthTokensTokenType = "Bearer"
@@ -35,9 +43,9 @@ const (
// Defines values for ClientType.
const (
Android ClientType = "android"
Desktop ClientType = "desktop"
Web ClientType = "web"
ClientTypeAndroid ClientType = "android"
ClientTypeDesktop ClientType = "desktop"
ClientTypeWeb ClientType = "web"
)
// Defines values for HealthStatusStatus.
@@ -319,6 +327,26 @@ type AndroidRelease struct {
VersionCode int `json:"version_code"`
}
// AppClient defines model for AppClient.
type AppClient string
// AppClientFile defines model for AppClientFile.
type AppClientFile struct {
Client AppClient `json:"client"`
DownloadUrl string `json:"download_url"`
Filename string `json:"filename"`
Sha256 *string `json:"sha256,omitempty"`
Size int64 `json:"size"`
}
// AppClients defines model for AppClients.
type AppClients struct {
Clients []AppClientFile `json:"clients"`
NotesMd string `json:"notes_md"`
Version string `json:"version"`
VersionCode int `json:"version_code"`
}
// Artist defines model for Artist.
type Artist struct {
AlbumCount int `json:"album_count"`
@@ -1688,6 +1716,12 @@ type ServerInterface interface {
// Последняя версия Android-приложения (из релизов Gitea через сервер)
// (GET /api/v1/app/android/latest)
GetAndroidLatest(w http.ResponseWriter, r *http.Request)
// Клиенты последнего релиза (Android и десктоп)
// (GET /api/v1/app/clients)
GetAppClients(w http.ResponseWriter, r *http.Request)
// Файл клиента (кэшируется на сервере, поддерживает Range)
// (GET /api/v1/app/download/{client}/{version})
DownloadAppClient(w http.ResponseWriter, r *http.Request, client AppClient, version string)
// Исполнитель
// (GET /api/v1/artists/{id})
GetArtist(w http.ResponseWriter, r *http.Request, id EntityId)
@@ -2054,6 +2088,18 @@ func (_ Unimplemented) GetAndroidLatest(w http.ResponseWriter, r *http.Request)
w.WriteHeader(http.StatusNotImplemented)
}
// Клиенты последнего релиза (Android и десктоп)
// (GET /api/v1/app/clients)
func (_ Unimplemented) GetAppClients(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Файл клиента (кэшируется на сервере, поддерживает Range)
// (GET /api/v1/app/download/{client}/{version})
func (_ Unimplemented) DownloadAppClient(w http.ResponseWriter, r *http.Request, client AppClient, version string) {
w.WriteHeader(http.StatusNotImplemented)
}
// Исполнитель
// (GET /api/v1/artists/{id})
func (_ Unimplemented) GetArtist(w http.ResponseWriter, r *http.Request, id EntityId) {
@@ -3438,6 +3484,54 @@ func (siw *ServerInterfaceWrapper) GetAndroidLatest(w http.ResponseWriter, r *ht
handler.ServeHTTP(w, r)
}
// GetAppClients operation middleware
func (siw *ServerInterfaceWrapper) GetAppClients(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetAppClients(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// DownloadAppClient operation middleware
func (siw *ServerInterfaceWrapper) DownloadAppClient(w http.ResponseWriter, r *http.Request) {
var err error
// ------------- Path parameter "client" -------------
var client AppClient
err = runtime.BindStyledParameterWithOptions("simple", "client", chi.URLParam(r, "client"), &client, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "client", Err: err})
return
}
// ------------- Path parameter "version" -------------
var version string
err = runtime.BindStyledParameterWithOptions("simple", "version", chi.URLParam(r, "version"), &version, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "version", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.DownloadAppClient(w, r, client, version)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetArtist operation middleware
func (siw *ServerInterfaceWrapper) GetArtist(w http.ResponseWriter, r *http.Request) {
@@ -5074,6 +5168,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/api/v1/app/android/latest", wrapper.GetAndroidLatest)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/api/v1/app/clients", wrapper.GetAppClients)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/api/v1/app/download/{client}/{version}", wrapper.DownloadAppClient)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/api/v1/artists/{id}", wrapper.GetArtist)
})
@@ -6269,6 +6369,74 @@ func (response GetAndroidLatestdefaultApplicationProblemPlusJSONResponse) VisitG
return json.NewEncoder(w).Encode(response.Body)
}
type GetAppClientsRequestObject struct {
}
type GetAppClientsResponseObject interface {
VisitGetAppClientsResponse(w http.ResponseWriter) error
}
type GetAppClients200JSONResponse AppClients
func (response GetAppClients200JSONResponse) VisitGetAppClientsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetAppClientsdefaultApplicationProblemPlusJSONResponse struct {
Body Problem
StatusCode int
}
func (response GetAppClientsdefaultApplicationProblemPlusJSONResponse) VisitGetAppClientsResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type DownloadAppClientRequestObject struct {
Client AppClient `json:"client"`
Version string `json:"version"`
}
type DownloadAppClientResponseObject interface {
VisitDownloadAppClientResponse(w http.ResponseWriter) error
}
type DownloadAppClient200ApplicationoctetStreamResponse struct {
Body io.Reader
ContentLength int64
}
func (response DownloadAppClient200ApplicationoctetStreamResponse) VisitDownloadAppClientResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/octet-stream")
if response.ContentLength != 0 {
w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength))
}
w.WriteHeader(200)
if closer, ok := response.Body.(io.ReadCloser); ok {
defer closer.Close()
}
_, err := io.Copy(w, response.Body)
return err
}
type DownloadAppClientdefaultApplicationProblemPlusJSONResponse struct {
Body Problem
StatusCode int
}
func (response DownloadAppClientdefaultApplicationProblemPlusJSONResponse) VisitDownloadAppClientResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(response.StatusCode)
return json.NewEncoder(w).Encode(response.Body)
}
type GetArtistRequestObject struct {
Id EntityId `json:"id"`
}
@@ -7816,6 +7984,12 @@ type StrictServerInterface interface {
// Последняя версия Android-приложения (из релизов Gitea через сервер)
// (GET /api/v1/app/android/latest)
GetAndroidLatest(ctx context.Context, request GetAndroidLatestRequestObject) (GetAndroidLatestResponseObject, error)
// Клиенты последнего релиза (Android и десктоп)
// (GET /api/v1/app/clients)
GetAppClients(ctx context.Context, request GetAppClientsRequestObject) (GetAppClientsResponseObject, error)
// Файл клиента (кэшируется на сервере, поддерживает Range)
// (GET /api/v1/app/download/{client}/{version})
DownloadAppClient(ctx context.Context, request DownloadAppClientRequestObject) (DownloadAppClientResponseObject, error)
// Исполнитель
// (GET /api/v1/artists/{id})
GetArtist(ctx context.Context, request GetArtistRequestObject) (GetArtistResponseObject, error)
@@ -8972,6 +9146,57 @@ func (sh *strictHandler) GetAndroidLatest(w http.ResponseWriter, r *http.Request
}
}
// GetAppClients operation middleware
func (sh *strictHandler) GetAppClients(w http.ResponseWriter, r *http.Request) {
var request GetAppClientsRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.GetAppClients(ctx, request.(GetAppClientsRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetAppClients")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(GetAppClientsResponseObject); ok {
if err := validResponse.VisitGetAppClientsResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// DownloadAppClient operation middleware
func (sh *strictHandler) DownloadAppClient(w http.ResponseWriter, r *http.Request, client AppClient, version string) {
var request DownloadAppClientRequestObject
request.Client = client
request.Version = version
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.DownloadAppClient(ctx, request.(DownloadAppClientRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "DownloadAppClient")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(DownloadAppClientResponseObject); ok {
if err := validResponse.VisitDownloadAppClientResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// GetArtist operation middleware
func (sh *strictHandler) GetArtist(w http.ResponseWriter, r *http.Request, id EntityId) {
var request GetArtistRequestObject
@@ -10343,273 +10568,278 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"zW9kz1wBAA==",
"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",
"AQA=",
}
// GetSwagger returns the content of the embedded swagger specification file
+212 -52
View File
@@ -1,5 +1,5 @@
// Package appupdate — самообновление Android-приложения через сервер (docs/mobile.md#самообновление):
// сервер читает последний релиз Gitea своим токеном, кэширует ответ и APK.
// Package appupdate — раздача клиентов через сервер (docs/mobile.md#самообновление):
// сервер читает последний релиз Gitea своим токеном, кэширует ответ и файлы.
package appupdate
import (
@@ -25,7 +25,7 @@ import (
"pipu/api/internal/platform/httpx"
)
// CodeUnavailable — обновления не настроены или релиза с APK нет.
// CodeUnavailable — обновления не настроены или релиза с клиентами нет.
const CodeUnavailable = "app.update_unavailable"
const cacheTTL = 10 * time.Minute
@@ -73,14 +73,22 @@ type release struct {
Assets []asset `json:"assets"`
}
// Latest — сведения о последней версии.
type clientFile struct {
Kind, Name, URL, ChecksumURL, SHA256 string
Size int64
}
// Latest — сведения о последней версии и файлах клиентов.
type Latest struct {
Version string
VersionCode int
Notes string
Size int64
SHA256 string
apkURL string
Files map[string]clientFile
}
func (l *Latest) android() (clientFile, bool) {
f, ok := l.Files["android"]
return f, ok && f.SHA256 != ""
}
// Handler реализует app-часть gen.StrictServerInterface.
@@ -92,7 +100,7 @@ type Handler struct {
mu sync.Mutex
cached *Latest
cachedAt time.Time
dl sync.Mutex // одна загрузка APK за раз
dl sync.Mutex
}
// NewHandler создаёт обработчик.
@@ -100,7 +108,6 @@ func NewHandler(cfg Config, log *slog.Logger) *Handler {
return &Handler{cfg: cfg, client: &http.Client{Timeout: 10 * time.Minute}, log: log}
}
// configured — адрес и репозиторий заданы, и это не заглушка из .env.example.
func (h *Handler) configured() bool {
return h.cfg.GiteaURL != "" && h.cfg.Repo != "" &&
!strings.Contains(h.cfg.GiteaURL, "example.") && !strings.HasPrefix(h.cfg.Repo, "owner/")
@@ -125,7 +132,70 @@ func (h *Handler) get(ctx context.Context, url string) (*http.Response, error) {
return res, nil
}
// latest — последний релиз с APK (кэш на 10 минут).
func classifyAsset(name string) string {
n := strings.ToLower(name)
if strings.HasSuffix(n, ".sha256") {
return ""
}
switch {
case strings.HasSuffix(n, ".apk"):
return "android"
case strings.HasSuffix(n, ".exe"), strings.HasSuffix(n, ".msi"):
return "windows"
case strings.HasSuffix(n, ".appimage"), strings.HasSuffix(n, ".deb"), strings.HasSuffix(n, ".rpm"):
return "linux"
case strings.HasSuffix(n, ".dmg"), strings.Contains(n, ".app.tar"):
return "macos"
default:
return ""
}
}
func assetRank(kind, name string) int {
n := strings.ToLower(name)
switch kind {
case "windows":
if strings.Contains(n, "setup") {
return 2
}
return 1
case "linux":
switch {
case strings.HasSuffix(n, ".appimage"):
return 3
case strings.HasSuffix(n, ".deb"):
return 2
default:
return 1
}
default:
return 1
}
}
func pickClientFiles(assets []asset) map[string]clientFile {
sums := map[string]string{}
for _, a := range assets {
if strings.HasSuffix(strings.ToLower(a.Name), ".sha256") {
sums[strings.TrimSuffix(a.Name, ".sha256")] = a.URL
sums[strings.TrimSuffix(a.Name, ".SHA256")] = a.URL
}
}
out := map[string]clientFile{}
for _, a := range assets {
kind := classifyAsset(a.Name)
if kind == "" {
continue
}
cur, ok := out[kind]
if ok && assetRank(kind, a.Name) <= assetRank(kind, cur.Name) {
continue
}
out[kind] = clientFile{Kind: kind, Name: a.Name, URL: a.URL, Size: a.Size, ChecksumURL: sums[a.Name]}
}
return out
}
func (h *Handler) latest(ctx context.Context) (*Latest, error) {
if !h.configured() {
return nil, apperr.NotFound(CodeUnavailable, "gitea is not configured")
@@ -142,7 +212,7 @@ func (h *Handler) latest(ctx context.Context) (*Latest, error) {
if err != nil {
h.log.WarnContext(ctx, "app update: gitea unavailable", "err", err)
if h.cached != nil {
return h.cached, nil // устаревший ответ лучше, чем никакого
return h.cached, nil
}
return nil, apperr.NotFound(CodeUnavailable, "release is unavailable")
}
@@ -156,35 +226,77 @@ func (h *Handler) latest(ctx context.Context) (*Latest, error) {
if err != nil {
return nil, apperr.NotFound(CodeUnavailable, err.Error())
}
name := "pipu-music-" + version + ".apk"
var apk, sum *asset
for i := range rel.Assets {
switch rel.Assets[i].Name {
case name:
apk = &rel.Assets[i]
case name + ".sha256":
sum = &rel.Assets[i]
files := pickClientFiles(rel.Assets)
if len(files) == 0 {
return nil, apperr.NotFound(CodeUnavailable, "release has no client builds")
}
if apk, ok := files["android"]; ok {
if apk.ChecksumURL == "" {
delete(files, "android")
} else if digest, err := h.readDigest(ctx, apk.ChecksumURL); err != nil {
h.log.WarnContext(ctx, "app update: apk checksum unavailable", "err", err)
delete(files, "android")
} else {
apk.SHA256 = digest
files["android"] = apk
}
if len(files) == 0 {
return nil, apperr.NotFound(CodeUnavailable, "release has no APK yet")
}
}
if apk == nil || sum == nil {
return nil, apperr.NotFound(CodeUnavailable, "release has no APK yet")
}
sres, err := h.get(ctx, sum.URL)
h.cached = &Latest{Version: version, VersionCode: code, Notes: rel.Body, Files: files}
h.cachedAt = time.Now()
return h.cached, nil
}
func (h *Handler) readDigest(ctx context.Context, url string) (string, error) {
sres, err := h.get(ctx, url)
if err != nil {
return nil, apperr.NotFound(CodeUnavailable, "checksum is unavailable")
return "", err
}
raw, err := io.ReadAll(io.LimitReader(sres.Body, 256))
_ = sres.Body.Close()
if err != nil {
return nil, err
return "", err
}
digest := strings.ToLower(strings.Fields(string(raw) + " x")[0])
if len(digest) != 64 {
return nil, apperr.NotFound(CodeUnavailable, "malformed checksum")
return "", fmt.Errorf("malformed checksum")
}
h.cached = &Latest{Version: version, VersionCode: code, Notes: rel.Body, Size: apk.Size, SHA256: digest, apkURL: apk.URL}
h.cachedAt = time.Now()
return h.cached, nil
return digest, nil
}
func (h *Handler) fileURL(kind, version string) string {
return "/api/v1/app/download/" + kind + "/" + version
}
// GetAppClients реализует gen.StrictServerInterface.
func (h *Handler) GetAppClients(ctx context.Context, _ gen.GetAppClientsRequestObject) (gen.GetAppClientsResponseObject, error) {
l, err := h.latest(ctx)
if err != nil {
return nil, err
}
order := []string{"windows", "macos", "linux", "android"}
clients := make([]gen.AppClientFile, 0, len(l.Files))
for _, kind := range order {
f, ok := l.Files[kind]
if !ok {
continue
}
item := gen.AppClientFile{
Client: gen.AppClient(kind),
Filename: f.Name,
Size: f.Size,
DownloadUrl: h.fileURL(kind, l.Version),
}
if f.SHA256 != "" {
item.Sha256 = &f.SHA256
}
clients = append(clients, item)
}
return gen.GetAppClients200JSONResponse{
Version: l.Version, VersionCode: l.VersionCode, NotesMd: l.Notes, Clients: clients,
}, nil
}
// GetAndroidLatest реализует gen.StrictServerInterface.
@@ -193,15 +305,18 @@ func (h *Handler) GetAndroidLatest(ctx context.Context, _ gen.GetAndroidLatestRe
if err != nil {
return nil, err
}
f, ok := l.android()
if !ok {
return nil, apperr.NotFound(CodeUnavailable, "release has no APK yet")
}
return gen.GetAndroidLatest200JSONResponse{
Version: l.Version, VersionCode: l.VersionCode, NotesMd: l.Notes, Size: l.Size, Sha256: l.SHA256,
Version: l.Version, VersionCode: l.VersionCode, NotesMd: l.Notes, Size: f.Size, Sha256: f.SHA256,
DownloadUrl: "/api/v1/app/android/download/" + l.Version, MinSupported: h.cfg.MinSupported,
}, nil
}
// ensure скачивает APK в кэш (если его там нет) и сверяет SHA-256 из релиза.
func (h *Handler) ensure(ctx context.Context, l *Latest) (string, error) {
dst := filepath.Join(h.cfg.Dir, "pipu-music-"+l.Version+".apk")
func (h *Handler) ensure(ctx context.Context, f clientFile) (string, error) {
dst := filepath.Join(h.cfg.Dir, f.Name)
if _, err := os.Stat(dst); err == nil {
return dst, nil
}
@@ -213,12 +328,12 @@ func (h *Handler) ensure(ctx context.Context, l *Latest) (string, error) {
if err := os.MkdirAll(h.cfg.Dir, 0o755); err != nil {
return "", err
}
res, err := h.get(ctx, l.apkURL)
res, err := h.get(ctx, f.URL)
if err != nil {
return "", err
}
defer func() { _ = res.Body.Close() }()
tmp, err := os.CreateTemp(h.cfg.Dir, ".apk-*")
tmp, err := os.CreateTemp(h.cfg.Dir, ".dl-*")
if err != nil {
return "", err
}
@@ -231,26 +346,53 @@ func (h *Handler) ensure(ctx context.Context, l *Latest) (string, error) {
if err := tmp.Close(); err != nil {
return "", err
}
if got := hex.EncodeToString(hash.Sum(nil)); got != l.SHA256 {
return "", fmt.Errorf("appupdate: checksum mismatch: %s != %s", got, l.SHA256)
if f.SHA256 != "" {
if got := hex.EncodeToString(hash.Sum(nil)); got != f.SHA256 {
return "", fmt.Errorf("appupdate: checksum mismatch: %s != %s", got, f.SHA256)
}
}
if err := os.Rename(tmp.Name(), dst); err != nil {
return "", err
}
h.cleanup(dst)
h.cleanup(f.Name)
return dst, nil
}
// cleanup оставляет в кэше только текущий APK.
func (h *Handler) cleanup(keep string) {
matches, _ := filepath.Glob(filepath.Join(h.cfg.Dir, "pipu-music-*.apk"))
func (h *Handler) cleanup(keepName string) {
matches, _ := filepath.Glob(filepath.Join(h.cfg.Dir, "pipu-music-*"))
ver := versionFromName(keepName)
for _, m := range matches {
if m != keep {
base := filepath.Base(m)
if ver != "" && strings.Contains(base, ver) {
continue
}
if base != keepName {
_ = os.Remove(m)
}
}
}
func versionFromName(name string) string {
m := regexp.MustCompile(`(\d+\.\d+\.\d+(?:-rc\.\d+)?)`).FindStringSubmatch(name)
if m == nil {
return ""
}
return m[1]
}
func (h *Handler) serveFile(ctx context.Context, l *Latest, kind string) (fileResponse, error) {
f, ok := l.Files[kind]
if !ok || (kind == "android" && f.SHA256 == "") {
return fileResponse{}, apperr.NotFound(CodeUnavailable, "client build is unavailable")
}
path, err := h.ensure(ctx, f)
if err != nil {
h.log.ErrorContext(ctx, "app update: download failed", "err", err, "client", kind)
return fileResponse{}, apperr.New(http.StatusBadGateway, CodeUnavailable, "could not fetch the file")
}
return fileResponse{path: path, name: f.Name, req: httpx.RequestFrom(ctx)}, nil
}
// DownloadAndroidApk реализует gen.StrictServerInterface. Отдаётся только последняя версия.
func (h *Handler) DownloadAndroidApk(ctx context.Context, req gen.DownloadAndroidApkRequestObject) (gen.DownloadAndroidApkResponseObject, error) {
l, err := h.latest(ctx)
@@ -260,22 +402,36 @@ func (h *Handler) DownloadAndroidApk(ctx context.Context, req gen.DownloadAndroi
if req.Version != l.Version {
return nil, apperr.NotFound(CodeUnavailable, "only the latest version is available")
}
path, err := h.ensure(ctx, l)
if err != nil {
h.log.ErrorContext(ctx, "app update: apk download failed", "err", err)
return nil, apperr.New(http.StatusBadGateway, CodeUnavailable, "could not fetch the APK")
}
return apkResponse{path: path, name: filepath.Base(path), req: httpx.RequestFrom(ctx)}, nil
return h.serveFile(ctx, l, "android")
}
type apkResponse struct {
// DownloadAppClient реализует gen.StrictServerInterface.
func (h *Handler) DownloadAppClient(ctx context.Context, req gen.DownloadAppClientRequestObject) (gen.DownloadAppClientResponseObject, error) {
l, err := h.latest(ctx)
if err != nil {
return nil, err
}
if req.Version != l.Version {
return nil, apperr.NotFound(CodeUnavailable, "only the latest version is available")
}
return h.serveFile(ctx, l, string(req.Client))
}
type fileResponse struct {
path string
name string
req *http.Request
}
// VisitDownloadAndroidApkResponse реализует gen.DownloadAndroidApkResponseObject (Range — для докачки).
func (a apkResponse) VisitDownloadAndroidApkResponse(w http.ResponseWriter) error {
func (a fileResponse) VisitDownloadAndroidApkResponse(w http.ResponseWriter) error {
return a.write(w)
}
func (a fileResponse) VisitDownloadAppClientResponse(w http.ResponseWriter) error {
return a.write(w)
}
func (a fileResponse) write(w http.ResponseWriter) error {
f, err := os.Open(a.path)
if err != nil {
return err
@@ -288,7 +444,11 @@ func (a apkResponse) VisitDownloadAndroidApkResponse(w http.ResponseWriter) erro
if a.req == nil {
return errors.New("appupdate: request missing in context")
}
w.Header().Set("Content-Type", "application/vnd.android.package-archive")
ctype := "application/octet-stream"
if strings.HasSuffix(strings.ToLower(a.name), ".apk") {
ctype = "application/vnd.android.package-archive"
}
w.Header().Set("Content-Type", ctype)
w.Header().Set("Content-Disposition", `attachment; filename="`+a.name+`"`)
http.ServeContent(w, a.req, a.name, info.ModTime(), f)
return nil
@@ -16,6 +16,22 @@ import (
"github.com/stretchr/testify/require"
)
func TestPickClientFiles(t *testing.T) {
files := pickClientFiles([]asset{
{Name: "pipu-music-1.3.0.apk", Size: 10},
{Name: "pipu-music-1.3.0.apk.sha256", Size: 65, URL: "https://gitea/sum"},
{Name: "pipu-music_1.3.0_x64-setup.exe", Size: 20},
{Name: "pipu-music_1.3.0_amd64.deb", Size: 11},
{Name: "pipu-music_1.3.0_amd64.AppImage", Size: 30},
{Name: "pipu-music_1.3.0_aarch64.dmg", Size: 40},
})
assert.Equal(t, "pipu-music-1.3.0.apk", files["android"].Name)
assert.Equal(t, "https://gitea/sum", files["android"].ChecksumURL)
assert.Equal(t, "pipu-music_1.3.0_x64-setup.exe", files["windows"].Name)
assert.Equal(t, "pipu-music_1.3.0_amd64.AppImage", files["linux"].Name)
assert.Equal(t, "pipu-music_1.3.0_aarch64.dmg", files["macos"].Name)
}
func TestVersionCode(t *testing.T) {
cases := map[string]int{"v0.6.1": 60_199, "1.2.0-rc.3": 1_020_003, "v10.0.0": 10_000_099}
for tag, want := range cases {
@@ -60,17 +76,18 @@ func TestLatestAndDownload(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "0.6.1", l.Version)
assert.Equal(t, 60_199, l.VersionCode)
assert.Equal(t, hex.EncodeToString(sum[:]), l.SHA256)
android := l.Files["android"]
assert.Equal(t, hex.EncodeToString(sum[:]), android.SHA256)
path, err := h.ensure(context.Background(), l)
path, err := h.ensure(context.Background(), android)
require.NoError(t, err)
got, _ := os.ReadFile(path)
assert.Equal(t, apk, got)
// Подменённый файл не пройдёт сверку.
require.NoError(t, os.Remove(path))
l.SHA256 = "00" + l.SHA256[2:]
_, err = h.ensure(context.Background(), l)
android.SHA256 = "00" + android.SHA256[2:]
_, err = h.ensure(context.Background(), android)
assert.ErrorContains(t, err, "checksum mismatch")
_, statErr := os.Stat(filepath.Join(dir, "pipu-music-0.6.1.apk"))
assert.True(t, os.IsNotExist(statErr))
+1
View File
@@ -566,5 +566,6 @@ func TestPublicRoutes(t *testing.T) {
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/server-info", nil).Code)
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/healthz", nil).Code)
assert.NotEqual(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/app/android/latest", nil).Code, "APK page is public")
assert.NotEqual(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/app/clients", nil).Code, "download page is public")
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/unknown", nil).Code)
}
+4 -2
View File
@@ -55,8 +55,9 @@ var publicRoutes = map[string]bool{
"GET /api/v1/server-info": true,
"POST /api/v1/auth/login": true,
"POST /api/v1/auth/refresh": true,
// Android-приложение раздаётся по ссылке /android без входа: в APK нет ничего, кроме клиента.
// Клиенты раздаются по ссылке /download без входа: в файлах нет ничего, кроме приложения.
"GET /api/v1/app/android/latest": true,
"GET /api/v1/app/clients": true,
}
// Что разрешено, пока пользователь обязан сменить учётные данные / пароль.
@@ -93,7 +94,8 @@ func Middleware(svc *Service, log *slog.Logger) func(http.Handler) http.Handler
// Аудио открывает <audio> — тоже без заголовка; ссылка подписана для пользователя.
cover := (r.Method == http.MethodGet || r.Method == http.MethodHead) &&
(strings.HasPrefix(r.URL.Path, "/api/v1/covers/") || strings.HasPrefix(r.URL.Path, "/api/v1/stream/") ||
strings.HasPrefix(r.URL.Path, "/api/v1/app/android/download/"))
strings.HasPrefix(r.URL.Path, "/api/v1/app/android/download/") ||
strings.HasPrefix(r.URL.Path, "/api/v1/app/download/"))
if !strings.HasPrefix(r.URL.Path, "/api/v1/") || publicRoutes[route] || cover {
next.ServeHTTP(w, r.WithContext(ctx))
return