Привет, Хабр!
Меня зовут Егор, я руководитель разработки таск-менеджера АИПлан. В комментах к прошлой статье были вопросы про экспорт из Jira на наш аналог, платформу АИПлан.
Мы решили поделиться своим опытом решения проблем, с которыми на этой пути сталкивается идущий. Пойдем по пунктам: проблема – решение.
Эта статья может быть интересна тем, кто сейчас в поисках рабочих костылей, а еще – тем, кто уже решил проблему экспорта по-своему.
Получение пользователей с доступом к проекту
Начнем с того, что у Джиры в принципе нет понятия «член проекта». Принадлежность юзера к проекту определяется правами доступа, а вот админского API, чтобы получить список пользователей с доступом к проекту, просто нет.
Приходится вытягивать нужных пользователей из списка задач по мере парсинга, параллельно решая вопрос с хранением и быстрым доступом к уже найденным пользователям(расскажу в следующих статьях, как я познал дзен дженерики). Для этого их нужно идентифицировать! Что подводит нас к следующему пункту:
Отсутствие единого стандарта идентификации
Итак, нам нужно развеять туман над Гангом и уточнить каждого юзера. Однозначного ID пользователя в Жире нет как такового (в облачной Джире, например, используется поле accountID, а в локальной — username). При этом accountId может отсутствовать, если есть username, и наоборот.
Решили методом проверки первого юзера на наличие нужного поля и дальше уже ориентировались на него.
func (c *ImportContext) getJiraUserUsername(user interface{}) string {
switch v := user.(type) {
case jira.User:
if v.Name != "" {
c.usernamesSearch = true
return v.Name
}
return v.AccountID
case jira.Watcher:
if v.Name != "" {
c.usernamesSearch = true
return v.Name
}
return v.AccountID
}
return ""
}
...
// Непосредственно запрос пользователя
req, _ := c.client.NewRequest("GET", fmt.Sprintf("/rest/api/2/user?accountId=%s", accountId), nil)
if c.usernamesSearch {
req, _ = c.client.NewRequest("GET", fmt.Sprintf("/rest/api/2/user?username=%s", accountId), nil)
}
Следующий неприятный сюрприз: Jira считает почту пользователя приватным полем, а настройкой видимости занимается исключительно админ. Как направить приглашение новым импортированным юзерам?
Решили, проставив почты сотрудникам вручную, через настройки пространства:
Вопрос приоритетов
В Джире приоритеты не распределены по проектам, а глобально – по всей системе. В результате мы получаем кучу повторяющихся, вот пример:
Нас такой бардак не устраивал, поэтому мы задались целью и решили проблему путем предоставления пользователю маппинга вручную, в том числе связей между задачами. В конечном счете это экономит время и помогает избежать кучи «непоняток».
Ненадежные вложения
Следующий булыжник на нашем пути – отображение картинок, встроенных в текст описания или комментариев. Здесь индусы превзошли себя!
Каждый блок представляет из себя span с классом image-wrap. В нем находятся превью картинки и ссылка на ее полный размер. Пример:
<span class="image-wrap" style="">
<a id="attachmentID_thumb" href="attachmentURL" title="filename.PNG" file-preview-type="image" file-preview-id="attachmentID" file-preview-title="filename.PNG">
<img src="thumbnailURL" style="border: 0px solid black" />
</a>
</span>
Казалось бы: парсим, берем ссылку на полную картинку, сохраняем к нам, profit. Но выше – это идеальный вариант. В реальности и половины от него не будет. Встречаются такие случаи:
Есть только thumbnail. Видимо, Жира сохраняет мелкие картинки без обработки, сразу воспринимая их как превью. Решение: тянем превью и верим в лучшее (опционально можно при этом молиться Вишну, но помогает не всегда).
Есть картинка, но без атрибутов. Почему Жира не всегда возвращает file-preview-id, по которому удобно вытягивать метаданные аттачмента? Это древня индусская тайна.
Решение: парсим attachmentURL – вытягиваем attachmentID – уже по нему работаем.У пикчи нет атрибута ширины. Так происходит, если картинку вставляли без ресайза. В таком случае Jira отрисовывает превью, которое генерит по своим алгоритмам.
Наше решение: тянем превью, парсим заголовок картинки (спасибо стандартному пакету image и его методу DecodeConfig — не нужно читать всю картинку!) и сохраняем ширину у себя.Некорректный attachmentURL — порой приходят встроенные иконки с кривыми URLами. Решение: игнорируем.
Пустой span — тайна, покрытая мраком. Решение: игнорируем.
HTML отлично парсится стандартной библиотекой net/html, никаких сторонних либ не нужно.
Download failed
Еще одна проблема на стороне Жиры – со скачиванием файлов. Организовать нормальный pipe сразу в наш minio чаще всего невозможно, из-за любви локальной Jira обрывать соединения без уточнения причин.
Вот так:
Придется идти более долгим, зато надежным путем. Скачиваем файл в буфер – и только потом начинаем закачку в minio или любое другое объектное хранилище на ваш вкус. При обрывах соединения - пробуем до 5 раз, а потом заботливо показываем пользователю список проблемных вложений и их задачи.
Трудности html’а
Тот html, который выдает Джира, в принципе вызывает в памяти индуистские обряды с обязательным использованием курильниц. Например, такой:
<div id="syntaxplugin" class="syntaxplugin">
<table cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr id="syntaxplugin_code_and_gutter">
<td style=" line-height: 1.4em !important; padding: 0em; vertical-align: top;">
<pre><span>// заполнитель кода</span></pre>
</td>
</tr>
<tr id="syntaxplugin_code_and_gutter">
<td style=" line-height: 1.4em !important; padding: 0em; vertical-align: top;">
<pre><span>код</span></pre>
</td>
</tr>
<tr id="syntaxplugin_code_and_gutter">
<td style=" line-height: 1.4em !important; padding: 0em; vertical-align: top;">
<pre><span>код</span></pre>
</td>
</tr>
<tr id="syntaxplugin_code_and_gutter">
<td style=" line-height: 1.4em !important; padding: 0em; vertical-align: top;">
<pre><span>код</span></pre>
</td>
</tr>
<tr id="syntaxplugin_code_and_gutter">
<td style=" line-height: 1.4em !important; padding: 0em; vertical-align: top;">
<pre><span>код</span></pre>
</td>
</tr>
</tbody>
</table>
</div>
Код <pre> хранится очень странно. Выглядит как таблица, каждая строка которой - строчка кода, обернутая в <pre>.
Мы пришли к тому, чтобы вытаскивать все <pre> и склеивать воедино с нормальным переносом строки через /n.
Отдельная боль - табуляции (/n/t), которые крошат отображение. Такие чистим простой регуляркой. Пример:
<p>
<ul>
/n/t<li>
Текст
</li>
/n/t<li>
Текст
</li>
</ul>
<p/>
/n<p>Текст</p>
После всех замен и чисток прогоняем получившийся html через sanitizer bluemonday с правилами ugc (с кастомными настройками под наш редактор) и strict.
В результате всех манипуляций получаем красивый и чистый html для нашего редактора. Плюсом – чистый текст для уведомлений на почту или в Телеграм.
Послесловие
Это не полный перечень сложностей, конечно. Скорее из серии «самого-самого», краткий перечень того, с чем мы столкнулись. В результате удалось добиться главного: сейчас можно перенести свой проект в АИПлан из Джиры, указав пространство в системе, приоритеты и выбрав блокирующую связь. Без лишних танцев с бубном.
Будет здорово, если в комментах подбросите вопросов или расскажете, как сталкивались с похожими задачами и как их решали. Вдвойне здорово – если кому-то наш опыт поможет.
С праздниками, Хабр!
Комментарии (2)
sattvadigit
02.01.2025 18:38С начала подумал, что это open source, а оказалось какая-то реклама проприетарщины, очень очень посредственной поделки, да и вообще никак не интересной для разработчиков на go. Идите лучше в рекламные сети и там пиарьтесь. Для сообщества нулевая польза от статьи.
karrakoliko
Звучно. Осталось продать сервис яндексу, и стать "ЯИплан".
Простите