Что в exifr заставляет этот Tempfile закрываться?

В этом фрагменте кода Ruby, который обрабатывает UploadedFile с помощью exifr

f = uploaded_file.tempfile
p "1 #{f.closed?} #{f.instance_variable_get(:'@unlinked')}"
#1 EXIFR::JPEG.new(StringIO.new(f.read))
#2 EXIFR::JPEG.new(f)
p "2 #{f.closed?} #{f.instance_variable_get(:'@unlinked')}"
GC.start
sleep 0.01
p "3 #{f.closed?} #{f.instance_variable_get(:'@unlinked')}"
p "4 #{f.size}"

Н.Б. GC.start/sleep предназначен для надежного повторения проблемы.

при раскомментировании #1 всё в порядке:

"1 false false"
"2 false false"
"3 false false"
"4 3822528"

Однако результат раскомментирования #2 вместо #1 дает следующее:

"1 false false"
"2 false false"
"3 true false"
[c4b7ce6b-5492-43db-8c64-726cafaccce0] [Thread: 24800] Errno::ENOENT (No such file or directory @ rb_file_s_size - /var/folders/vx/v0rn818s0257_3l491_v48bm0000gn/T/RackMultipart20240221-71765-acbi7v.JPG):

Теперь все, что делает exifr, это:

    def initialize(file, load_thumbnails: true)
...
        examine(file.dup, load_thumbnails: load_thumbnails)
...
      end
    end

    class Reader < SimpleDelegator
      def readbyte; readchar; end unless File.method_defined?(:readbyte)
      def readint; (readbyte << 8) + readbyte; end
      def readframe; read(readint - 2); end
      def readsof; [readint, readbyte, readint, readint, readbyte]; end
      def next
        c = readbyte while c != 0xFF
        c = readbyte while c == 0xFF
        c
      end
    end

    def examine(io, load_thumbnails: true)
      io = Reader.new(io)
...

и немного чтения из io, поэтому я не понимаю, что может привести к закрытию файла.

Это происходит в приложении Rails, работающем на puma.

Вариант 2 был бы предпочтительнее, так как не требует полной загрузки файла в память (в моем случае речь идет до 50 МБ).

Что за объект uploaded_file.tempfile? Какой класс?

Casper 21.02.2024 14:07
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
71
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Причина, по которой это происходит, скорее всего, связана с этим объяснением в документации Tempfile:

Когда объект Tempfile подвергается сборке мусора или когда интерпретатор Ruby завершается, связанный с ним временный файл автоматически удаляется.

Теперь EXIFR выполняет file.dup для файлового объекта при вызове examine. Этот дублированный объект будет удален после EXIFR чтения файла, и как побочный эффект этой сборки мусора временный файл будет удален.

Назовем эту проблему состоянием гонки. Tempfile, скорее всего, не защищен от дублирования, а Tempfile, вероятно, запрограммирован в ожидании объекта EXIFR, и в этом случае этой проблемы не произойдет.

Поэтому решение состоит в том, чтобы не использовать File, если вы хотите сохранить временный файл для последующей обработки.

Другое решение — открыть временный файл самостоятельно, используя объект EXIFR::JPEG.new(f), и вместо этого передать этот объект File.

Таким образом, вам не придется считывать файл в память, а сборщик мусора не удалит временный файл, пока вы где-то сохраняете ссылку на EXIFR::JPEG.new.

И последнее и, вероятно, самое простое решение — заметить, что f также принимает строку пути к файлу для метода EXIFR. Поэтому это, вероятно, будет самое простое решение:

EXIFR::JPEG.new(f.path)
Ответ принят как подходящий

Благодаря @Casper я понял, что меня обманул f.dup - я бы не подумал, что часть стандартной библиотеки Ruby будет вести себя таким образом - удаляя Tempfile, когда (дублированная) ссылка все еще существует.

Однако способ, которым я решил это исправить, отличается от решений Casper, поскольку у меня уже есть открытый Tempfile, и я хочу использовать его вместо одновременного повторного открытия файла. (Кто знает, какие последствия это будет иметь для разных ОС?)

Итак, вот как я это исправил:

EXIFR::JPEG.new(SelfDuper.new(f))

И это вспомогательный класс, который я написал для него:

require 'delegate'

class SelfDuper < SimpleDelegator
  def dup
    self
  end
end

Другие вопросы по теме