在这段使用
exifr处理
UploadedFile
的 Ruby 代码中
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
的阅读,所以我不明白什么会导致文件被关闭。
这发生在 puma 上运行的 Rails 应用程序中。
#2 更好,因为它不需要将文件完全加载到内存中(在我的例子中,我们谈论的是最多 50 MB)。
发生这种情况的原因很可能是因为
Tempfile
文档中的解释:
当 Tempfile 对象被垃圾回收时,或者当 Ruby 解释器 退出时,其关联的临时文件会被自动删除。
现在,当
EXIFR
调用 file.dup
时,它会在文件对象上执行 examine
。一旦 EXIFR
读取完文件,就会对这个重复的对象进行垃圾收集,并且作为此垃圾收集的副作用,临时文件将被删除。
我们将此问题称为
Tempfile
竞争条件。 Tempfile
很可能不是“dup-safe”,并且 EXIFR
可能被编程为期望 File
对象,在这种情况下,这个问题不会发生。
因此,如果您想保留临时文件以供以后处理,解决方案是不要使用
EXIFR::JPEG.new(f)
。
另一种解决方案是使用
File
对象自行打开临时文件,然后将该对象传递给 EXIFR::JPEG.new
。
这样您就不必将文件读入内存,只要您在某处维护对
f
的引用,垃圾收集也不会删除临时文件。
最后也是最简单的解决方案是注意
EXIFR
还接受 initialize
方法的文件路径字符串。因此,这可能是最简单的修复:
EXIFR::JPEG.new(f.path)
感谢@Casper,我知道我被 f.dup 欺骗了 - 不会想到标准 Ruby 库的一部分会以这种方式运行 - 当(dup'ed)引用仍然存在时删除临时文件。
我选择修复此问题的方式与 Casper 的解决方案不同,因为我已经有一个打开的临时文件,并且我想使用它,而不是同时重新打开文件。 (谁知道这对不同的操作系统会产生什么影响?)
这就是我修复它的方法:
EXIFR::JPEG.new(SelfDuper.new(f))
这是我为其编写的辅助类:
require 'delegate'
class SelfDuper < SimpleDelegator
def dup
self
end
end