Свой артефактори для Android библиотек
Мне понадобилось публиковать библиотеки для Android (и iOS, но это потом). Очевидно, для этого нужен maven репозиторий.
Я не профи в maven, но обнаружил, что репозиторий устроен довольно просто. Самый легкий способ выложить библиотеку - опубликовать ее локально и скопировать в публичную папку на сервере. Тут нет никакой магии, все очевидно и в 90% случаев этого достаточно.
Но очень хочется иметь возможность запустить команду из грейдла и получить опубликованную библиотеку на сервере. Нужна возможность удалять артефакты, добавлять какое-нибудь описание для артефакта и версии. Желательно, красиво выводить список артефактов на веб-страничке
Конечно, задача не новая. Сейчас есть много инструментов, которые можно установить на свой сервер. Можно использовать доступные публичные сервисы и паблишить сколько угодно библиотек. Что я сходу нагуглил:
- JFrog - классика, которая используется в больших компаниях. Но вам такой комбайн вряд ли понадобится
- nexus - это самая универсальная штука.Тут есть вообще все и даже свой собственный docker hub
- gitflic - да, можно не заморачиваться с селфхостед, а выкладывать все в реджистри на gitflic
- github - тож самое как и c gitflic
- reposilite - вот это очень классный аналог. Простой и понятный сервис, который позволяет опубликовать библиотеки одной gradle командой. Есть подготовленный docker контейнер. Но! У него один критический недостаток - сделан не мной
В результате моего небольшого исследования я написал селфхотстед сервис - depot. Построен на базе PocketBase с несколькими дополнительными ручками и главной страницей со списком артефактов
Структура данных
Сначала определимся с терминами. В контексте статьи репозиторий и реджистри - это одинаковые штуки, источник откуда вы можете скачать нужную версию. Артефактом я называю саму библиотеку, а версией - конкретную сборку библиотеки. Те нельзя просто скачать артефакт, это некоторая абстракция. Скачать можно только конкретную версию
Для мой задачи нужно всего две коллекции - артефакты и версии.
Для артефакта нам нужно:
- name - название библиотеки
- description - описание библиотеки
- group - группа или неймспес для библиотеки, например ru.kovardin
- type - можно выбрать ios или android
- last - дата последнего обновления
- enabled - доступность библиотеки
Поля будут заполняться автоматически при публикации библиотеки. По умолчанию библиотеки будут доступны и enabled будет выставлен в true
. Поле description нужно заполнять руками для каждого артефакта
С версиями тоже никаких сложностей
Для артефакта нам нужно:
- name - название библиотеки
- notes - описание что изменилось в этой версии
- version - версия
- enable - доступна ли версия для установки
Notes - нужно заполнять для каждой версии самостоятельно
Создав две описанные выше коллекции, мы сделали половину сервиса для публикации библиотек. Осталось добавить логику загрузки и красивый список артефактов
Загрузка артефактов
Перед тем как начать загружать артефакты, нужно разобраться как добавить репозиторий в настройки Android проекта
В файле build.gradle для модуля который хотим опубликовать(например, somelib) добавляем код
1repositories {
2 // ...
3 maven {
4 Properties properties = new Properties()
5 properties.load(project.rootProject.file('local.properties').newDataInputStream())
6
7 def uploadToken = properties.getProperty('uploadToken')
8
9 name = "depot"
10 url = "https://depot.kovardin.ru/packages"
11 credentials(HttpHeaderCredentials) {
12 name = "Authorization"
13 value = uploadToken
14 }
15 authentication {
16 header(HttpHeaderAuthentication)
17 }
18 }
19}
Конечно, мы не хотим дать всем возможность опубликовать что угодно в нашем реджестри, поэтому мы будем указывать токен для авторизации. Сам токен указан в файле local.properties
, а тут мы получаем его из файла и передаем в заголовок Authorization
. Чуть позже укажу как получить эту самую авторизацию
- depot.kovardin.ru/packages - это адрес моего реджистри
- depot - название репозитория, может быть любое. От этого название будет зависеть название gradle команды для публикации
Указав все необходимые настройки, в gradle появиться команда вида:
./gradlew somelib:publishAllPublicationsToDepotRepository
После запуска этой команды, gradle соберет необходимые артефакты и отправит их на сервер в PUT запросе
Пример такого запроса
1PUT /packages/ru/kovardin/somelib/0.1.1/somelib-0.1.1.aar
2PUT /packages/ru/kovardin/somelib/maven-metadata.xml
3...
Название файла будет доступно в path, а содержимое каждого файла прилетит в теле запроса. Для работы с такими запросами, создаем новую ручку artifacts.Publish
и добавляем ее для обработки PUT запросов
1e.Router.PUT("/packages/*", artifacts.Publish, apis.RequireAdminAuth())
apis.RequireAdminAuth()
- это миделваря, которая делает ручку доступной только для администратора.
В методе artifacts.Publish
нужно получить название файла, его содержимое и сохранить все на сервере.
1file := strings.Replace(c.Request().URL.Path, "/packages/", "", -1)
2
3defer c.Request().Body.Close()
4
5uploadFolder := path.Join(h.settings.UploadFolder(""), filepath.Dir(file))
6uploadFile := path.Join(uploadFolder, filepath.Base(file))
7
8if err := os.MkdirAll(uploadFolder, os.ModePerm); err != nil {
9 return err
10}
11
12dst, err := os.Create(uploadFile)
13if err != nil {
14 return err
15}
16
17defer dst.Close()
18
19if _, err := io.Copy(dst, c.Request().Body); err != nil {
20 return err
21}
Нужно создать новый файл на сервере в локации ru/kovardin/somelib/
и записать в него все содержимое из c.Request().Body
Теперь нужно создать сущности в базе. Сначала создаем запись о артефакте. Чтобы узнать название и группу артефакта - распарсим файл maven-metadata.xml
. Кроме артефакта, в этом файле указаны названия доступных версий:
1<?xml version="1.0" encoding="UTF-8"?>
2<metadata>
3 <groupId>ru.kovardin</groupId>
4 <artifactId>boosty</artifactId>
5 <versioning>
6 <latest>0.1.1</latest>
7 <release>0.1.1</release>
8 <versions>
9 <version>0.1.1</version>
10 </versions>
11 <lastUpdated>20240818151425</lastUpdated>
12 </versioning>
13</metadata>
Парсим файл и сохраняем все в базу:
1if filepath.Base(uploadFile) == "maven-metadata.xml" {
2 metaFile := uploadFile
3 data, err := os.ReadFile(metaFile)
4 if err != nil {
5 return err
6 }
7
8 metadata := Metadata{}
9 if err := xml.Unmarshal(data, &metadata); err != nil {
10 return err
11 }
12
13 artifact, _ := h.app.Dao().FindFirstRecordByFilter(
14 "artifacts",
15 "group = {:group} && name = {:name}",
16 dbx.Params{"group": metadata.GroupId, "name": metadata.ArtifactId},
17 )
18 if artifact == nil {
19 collection, err := h.app.Dao().FindCollectionByNameOrId("artifacts")
20 if err != nil {
21 return err
22 }
23
24 artifact = models.NewRecord(collection)
25 }
26
27 artifact.Set("name", metadata.ArtifactId)
28 artifact.Set("group", metadata.GroupId)
29 artifact.Set("type", "android")
30 artifact.Set("enabled", true)
31
32 if err := h.app.Dao().SaveRecord(artifact); err != nil {
33 return err
34 }
35// ...
36}
Осталось добавить в базу версии, указанные в файле maven-metadata.xml
1for _, ver := range metadata.Versioning.Versions.Version {
2 version, _ := h.app.Dao().FindFirstRecordByFilter("versions",
3 "name = {:name} && version = {:version} && artifact = {:artifact}",
4 dbx.Params{"name": metadata.ArtifactId, "version": ver, "artifact": artifact.Id},
5 )
6 if version == nil {
7 collection, err := h.app.Dao().FindCollectionByNameOrId("versions")
8 if err != nil {
9 return err
10 }
11
12 version = models.NewRecord(collection)
13 }
14
15 version.Set("name", metadata.ArtifactId)
16 version.Set("version", ver)
17 version.Set("artifact", artifact.Id)
18 version.Set("enabled", true)
19
20 if err := h.app.Dao().SaveRecord(version); err != nil {
21 return err
22 }
23}
Отлично, новая версия библиотеки загружена на сервер. Нужно реализовать возможность скачивать эти версии. Gradle для скачивания отправляет два запроса GET
и HEAD
. Реализуем очень простые ручки, которые позволяют отдать любой файл по запросу
1e.Router.GET("/packages/*", apis.StaticDirectoryHandler(os.DirFS(settings.UploadFolder("")), true))
2e.Router.HEAD("/packages/*", apis.StaticDirectoryHandler(os.DirFS(settings.UploadFolder("")), true))
Скачивать и устанавливать библиотеки могут все пользователи, поэтому тут нет миделвари apis.RequireAdminAuth()
Авторизация
Теперь о том, как получить токен администратора для загрузки библиотеки. В PocketBase уже есть API для работы с авторизацией для администратор. Чтобы получить токен, нужно оправить POST запрос на ручку /api/admins/auth-with-password
1POST https://127.0.0.1:8080/api/admins/auth-with-password
2Content-Type: application/json
3
4{
5 "identity": "example@yandex.ru",
6 "password": "password"
7}
В ответ вернется JSON с полем token
1{
2 "admin": {
3 "id": "zf8ehix5ljg2fu5",
4 "created": "2024-08-04 23:10:15.851Z",
5 "updated": "2024-08-04 23:10:15.851Z",
6 "avatar": 0,
7 "email": "example@yandex.ru"
8 },
9 "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjUyMDU1NTEsImlkIjoiemY4ZWhpeDVsamcyZnU1IiwidHlwZSI6ImFkbWluIn0.j7utkMnSHvd9jNLKCZJIoZ9SKhPt2Tcj1DuuIZCeUOo"
10}
Этот token
нужно использовать для авторизации при загрузке библиотеки
Список артефактов
Уже сейчас достаточно функциональности, чтобы использовать сервис для публикации и использования библиотек в ваших Android проектах. Но добавим чуть-чуть красоты и выведем список библиотек на главной странице сервиса.
Для этого получаем список артефактов и последнюю версию:
1records, err := h.app.Dao().FindRecordsByFilter(
2 "artifacts",
3 "enabled = true",
4 "-created",
5 100,
6 0,
7)
8
9if err != nil {
10 h.app.Logger().Error("failed fetch artifacts", "err", err)
11}
12
13aa := []Artifact{}
14for _, record := range records {
15 versions, err := h.app.Dao().FindRecordsByFilter(
16 "versions",
17 "enabled = true && artifact = {:artifact}",
18 "-created",
19 1,
20 0,
21 dbx.Params{"artifact": record.Id},
22 )
23
24 if err != nil {
25 h.app.Logger().Error("failed fetch versions", "err", err)
26
27 continue
28 }
29
30 v := ""
31 if len(versions) > 0 {
32 v = versions[0].GetString("version")
33 }
34
35 aa = append(aa, Artifact{
36 Name: record.GetString("name"),
37 Group: record.GetString("group"),
38 Description: record.GetString("description"),
39 LastVersion: v,
40 })
41}
42
43html, err := h.registry.LoadFS(views.FS,
44 "layout.html",
45 "home/home.html",
46).Render(map[string]any{
47 "artifacts": aa,
48})
В результате получаем красивую страничку со списком всех доступных библиотек
Конечно, еще много всего нужно улучшить и добавить:
- Сделать докер образ для быстрой установки
- Добавить список всех доступных версий
- Доработать удаление артефактов
- Добавить возможность закрывать доступ к артефактам по тоглу enabled
- Переключать репозиторий в приватный публичный режим
- Добавить регистрацию пользователей
- Написать подробную документацию
Надеюсь, у меня хватит времени добраться до всех этих задач. А сейчас проект доступен по на gitflic - depot