Добавление отсутствующих загрузок в базу данных

У меня есть сайт, где, похоже, множество загрузок были удалены из таблицы uploads в базе данных, но всё ещё существуют в файловой системе. Из-за этого ссылки на них сломаны. Я исправил несколько, загрузив файлы заново в новую тему, чтобы создать запись в БД (сначала я думал, что файлы тоже отсутствуют, но @angus любезно указал, почему я их не находил — когда-нибудь спрошу: зачем у нас есть и sha1, и base62 имена для всех этих ресурсов?), а затем пересобрал посты, включающие эти загрузки, вот так:

def rebake_posts_with_uploads(topic_id, post_number)
  p = Post.find_by(topic_id: topic_id, post_number: post_number)
  exit unless p
  re = /upload:\/\/(.+?)\)/
  shas= p.raw.scan(re)

  shas.each do |sha|
    posts = Post.where("raw like '%#{sha[0]}%'")
    next unless posts
    puts "Found #{posts.count - 1} #{sha[0]}"
    posts.each do |post|
      next if post.id == p.id
      puts "rebake #{post.id}--#{BASE_URL}/t/-/#{post.topic_id}/#{post.post_number}"
      post.rebake!
    end
  end
end

Мой новый план, поскольку файлы, похоже, существуют в файловой системе, но отсутствуют в базе данных, заключается в следующем:

for (all files in /shared/uploads/default/original/1x) do |file|
  unless file is in uploads table
     create upload record 
     for each post that includes that upload record
       rebake

Это кажется правильным? Я смотрю на uploads.rake и не вижу ничего, что уже делало бы это. Это своего рода обратная версия

но вместо FileUtils.rm(file_path) я бы, думаю, использовал Upload.create.

Если это кажется совсем глупым или есть гораздо лучшее решение, буду рад услышать его, прежде чем углубляться в эту кроличью нору.

Спасибо.

Я не знаю, как это произошло. Я надеялся возложить вину на кастомный плагин, но боюсь, что это не так. Возможно, это связано с другим обсуждением, где кто-то сказал:

Да, наша история с загрузками здесь довольно неустойчивая, и это наша вина. @sam, не могли бы вы порекомендовать кого-нибудь, кто мог бы дать краткий совет?

Это может сработать, просто нужно быть осторожным и тестировать на локальной среде… Также обратите внимание на директории 2x/3x — там загрузки повсюду.

Я пробую что-то вроде этого:

def add_missing_files_to_uploads
  public_directory = Rails.root.join("public").to_s
  db = RailsMultisite::ConnectionManagement.current_db
  uploads_directory = File.join(public_directory, 'uploads', db).to_s
  # загруженные файлы и оптимизированные изображения
  missing = 0
  matched = 0
  Dir.glob("#{uploads_directory}/**/*.pdf").each do |file_path|
    sha1 = Upload.generate_digest(file_path)
    url = file_path.split(public_directory, 2)[1]
    if (Upload.where(sha1: sha1).empty? &&
        Upload.where(url: url).empty?)
      puts "MISSING #{file_path}" if DEBUG
      missing += 1
    else
      matched += 1
    end
  end
  puts "MISS: #{missing}. Match #{matched}"
end

Выглядит ли это примерно так? Мой первый тест, кажется, не удался, но возможно, я что-то напутал.

Привет, @pfaffman. Я читал эту тему, так как она выглядит многообещающим решением для моей проблемы здесь.
Вам удалось в итоге добиться хорошего результата?

Извините, я не могу вспомнить.

На мой взгляд и взгляд Сэма, это должно сработать.

Я только что решил проблему с симулятором, создав пост, вставив в него ссылку на все изображения, а затем пересоздав все посты, содержащие transparent.gif.

Это выглядит как более элегантное решение.

Я бы предложил сделать резервную копию только базы данных и попробовать. Я сейчас в отпуске, но если у вас есть бюджет, я могу посмотреть, что смогу сделать.

И у меня тоже есть важный клиент на ams3; я ещё не перенёс его на AWS, но думаю, это скоро произойдёт. У меня есть друг, который работал в DigitalOcean, и он рекомендовал ams3, потому что это был их лучший дата-центр, который был в значительной степени недоиспользован (это было уже давно). Всё это не сработало так, как я надеялся.

Интересно, является ли это лучшим методом на сегодняшний день?

Я пытаюсь восстановить загрузки, которые находятся в файловой системе, но отсутствуют в базе данных после неудачной миграции S3 (с использованием старого rake:s3_migrate).

