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