diff --git a/api/openapi.yaml b/api/openapi.yaml index 89c1d07..b7883eb 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1547,9 +1547,10 @@ paths: operationId: getAppClients tags: [app] security: [] # публично: страница /download без входа - summary: Клиенты последнего релиза (Android и десктоп) + summary: Клиенты последних релизов (Android и десктоп) description: | Тот же релиз Gitea, что и для Android: APK, установщик Windows, .deb, DMG. + У каждой платформы `older` — предыдущие сборки (до 8 релизов, сколько есть с файлами). Нет настроек Gitea или файлов клиентов — `404 app.update_unavailable`. responses: "200": @@ -3581,12 +3582,12 @@ components: AppClient: type: string enum: [android, windows, linux, macos] - AppClientFile: + AppClientBuild: type: object - required: [client, filename, size, download_url] + required: [version, filename, size, download_url] properties: - client: - $ref: "#/components/schemas/AppClient" + version: + type: string filename: type: string size: @@ -3596,6 +3597,27 @@ components: type: string download_url: type: string + AppClientFile: + type: object + required: [client, filename, size, download_url] + properties: + client: + $ref: "#/components/schemas/AppClient" + version: + type: string + filename: + type: string + size: + type: integer + format: int64 + sha256: + type: string + download_url: + type: string + older: + type: array + items: + $ref: "#/components/schemas/AppClientBuild" AppClients: type: object required: [version, version_code, notes_md, clients] diff --git a/apps/web/src/pages/AppDownloadPage.tsx b/apps/web/src/pages/AppDownloadPage.tsx index bbcbfc0..3fa7310 100644 --- a/apps/web/src/pages/AppDownloadPage.tsx +++ b/apps/web/src/pages/AppDownloadPage.tsx @@ -12,7 +12,7 @@ import s from './auth.module.css'; /** * Публичная страница приложений (/download, /android): ссылку можно отправить человеку. - * Сборки — последний релиз Gitea через сервер; первым предлагается клиент текущей ОС. + * Сборки — до 8 последних релизов Gitea через сервер; первым предлагается клиент текущей ОС. */ export function AppDownloadPage() { const { t, i18n } = useTranslation(['auth', 'common']); @@ -43,7 +43,10 @@ export function AppDownloadPage() { {latest.isPending ? null : data ? ( <> {primary ? ( - + <> + + + ) : detected ? (
{t('app.unavailable_platform', { platform: t(`app.platform.${detected}`) })}
) : null} @@ -72,12 +75,15 @@ export function AppDownloadPage() { {others.map((id) => { const file = files[id]!; return ( - - {t(`app.download.${id}`)} - - {t('app.version', { version: data.version })} · {formatBytes(file.size, i18n.language)} - - +
+ + {t(`app.download.${id}`)} + + {t('app.version', { version: file.version ?? data.version })} · {formatBytes(file.size, i18n.language)} + + + +
); })} @@ -91,6 +97,22 @@ export function AppDownloadPage() { ); } +function OlderVersions({ items }: { items: { version: string; filename?: string; size: number; download_url: string }[] }) { + const { t, i18n } = useTranslation(['auth', 'common']); + if (items.length === 0) return null; + return ( +
+ {t('app.older')} + {items.map((b) => ( + + {t('app.version', { version: b.version })} + {formatBytes(b.size, i18n.language)} + + ))} +
+ ); +} + function DownloadButton({ client, href, meta }: { client: AppClient; href: string; meta: string }) { const { t } = useTranslation('auth'); return ( diff --git a/apps/web/src/pages/auth.module.css b/apps/web/src/pages/auth.module.css index 671c256..03792a3 100644 --- a/apps/web/src/pages/auth.module.css +++ b/apps/web/src/pages/auth.module.css @@ -244,6 +244,12 @@ gap: 8px; } +.otherBlock { + display: flex; + flex-direction: column; + gap: 8px; +} + .otherTitle { margin: 8px 0 0; color: var(--text3); @@ -272,6 +278,20 @@ font-weight: 500; } +.older { + font-size: 13px; + color: var(--text3); +} + +.older summary { + cursor: pointer; + font-weight: 600; +} + +.older .downloadAlt { + margin-top: 8px; +} + .link { align-self: center; } diff --git a/apps/web/src/test/fakeApi.ts b/apps/web/src/test/fakeApi.ts index d220302..3f12f94 100644 --- a/apps/web/src/test/fakeApi.ts +++ b/apps/web/src/test/fakeApi.ts @@ -122,7 +122,13 @@ export function fakeApi(initial: FakeUser[]) { version_code: 1_030_099, notes_md: '', clients: [ - { client: 'windows', filename: 'pipu-music_1.3.0_x64-setup.exe', size: 20_000_000, download_url: '/api/v1/app/download/windows/1.3.0' }, + { + client: 'windows', + filename: 'pipu-music_1.3.0_x64-setup.exe', + size: 20_000_000, + download_url: '/api/v1/app/download/windows/1.3.0', + older: [{ version: '1.2.0', filename: 'pipu-music_1.2.0_x64-setup.exe', size: 19_000_000, download_url: '/api/v1/app/download/windows/1.2.0' }], + }, { client: 'linux', filename: 'pipu-music_1.3.0_amd64.deb', size: 4_000_000, download_url: '/api/v1/app/download/linux/1.3.0' }, { client: 'macos', filename: 'pipu-music_1.3.0_aarch64.dmg', size: 22_000_000, download_url: '/api/v1/app/download/macos/1.3.0' }, { client: 'android', filename: 'pipu-music-1.3.0.apk', size: 12_000_000, sha256: 'ab'.repeat(32), download_url: '/api/v1/app/download/android/1.3.0' }, diff --git a/docs/api.md b/docs/api.md index 00b82ea..08dd63f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -106,8 +106,8 @@ ### Мобильное обновление — [mobile.md](mobile.md#самообновление) | Метод | Путь | Описание | |---|---|---| -| GET | `/app/clients` | `{version, version_code, notes_md, clients:[{client, filename, size, sha256?, download_url}]}` — Android, Windows, Linux, macOS | -| GET | `/app/download/{client}/{version}` | файл клиента (прокси / кэш релиза Gitea) | +| GET | `/app/clients` | `{version, version_code, notes_md, clients:[{client, filename, size, sha256?, download_url, older:[{version, filename, size, download_url}]}]}` — до 8 релизов на платформу | +| GET | `/app/download/{client}/{version}` | файл клиента этой версии (прокси / кэш релиза Gitea) | | GET | `/app/android/latest` | `{version, version_code, notes_md, size, sha256, download_url, min_supported}` | | GET | `/app/android/download/{version}` | APK (прокси / кэш релиза Gitea) | diff --git a/docs/mobile.md b/docs/mobile.md index f97961f..fb6823e 100644 --- a/docs/mobile.md +++ b/docs/mobile.md @@ -99,13 +99,13 @@ ## Самообновление -Первая установка — публичная страница `/download` веб-клиента (`/android` ведёт туда же): APK, десктопные сборки по ОС, адрес сервера и инструкция. +Первая установка — публичная страница `/download` веб-клиента (`/android` ведёт туда же): APK, десктопные сборки по ОС, предыдущие версии (до 8 последних релизов, сколько есть), адрес сервера и инструкция. Приложение распространяется только через Gitea Releases (не Google Play). **Поток:** 1. При запуске (не чаще раза в 6 ч) и вручную в профиле: `GET /api/v1/app/android/latest` на **сервере Pipu**. - Сервер сам опрашивает Gitea API (`GET /api/v1/repos/{owner}/{repo}/releases/latest`) с серверным токеном, + Сервер сам опрашивает Gitea API (список релизов, до 8 с файлами; fallback — `/releases/latest`) с серверным токеном, кэширует ответ на 10 мин и APK на диске. Так: - мобильному приложению не нужен доступ к Gitea и токен (репозиторий может быть приватным); - версия приложения согласована с версией сервера (`min_supported` из `server-info`). diff --git a/packages/api-client/src/generated/schema.ts b/packages/api-client/src/generated/schema.ts index e7c0946..0b95b28 100644 --- a/packages/api-client/src/generated/schema.ts +++ b/packages/api-client/src/generated/schema.ts @@ -1335,8 +1335,9 @@ export interface paths { cookie?: never; }; /** - * Клиенты последнего релиза (Android и десктоп) + * Клиенты последних релизов (Android и десктоп) * @description Тот же релиз Gitea, что и для Android: APK, установщик Windows, .deb, DMG. + * У каждой платформы `older` — предыдущие сборки (до 8 релизов, сколько есть с файлами). * Нет настроек Gitea или файлов клиентов — `404 app.update_unavailable`. */ get: operations["getAppClients"]; @@ -2356,14 +2357,24 @@ export interface components { }; /** @enum {string} */ AppClient: "android" | "windows" | "linux" | "macos"; - AppClientFile: { - client: components["schemas"]["AppClient"]; + AppClientBuild: { + version: string; filename: string; /** Format: int64 */ size: number; sha256?: string; download_url: string; }; + AppClientFile: { + client: components["schemas"]["AppClient"]; + version?: string; + filename: string; + /** Format: int64 */ + size: number; + sha256?: string; + download_url: string; + older?: components["schemas"]["AppClientBuild"][]; + }; AppClients: { version: string; version_code: number; diff --git a/packages/i18n/locales/en/auth.json b/packages/i18n/locales/en/auth.json index 41f7f54..64771bf 100644 --- a/packages/i18n/locales/en/auth.json +++ b/packages/i18n/locales/en/auth.json @@ -40,6 +40,7 @@ }, "version": "version {{version}}", "other": "Other platforms", + "older": "Previous versions", "step_install": { "android": "Open the downloaded file. If Android asks, allow installs from this source.", "windows": "Run the installer and follow the steps. pipu will appear in the Start menu.", diff --git a/packages/i18n/locales/ru/auth.json b/packages/i18n/locales/ru/auth.json index 6dafbb2..dc1f445 100644 --- a/packages/i18n/locales/ru/auth.json +++ b/packages/i18n/locales/ru/auth.json @@ -40,6 +40,7 @@ }, "version": "версия {{version}}", "other": "Другие платформы", + "older": "Предыдущие версии", "step_install": { "android": "Откройте скачанный файл. Если Android спросит — разрешите установку из этого источника.", "windows": "Запустите установщик и следуйте шагам. pipu появится в меню Пуск.", diff --git a/services/api/internal/api/gen/api.gen.go b/services/api/internal/api/gen/api.gen.go index 9b24135..4633e34 100644 --- a/services/api/internal/api/gen/api.gen.go +++ b/services/api/internal/api/gen/api.gen.go @@ -337,13 +337,24 @@ type AndroidRelease struct { // AppClient defines model for AppClient. type AppClient string +// AppClientBuild defines model for AppClientBuild. +type AppClientBuild struct { + DownloadUrl string `json:"download_url"` + Filename string `json:"filename"` + Sha256 *string `json:"sha256,omitempty"` + Size int64 `json:"size"` + Version string `json:"version"` +} + // 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"` + Client AppClient `json:"client"` + DownloadUrl string `json:"download_url"` + Filename string `json:"filename"` + Older *[]AppClientBuild `json:"older,omitempty"` + Sha256 *string `json:"sha256,omitempty"` + Size int64 `json:"size"` + Version *string `json:"version,omitempty"` } // AppClients defines model for AppClients. @@ -1778,7 +1789,7 @@ type ServerInterface interface { // Последняя версия Android-приложения (из релизов Gitea через сервер) // (GET /api/v1/app/android/latest) GetAndroidLatest(w http.ResponseWriter, r *http.Request) - // Клиенты последнего релиза (Android и десктоп) + // Клиенты последних релизов (Android и десктоп) // (GET /api/v1/app/clients) GetAppClients(w http.ResponseWriter, r *http.Request) // Файл клиента (кэшируется на сервере, поддерживает Range) @@ -2163,7 +2174,7 @@ func (_ Unimplemented) GetAndroidLatest(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusNotImplemented) } -// Клиенты последнего релиза (Android и десктоп) +// Клиенты последних релизов (Android и десктоп) // (GET /api/v1/app/clients) func (_ Unimplemented) GetAppClients(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) @@ -8200,7 +8211,7 @@ type StrictServerInterface interface { // Последняя версия Android-приложения (из релизов Gitea через сервер) // (GET /api/v1/app/android/latest) GetAndroidLatest(ctx context.Context, request GetAndroidLatestRequestObject) (GetAndroidLatestResponseObject, error) - // Клиенты последнего релиза (Android и десктоп) + // Клиенты последних релизов (Android и десктоп) // (GET /api/v1/app/clients) GetAppClients(ctx context.Context, request GetAppClientsRequestObject) (GetAppClientsResponseObject, error) // Файл клиента (кэшируется на сервере, поддерживает Range) @@ -10868,284 +10879,286 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "hIU8hV6vce/Wvf8bAAD//7F7c4hPbwEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/services/api/internal/appupdate/appupdate.go b/services/api/internal/appupdate/appupdate.go index d530395..9578759 100644 --- a/services/api/internal/appupdate/appupdate.go +++ b/services/api/internal/appupdate/appupdate.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strconv" "strings" "sync" @@ -30,6 +31,9 @@ const CodeUnavailable = "app.update_unavailable" const cacheTTL = 10 * time.Minute +// keepClientReleases — сколько последних релизов с файлами отдаём на /download (как prune в CI). +const keepClientReleases = 8 + var tagRe = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$`) // VersionCode — versionCode по тегу; та же формула, что в scripts/version.mjs: @@ -68,9 +72,12 @@ type asset struct { } type release struct { - Tag string `json:"tag_name"` - Body string `json:"body"` - Assets []asset `json:"assets"` + Tag string `json:"tag_name"` + Body string `json:"body"` + Draft bool `json:"draft"` + PublishedAt time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + Assets []asset `json:"assets"` } type clientFile struct { @@ -98,11 +105,16 @@ type Handler struct { log *slog.Logger mu sync.Mutex - cached *Latest + cached *catalog cachedAt time.Time dl sync.Mutex } +type catalog struct { + latest *Latest + all []*Latest +} + // NewHandler создаёт обработчик. func NewHandler(cfg Config, log *slog.Logger) *Handler { return &Handler{cfg: cfg, client: &http.Client{Timeout: 10 * time.Minute}, log: log} @@ -196,7 +208,7 @@ func pickClientFiles(assets []asset) map[string]clientFile { return out } -func (h *Handler) latest(ctx context.Context) (*Latest, error) { +func (h *Handler) catalog(ctx context.Context) (*catalog, error) { if !h.configured() { return nil, apperr.NotFound(CodeUnavailable, "gitea is not configured") } @@ -208,7 +220,8 @@ func (h *Handler) latest(ctx context.Context) (*Latest, error) { ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() base := strings.TrimRight(h.cfg.GiteaURL, "/") - res, err := h.get(ctx, base+"/api/v1/repos/"+h.cfg.Repo+"/releases/latest") + repo := h.cfg.Repo + rels, err := h.listReleases(ctx, base, repo) if err != nil { h.log.WarnContext(ctx, "app update: gitea unavailable", "err", err) if h.cached != nil { @@ -216,37 +229,114 @@ func (h *Handler) latest(ctx context.Context) (*Latest, error) { } return nil, apperr.NotFound(CodeUnavailable, "release is unavailable") } - var rel release - err = json.NewDecoder(res.Body).Decode(&rel) - _ = res.Body.Close() - if err != nil { - return nil, fmt.Errorf("gitea: decode release: %w", err) + var all []*Latest + for _, rel := range rels { + if rel.Draft { + continue + } + l := h.parseRelease(ctx, rel) + if l == nil { + continue + } + all = append(all, l) + if len(all) >= keepClientReleases { + break + } } + if len(all) == 0 { + return nil, apperr.NotFound(CodeUnavailable, "release has no client builds") + } + cat := &catalog{latest: all[0], all: all} + h.cached = cat + h.cachedAt = time.Now() + return cat, nil +} + +func (h *Handler) listReleases(ctx context.Context, base, repo string) ([]release, error) { + res, err := h.get(ctx, base+"/api/v1/repos/"+repo+"/releases?limit=20") + if err == nil { + var rels []release + decErr := json.NewDecoder(res.Body).Decode(&rels) + _ = res.Body.Close() + if decErr != nil { + return nil, fmt.Errorf("gitea: decode releases: %w", decErr) + } + sort.SliceStable(rels, func(i, j int) bool { + ti, tj := rels[i].PublishedAt, rels[j].PublishedAt + if ti.IsZero() { + ti = rels[i].CreatedAt + } + if tj.IsZero() { + tj = rels[j].CreatedAt + } + return ti.After(tj) + }) + return rels, nil + } + res, err = h.get(ctx, base+"/api/v1/repos/"+repo+"/releases/latest") + if err != nil { + return nil, err + } + var rel release + decErr := json.NewDecoder(res.Body).Decode(&rel) + _ = res.Body.Close() + if decErr != nil { + return nil, fmt.Errorf("gitea: decode release: %w", decErr) + } + return []release{rel}, nil +} + +func (h *Handler) parseRelease(ctx context.Context, rel release) *Latest { version, code, err := VersionCode(rel.Tag) if err != nil { - return nil, apperr.NotFound(CodeUnavailable, err.Error()) + return nil } files := pickClientFiles(rel.Assets) if len(files) == 0 { - return nil, apperr.NotFound(CodeUnavailable, "release has no client builds") + return nil } 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) + h.log.WarnContext(ctx, "app update: apk checksum unavailable", "err", err, "version", version) 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 len(files) == 0 { + return nil + } + return &Latest{Version: version, VersionCode: code, Notes: rel.Body, Files: files} +} + +func (h *Handler) latest(ctx context.Context) (*Latest, error) { + cat, err := h.catalog(ctx) + if err != nil { + return nil, err + } + return cat.latest, nil +} + +func (c *catalog) release(version string) *Latest { + for _, l := range c.all { + if l.Version == version { + return l } } - h.cached = &Latest{Version: version, VersionCode: code, Notes: rel.Body, Files: files} - h.cachedAt = time.Now() - return h.cached, nil + return nil +} + +func (c *catalog) keepNames() map[string]struct{} { + out := map[string]struct{}{} + for _, l := range c.all { + for _, f := range l.Files { + out[f.Name] = struct{}{} + } + } + return out } func (h *Handler) readDigest(ctx context.Context, url string) (string, error) { @@ -272,33 +362,73 @@ func (h *Handler) fileURL(kind, version string) string { // GetAppClients реализует gen.StrictServerInterface. func (h *Handler) GetAppClients(ctx context.Context, _ gen.GetAppClientsRequestObject) (gen.GetAppClientsResponseObject, error) { - l, err := h.latest(ctx) + cat, err := h.catalog(ctx) if err != nil { return nil, err } + l := cat.latest order := []string{"windows", "macos", "linux", "android"} - clients := make([]gen.AppClientFile, 0, len(l.Files)) + clients := make([]gen.AppClientFile, 0, len(order)) for _, kind := range order { - f, ok := l.Files[kind] + item, ok := h.clientWithOlder(cat, 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) } + if len(clients) == 0 { + return nil, apperr.NotFound(CodeUnavailable, "release has no client builds") + } return gen.GetAppClients200JSONResponse{ Version: l.Version, VersionCode: l.VersionCode, NotesMd: l.Notes, Clients: clients, }, nil } +func (h *Handler) clientWithOlder(cat *catalog, kind string) (gen.AppClientFile, bool) { + var head *Latest + var file clientFile + var older []gen.AppClientBuild + for _, rel := range cat.all { + f, ok := rel.Files[kind] + if !ok { + continue + } + if head == nil { + head, file = rel, f + continue + } + older = append(older, gen.AppClientBuild{ + Version: rel.Version, + Filename: f.Name, + Size: f.Size, + DownloadUrl: h.fileURL(kind, rel.Version), + Sha256: shaPtr(f.SHA256), + }) + } + if head == nil { + return gen.AppClientFile{}, false + } + item := gen.AppClientFile{ + Client: gen.AppClient(kind), + Version: &head.Version, + Filename: file.Name, + Size: file.Size, + DownloadUrl: h.fileURL(kind, head.Version), + Sha256: shaPtr(file.SHA256), + } + if len(older) > 0 { + item.Older = &older + } + return item, true +} + +func shaPtr(s string) *string { + if s == "" { + return nil + } + return &s +} + // GetAndroidLatest реализует gen.StrictServerInterface. func (h *Handler) GetAndroidLatest(ctx context.Context, _ gen.GetAndroidLatestRequestObject) (gen.GetAndroidLatestResponseObject, error) { l, err := h.latest(ctx) @@ -359,16 +489,20 @@ func (h *Handler) ensure(ctx context.Context, f clientFile) (string, error) { } func (h *Handler) cleanup(keepName string) { + keep := map[string]struct{}{keepName: {}} + h.mu.Lock() + if h.cached != nil { + for name := range h.cached.keepNames() { + keep[name] = struct{}{} + } + } + h.mu.Unlock() matches, _ := filepath.Glob(filepath.Join(h.cfg.Dir, "pipu-music-*")) - ver := versionFromName(keepName) for _, m := range matches { - base := filepath.Base(m) - if ver != "" && strings.Contains(base, ver) { + if _, ok := keep[filepath.Base(m)]; ok { continue } - if base != keepName { - _ = os.Remove(m) - } + _ = os.Remove(m) } } @@ -382,7 +516,7 @@ func versionFromName(name string) string { func (h *Handler) serveFile(ctx context.Context, l *Latest, kind string) (fileResponse, error) { f, ok := l.Files[kind] - if !ok || (kind == "android" && f.SHA256 == "") { + if !ok { return fileResponse{}, apperr.NotFound(CodeUnavailable, "client build is unavailable") } path, err := h.ensure(ctx, f) @@ -393,26 +527,31 @@ func (h *Handler) serveFile(ctx context.Context, l *Latest, kind string) (fileRe return fileResponse{path: path, name: f.Name, req: httpx.RequestFrom(ctx)}, nil } -// DownloadAndroidApk реализует gen.StrictServerInterface. Отдаётся только последняя версия. +// DownloadAndroidApk реализует gen.StrictServerInterface. Версия должна быть в каталоге (до 8 релизов). func (h *Handler) DownloadAndroidApk(ctx context.Context, req gen.DownloadAndroidApkRequestObject) (gen.DownloadAndroidApkResponseObject, error) { - l, err := h.latest(ctx) + cat, err := h.catalog(ctx) if err != nil { return nil, err } - if req.Version != l.Version { - return nil, apperr.NotFound(CodeUnavailable, "only the latest version is available") + l := cat.release(req.Version) + if l == nil { + return nil, apperr.NotFound(CodeUnavailable, "this version is no longer available") + } + if _, ok := l.android(); !ok { + return nil, apperr.NotFound(CodeUnavailable, "client build is unavailable") } return h.serveFile(ctx, l, "android") } // DownloadAppClient реализует gen.StrictServerInterface. func (h *Handler) DownloadAppClient(ctx context.Context, req gen.DownloadAppClientRequestObject) (gen.DownloadAppClientResponseObject, error) { - l, err := h.latest(ctx) + cat, err := h.catalog(ctx) if err != nil { return nil, err } - if req.Version != l.Version { - return nil, apperr.NotFound(CodeUnavailable, "only the latest version is available") + l := cat.release(req.Version) + if l == nil { + return nil, apperr.NotFound(CodeUnavailable, "this version is no longer available") } return h.serveFile(ctx, l, string(req.Client)) } diff --git a/services/api/internal/appupdate/appupdate_test.go b/services/api/internal/appupdate/appupdate_test.go index 709e7af..7a36ad3 100644 --- a/services/api/internal/appupdate/appupdate_test.go +++ b/services/api/internal/appupdate/appupdate_test.go @@ -10,10 +10,13 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "pipu/api/internal/api/gen" ) func TestPickClientFiles(t *testing.T) { @@ -51,19 +54,30 @@ func TestLatestAndDownload(t *testing.T) { var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "token secret", r.Header.Get("Authorization")) + assets := []map[string]any{ + {"name": "pipu-music-0.6.1.apk", "size": len(apk), "browser_download_url": srv.URL + "/apk"}, + {"name": "pipu-music-0.6.1.apk.sha256", "size": 65, "browser_download_url": srv.URL + "/sum"}, + } switch r.URL.Path { + case "/api/v1/repos/o/r/releases": + _ = json.NewEncoder(w).Encode([]map[string]any{ + {"tag_name": "v0.6.1", "body": "## Новое", "draft": false, "published_at": "2026-01-02T00:00:00Z", "assets": assets}, + {"tag_name": "v0.6.0", "body": "", "draft": false, "published_at": "2026-01-01T00:00:00Z", "assets": []map[string]any{ + {"name": "pipu-music-0.6.0.apk", "size": 8, "browser_download_url": srv.URL + "/apk-old"}, + {"name": "pipu-music-0.6.0.apk.sha256", "size": 65, "browser_download_url": srv.URL + "/sum-old"}, + {"name": "pipu-music-0.6.0-windows-setup.exe", "size": 9, "browser_download_url": srv.URL + "/exe-old"}, + }}, + }) case "/api/v1/repos/o/r/releases/latest": _ = json.NewEncoder(w).Encode(map[string]any{ - "tag_name": "v0.6.1", "body": "## Новое", - "assets": []map[string]any{ - {"name": "pipu-music-0.6.1.apk", "size": len(apk), "browser_download_url": srv.URL + "/apk"}, - {"name": "pipu-music-0.6.1.apk.sha256", "size": 65, "browser_download_url": srv.URL + "/sum"}, - }, + "tag_name": "v0.6.1", "body": "## Новое", "assets": assets, }) case "/apk": _, _ = w.Write(apk) case "/sum": _, _ = w.Write([]byte(hex.EncodeToString(sum[:]) + "\n")) + case "/sum-old": + _, _ = w.Write([]byte(strings.Repeat("ab", 32) + "\n")) default: http.NotFound(w, r) } @@ -91,4 +105,23 @@ func TestLatestAndDownload(t *testing.T) { assert.ErrorContains(t, err, "checksum mismatch") _, statErr := os.Stat(filepath.Join(dir, "pipu-music-0.6.1.apk")) assert.True(t, os.IsNotExist(statErr)) + + out, err := h.GetAppClients(context.Background(), gen.GetAppClientsRequestObject{}) + require.NoError(t, err) + body := out.(gen.GetAppClients200JSONResponse) + var androidClient, windowsClient *gen.AppClientFile + for i := range body.Clients { + switch body.Clients[i].Client { + case gen.AppClientAndroid: + androidClient = &body.Clients[i] + case gen.AppClientWindows: + windowsClient = &body.Clients[i] + } + } + require.NotNil(t, androidClient) + require.NotNil(t, androidClient.Older) + assert.Equal(t, "0.6.0", (*androidClient.Older)[0].Version) + require.NotNil(t, windowsClient) + assert.Contains(t, windowsClient.Filename, "0.6.0") + assert.True(t, windowsClient.Older == nil || len(*windowsClient.Older) == 0) }