How to find any missing images?

Конечно. Вот «сырой» текст:

"It looks like \"Fung-Wong\", \"Mario\", or simply \"Typhoon #16\" will be [making landfall in Japan on Thursday](http://www.jma.go.jp/jp/typh/1416l.html):\n\n![Typhoon 16](/uploads/default/35/4608d96d1b27846f.png)"

А вот «обработанный» текст:

"<p>It looks like “Fung-Wong”, “Mario”, or simply “Typhoon <span class=\"hashtag\">#16</span>” will be <a href=\"http://www.jma.go.jp/jp/typh/1416l.html\" rel=\"nofollow noopener\">making landfall in Japan on Thursday</a>:</p>\n<p><div class=\"lightbox-wrapper\"><a class=\"lightbox\" href=\"/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png\" title=\"4608d96d1b27846f.png\"><img src=\"/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png\" alt=\"Typhoon 16\" width=\"602\" height=\"500\"><div class=\"meta\">\n<svg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"><use xlink:href=\"#far-image\"></use></svg><span class=\"filename\">4608d96d1b27846f.png</span><span class=\"informations\">800×664</span><svg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"><use xlink:href=\"#discourse-expand\"></use></svg>\n</div></a></div></p>"

Такой текст довольно трудно читать, когда он сжат в одну строку, поэтому вот «сырой» текст с красивым форматированием:

