Импорт из Jira: перенос с запахом карри

от автора

Привет, Хабр!

Меня зовут Егор, я руководитель разработки таск-менеджера АИПлан. В комментах к прошлой статье были вопросы про экспорт из 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 для нашего редактора. Плюсом – чистый текст для уведомлений на почту или в Телеграм.

Послесловие

Это не полный перечень сложностей, конечно. Скорее из серии «самого-самого», краткий перечень того, с чем мы столкнулись. В результате удалось добиться главного: сейчас можно перенести свой проект в АИПлан из Джиры, указав пространство в системе, приоритеты и выбрав блокирующую связь. Без лишних танцев с бубном.

Будет здорово, если в комментах подбросите вопросов или расскажете, как сталкивались с похожими задачами и как их решали. Вдвойне здорово – если кому-то наш опыт поможет.

С праздниками, Хабр!


ссылка на оригинал статьи https://habr.com/ru/articles/871326/