По-настоящему хорошего метода нет. :crying_cat:

Но да, идея остаётся прежней. Я не думаю, что в Discourse с тех пор что-то изменилось. Обычно я делаю пару операций вручную, чтобы убедиться, что всё работает как ожидалось. Также обязательно сделайте резервную копию заранее и, возможно, переведите сайт в режим только для чтения. Тогда, если возникнет необходимость восстановить резервную копию, вам не придётся удалять сообщения, опубликованные в процессе ваших манипуляций.

Без знания гораздо большего количества деталей, которые трудно передать на форуме, я не могу сказать, действительно ли это лучший способ или существует что-то более простое. Например, вы могли бы просто заменить пути URL-адресов с помощью gsub. Если у вас есть бюджет, вы можете связаться со мной или задать вопрос в канале Marketplace.

Отлично, я решил проблему с помощью Claude и множества похвал. Я делюсь тем, что сделал, чтобы помочь любому, у кого возникла подобная или та же самая проблема.

Я не уверен, что это самый умный и оптимальный метод, просто тот, который сработал для меня.

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

Проблема (S3 → локальная файловая система)

После миграции с AWS S3 на локальную файловую систему множество изображений отображалось как transparent.png. Файлы всегда были на диске, но Discourse не мог их найти.

Корень проблемы — разорванная цепочка:

  1. Посты с короткими URL вида upload:// (SHA1, закодированный в base62).
  2. База данных uploads, сопоставляющая SHA1 → локальный путь к файлу.
  3. Файловая система, где файлы хранятся с именами, равными их SHA1-хешу.

Миграция корректно переместила файлы на диск, но записей uploads в базе данных не существовало. Без соответствующей записи Discourse возвращается к transparent.png.

Решение (создание записей и повторная обработка)

# Вход в контейнер
./launcher enter app
rails c

Создание отсутствующих записей загрузок из сиротских файлов:

dir = Rails.root.join("public", "uploads", "default", "original")
created = 0

Dir.glob(dir.join("**", "*")).select { |f| File.file?(f) }.each do |path|
  sha = File.basename(path, File.extname(path))
  next if Upload.find_by(sha1: sha)

  ext = File.extname(path).delete(".")
  relative = path.sub("#{Rails.root}/public", "")

  u = Upload.new
  u.sha1 = sha
  u.url = relative
  u.original_filename = File.basename(path)
  u.filesize = File.size(path)
  u.extension = ext
  u.user_id = -1
  u.save!(validate: false)

  created += 1
  puts "Created upload #{u.id}: #{sha}"
end

puts "Total created: #{created}"

Повторная обработка постов, ссылающихся на восстановленные загрузки:

fixed_posts = 0

Upload.where(user_id: -1).find_each do |u|
  short = u.short_url
  next unless short

  Post.where("raw LIKE '%upload://%'").find_each do |p|
    urls = p.raw.scan(/upload:\/\/[^\s\]\)]+/)
    urls.each do |url|
      decoded = Upload.sha1_from_short_url(url)
      if decoded == u.sha1
        p.rebake!
        fixed_posts += 1
        puts "Rebaked post #{p.id}"
        break
      end
    end
  end
end

puts "Total rebaked: #{fixed_posts}"

Перегенерация отсутствующих оптимизированных версий:

После исправления исходных файлов необходимо заполнить оптимизированные версии (1X, 2X и т.д.).

Rake работает в контейнере Discourse, но не в консоли Rails.

rake uploads:regenerate_missing_optimized

[ОПЦИОНАЛЬНО] Всё ещё отсутствуют оптимизированные версии

Если rake uploads:regenerate_missing_optimized не решил все проблемы с файлами и некоторые файлы всё ещё отсутствуют:

# Вход в контейнер
./launcher enter app
rails c
missing = 0
OptimizedImage.find_each do |oi|
  path = "#{Rails.root}/public#{oi.url}"
  unless File.exist?(path)
    missing += 1
    oi.delete
  end
end
puts "Deleted #{missing} broken optimized records"

Затем выйдите из Rails и выполните снова:

rake uploads:regenerate_missing_optimized

Безопасный откат (на всякий случай)

Все созданные записи используют user_id: -1, а метод delete_all пропускает колбэки, поэтому файлы на файловой системе не затрагиваются. Чтобы отменить изменения:

Upload.where(user_id: -1).delete_all

Ранее по ошибке использовал destroy_all, что вызвало колбэки и переместило файлы в «могилу» (tombstone).

Восстановил один отдельный файл, который использовал для тестирования, и пересмотрел свой подход.