"It looks like \"Fung-Wong\", \"Mario\", or simply
\"Typhoon #16\" will be [making landfall in Japan on Thursday]
(http://www.jma.go.jp/jp/typh/1416l.html):\n\n
![Typhoon 16](/uploads/default/35/4608d96d1b27846f.png)"

А вот «обработанный» текст с красивым форматированием:

"<p>
  It looks like “Fung-Wong”, “Mario”, or simply
  “Typhoon <span class=\"hashtag\">#16</span>” will be
  <a href=\"http://www.jma.go.jp/jp/typh/1416l.html\" rel=\"nofollow noopener\">
    making landfall in Japan on Thursday
  </a>:
 </p>\n
 <p>
   <div class=\"lightbox-wrapper\">
     <a class=\"lightbox\" href=\"/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png\" title=\"4608d96d1b27846f.png\">
       <img src=\"/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png\" alt=\"Typhoon 16\" width=\"602\" height=\"500\">
       <div class=\"meta\">\n
         <svg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\">
           <use xlink:href=\"#far-image\"></use>
         </svg>
         <span class=\"filename\">4608d96d1b27846f.png</span>
         <span class=\"informations\">800×664</span>
         <svg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\">
           <use xlink:href=\"#discourse-expand\"></use>
         </svg>\n
       </div>
     </a>
   </div>
 </p>"

Кстати, на моём сайте есть довольно много (более сотни) подобных постов:

[1] pry(main)> Post.where("raw ~* :regex AND cooked !~* :regex", regex: '/uploads/default/[0-9]+/').count
=> 135

В необработанном и обработанном содержимом не должно быть разных форматов URL. Пожалуйста, попробуйте пересобрать указанный выше пост. Это можно сделать через меню поста Rebuild HTML или с помощью команды post.rebake!. Существуют ли в этом посте пользовательские поля, связанные с “uploads”? Вы можете просмотреть все пользовательские поля с помощью команды post.custom_fields.

Вот все остальные пользовательские поля для этого конкретного поста (до выполнения команды Rebuild HTML):

  id: 43,
  user_id: 1,
  topic_id: 36,
  post_number: 3,
  created_at: Mon, 22 Sep 2014 05:05:16 UTC +00:00,
  updated_at: Mon, 22 Sep 2014 05:11:22 UTC +00:00,
  reply_to_post_number: nil,
  reply_count: 0,
  quote_count: 0,
  deleted_at: nil,
  off_topic_count: 0,
  like_count: 0,
  incoming_link_count: 0,
  bookmark_count: 0,
  avg_time: 58,
  score: 1.2,
  reads: 6,
  post_type: 1,
  sort_order: 3,
  last_editor_id: -1,
  hidden: false,
  hidden_reason_id: nil,
  notify_moderators_count: 0,
  spam_count: 0,
  illegal_count: 0,
  inappropriate_count: 0,
  last_version_at: Mon, 22 Sep 2014 05:11:22 UTC +00:00,
  user_deleted: false,
  reply_to_user_id: nil,
  percent_rank: 0.585365853658537,
  notify_user_count: 0,
  like_score: 0,
  deleted_by_id: nil,
  edit_reason: "downloaded local copies of images",
  word_count: 34,
  version: 2,
  cook_method: 1,
  wiki: false,
  baked_at: Sun, 14 Apr 2019 09:28:00 UTC +00:00,
  baked_version: 2,
  hidden_at: nil,
  self_edits: 2,
  reply_quoted: false,
  via_email: false,
  raw_email: nil,
  public_version: 2,
  action_code: nil,
  image_url: "/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png",
  locked_by_id: nil

Я не вижу поля uploads, но, возможно, image_url — это то, что вы ищете? Его значение — до выполнения команды Rebuild HTML — было:

/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png

Похоже, что выполнение команды Rebuild HTML изменило значение поля image_url на:

https://{{SITE FQDN}}/uploads/default/35/4608d96d1b27846f.png

Все URL-адреса в обработанном тексте также, по-видимому, были обновлены:

"<p>
  It looks like “Fung-Wong”, “Mario”, or simply
  “Typhoon <span class=\"hashtag\">#16</span>” will be
  <a href=\"http://www.jma.go.jp/jp/typh/1416l.html\" rel=\"nofollow noopener\">
    making landfall in Japan on Thursday
  </a>:
 </p>\n
 <p>
   <div class=\"lightbox-wrapper\">
     <a class=\"lightbox\" href=\"https://{{SITE FQDN}}/uploads/default/35/4608d96d1b27846f.png\">
       <img src=\"https://{{SITE FQDN}}/uploads/default/35/4608d96d1b27846f.png\" alt=\"Typhoon 16\" width=\"602\" height=\"500\">
       <div class=\"meta\">\n
         <svg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\">
           <use xlink:href=\"#far-image\"></use>
         </svg>
         <span class=\"filename\">4608d96d1b27846f.png</span>
         <span class=\"informations\">800×664</span>
         <svg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\">
           <use xlink:href=\"#discourse-expand\"></use>
         </svg>\n
       </div>
     </a>
   </div>
 </p>"

Какова связь между 4608d96d1b27846f.png и 01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png? У них одинаковые размеры, и на первый взгляд они выглядят идентично, но это явно разные файлы:

$ diff /var/discourse/shared/standalone/uploads/default/35/4608d96d1b27846f.png /var/discourse/shared/standalone/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png
Binary files /var/discourse/shared/standalone/uploads/default/35/4608d96d1b27846f.png and /var/discourse/shared/standalone/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png differ

$ ls -l /var/discourse/shared/standalone/uploads/default/35/4608d96d1b27846f.png
-rw-r--r-- 1 chris www-data 150319 Jan 19 01:14 /var/discourse/shared/standalone/uploads/default/35/4608d96d1b27846f.png

$ ls -l /var/discourse/shared/standalone/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png
-rw-r--r-- 1 chris chris 95005 Jul  3 15:25 /var/discourse/shared/standalone/uploads/default/original/2X/0/01bb9fb7e29c2b65fd663cdc58705d1720f8fea7.png

И, конечно же, остаётся главный вопрос: как мне следует мигрировать /uploads/default/35/4608d96d1b27846f.png в новую схему загрузки?

Похоже, что ваши загрузки не были корректно перенесены на новую схему. Само изменение SiteSetting.migrate_to_new_scheme = true должно решить эту проблему. Я не уверен, почему в вашем случае этого не произошло. Пожалуйста, проверьте количество загрузок, которые не были перенесены на новую схему. Запустите приведённые ниже команды, чтобы получить результаты.

Upload.by_users.where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/default%'").count
SiteSetting.migrate_to_new_scheme = true
Jobs::MigrateUploadScheme.new.execute(nil)
Upload.by_users.where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/default%'").count

Нет, это не пользовательские поля. Значения пользовательских полей следует получать с помощью команды post.custom_fields.

Ой, извините! Я совершенно неправильно понял, что вы имели в виду под пользовательскими полями.

Я могу ошибаться, но похоже, что в том конкретном посте их нет:

[1] pry(main)> Post.find_by(:id => 43).custom_fields
=> {}

Что ж, это интересно…

[2] pry(main)> Upload.by_users.where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/default%'").count
=> 0
[3] pry(main)> Post.find_by(:id => 43).image_url
=> "https://{{SITE FQDN}}/uploads/default/35/4608d96d1b27846f.png"

Похоже, что запрос, который вы предоставили, не возвращает никаких результатов. Это ожидаемое поведение?

Также:

Есть ли у вас идея, какой может быть ответ на этот вопрос?

Похоже, что ваши загрузки уже перенесены на новую схему. Однако посты не были корректно переотнесены к новым URL-адресам схемы. В настоящее время эти посты находятся в нерабочем состоянии. Могу ли я получить учетные данные вашего сайта в личном сообщении? Тогда я смогу провести дополнительное расследование, когда у меня появится время.

Ах, вот чего я и опасался. К сожалению, это частная установка, и я не уверен, имею ли я право предоставлять (root) доступ к серверу сторонней стороне. Неужели нет ничего другого, что я мог бы сделать для устранения этой проблемы?

У меня пропали изображения примерно с конца апреля, но я только сегодня начал разбираться в этом.

rake posts:missing_uploads
Отсутствуют загрузки для 26766 постов.

Отсутствуют 22693 загрузки.
Из них 22683 — загрузки по старой схеме.
Затронуты 9352 из 535188 постов.

Мы работаем на стабильной версии… Учитывая последние сообщения в этой теме, я не уверен, что делать дальше.

Редактирование: Я выбрал один конкретный GIF-файл и обнаружил, что его нет в директории загрузок на рабочем сервере, однако он присутствует в директории tombstone на сервере резервного копирования. Я скопировал этот файл с сервера резервного копирования (discourse/shared/standalone/uploads/tombstone/default/39/ee8670816301d4c4.gif) в соответствующую директорию tombstone на рабочем сервере, а затем снова выполнил указанную выше команду rake для проверки.

Теперь изображение отображается в соответствующем посте, а общие показатели снизились до:

Отсутствуют загрузки для 26750 постов.

Отсутствуют 22692 загрузки.
Из них 22682 — загрузки по старой схеме.
Затронуты 9336 из 535190 постов.

Похоже, что размер директории tombstone на рабочем сервере составляет 138 МБ, а на сервере резервного копирования — 9,5 ГБ, поэтому я выполню rsync этой директории и снова запущу команду rake в надежде, что это ещё больше снизит reported значения.

@kansaichris похоже, что у вас всего 3 поста с отсутствующими загрузками. В этом случае вам следует вручную отредактировать исходный текст поста, указав правильный URL загрузки.

@skl у вас много загрузок по старой схеме. После копирования «могильных» загрузок с резервного сервера выполните следующие команды для миграции на новую схему.

rake posts:missing_uploads    # убедитесь, что `rsync` скопировал файлы
rake uploads:recover          # если после rsync остались отсутствующие загрузки
SiteSetting.migrate_to_new_scheme = true
Jobs::MigrateUploadScheme.new.execute(nil)
Upload.by_users.where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/default%'").count
# убедитесь, что счётчик равен 0
rake posts:missing_uploads    # снова проверьте статистику

Спасибо за помощь, @vinothkannans. Я выполнил ваши инструкции (это заняло около 12 часов), и количество проблем сократилось:

Отсутствует 22 614 загрузок постов.
Отсутствует 19 830 загрузок.
19 821 из 19 830 — это загрузки по старой схеме.
Затронуты 7 339 постов из 535 224.

Поскольку загрузок всё ещё не хватает, я проверил папку за пределами tombstone и обнаружил, что на рабочем сервере в uploads/default находится 22 885 пустых директорий (на сервере резервного копирования их всего 10). Также разница в размере составляет более 10 ГБ, поэтому я сейчас выполню rsync для uploads/default с резервного сервера на рабочий, а затем снова применю ваши инструкции.

Редактирование: Задача rake posts:missing_uploads, похоже, является однопоточной задачей, ограниченной производительностью процессора, и выполняется уже более 30 часов, поэтому я временно перенёс сервер на выделенный экземпляр CPU. Картинки, похоже, вернулись, хотя и в старой схеме, так что, вероятно, первоначальное удаление было вызвано каким-то обновлением Discourse.

Хм… если действительно всего 3 сообщения с отсутствующими вложениями, почему, похоже, 135 сообщений используют старую схему ссылок на вложения в исходном тексте, хотя в отформатированном тексте используется новая схема?

[1] pry(main)> Post.where("raw ~* :regex AND cooked !~* :regex", regex: '/uploads/default/[0-9]+/').count
=> 135

Из-за несоответствий в схемах URL загрузок в столбцах raw и cooked. Задача rake posts:missing_uploads проверяет загрузки только по столбцу cooked. Вам каким-то образом нужно исправить эти несовпадающие URL загрузок. Без доступа к базе данных я не смогу вам помочь.

:crossed_fingers:

А, понятно, я не знал, что задача posts:missing_uploads проверяет только столбец cooked — это, безусловно, объясняет расхождение. :+1:

Правильно ли будет сказать, что процесс миграции, запущенный установкой SiteSetting.migrate_to_new_scheme равным true, также проверяет только значение столбца cooked?

Миграция на новую схему должна заменить URL как в raw, так и в cooked, но в вашем случае этого не произошло.

Думаю, да. Вы также можете проверить колонку last_updated_at у затронутых постов.

Задача прервалась с ошибкой, и при каждой попытке отображается одно и то же сообщение об ошибке:

[2019-07-26T09:18:56.829375 #572]  WARN -- : Неверно сформированный IFD: undefined method `map' for nil:NilClass
....rake aborted!
ArgumentError: указан отрицательный размер -2
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/exifr-1.3.6/lib/exifr/jpeg.rb:89:in `readframe'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/exifr-1.3.6/lib/exifr/jpeg.rb:116:in `examine'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/exifr-1.3.6/lib/exifr/jpeg.rb:34:in `block in initialize'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/exifr-1.3.6/lib/exifr/jpeg.rb:34:in `open'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/exifr-1.3.6/lib/exifr/jpeg.rb:34:in `initialize'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim/worker/jhead.rb:40:in `new'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim/worker/jhead.rb:40:in `oriented?'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim/worker/jhead.rb:27:in `optimize'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim.rb:122:in `block (5 levels) in optimize_image'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim/handler.rb:41:in `process'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim.rb:122:in `block (4 levels) in optimize_image'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim.rb:120:in `each'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim.rb:120:in `block (3 levels) in optimize_image'
/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/discourse_image_optim-0.26.2/lib/image_optim.rb:247:in `block in with_timeout'
Задачи: TOP => posts:missing_uploads

Вы на последней версии или на какой-то более старой?

Последняя стабильная версия «2.3.2 +4»

Скорее всего, вам понадобится последняя бета-версия.

Если вы используете собственное хостинг, нет особых причин использовать стабильную версию. На самом деле её поддерживать сложнее.

Клиент специально попросил оставаться на стабильной версии. Когда я принял проект, он был непреклонен в этом вопросе.