В этом фрагменте кода 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 МБ).
Причина, по которой это происходит, скорее всего, связана с этим объяснением в документации 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
Что за объект
uploaded_file.tempfile
? Какой класс?