在Ruby/Rails中创建线程安全的不可删除唯一文件名

3

我正在开发一个批量文件上传器。多个文件通过单独的请求上传,我的用户界面提供上传进度和成功/失败信息。然后,在所有文件完成后,会有一个最终请求来处理/完成它们。为了实现这一点,我需要创建许多临时文件,这些文件的生命周期超过了单个请求。当然,我还需要确保文件名在应用程序实例之间是唯一的。

通常,我会使用Tempfile来获得易于理解的唯一文件名,但在这种情况下,由于文件需要保留直到另一个请求进来进一步处理它们,所以这种方法行不通。当文件被关闭并进行垃圾回收时,Tempfile会自动取消链接这些文件。

早期的一个问题这里建议使用 Dir::Tmpname.make_tmpname,但是这似乎没有文档记录,并且我不知道它是否线程/多进程安全。它能够保证吗?

在C语言中,我将打开文件O_EXCL,如果文件已经存在,这将失败。然后我可以不断重试,直到成功获得一个具有真正唯一名称的文件句柄。但是Ruby的File.open似乎没有任何“独占”选项。如果我要打开的文件已经存在,我必须将它附加到末尾、以写入方式打开或清空它。

在Ruby中是否有正确的方法来完成这个任务?

我已经想出了一个方法,我认为是安全的,但是它似乎过于复杂:

# make a unique filename
time = Time.now
filename = "#{time.to_i}-#{sprintf('%06d', time.usec)}"

# make tempfiles (this is gauranteed to find a unique creatable name)
data_file = Tempfile.new(["upload", ".data"], UPLOAD_BASE)

# but the file will be deleted automatically, which we don't want, so now link it in a stable location
count = 1
loop do
   begin
      # File.link will raise an exception if the destination path exists
      File.link(data_file.path, File.join(UPLOAD_BASE, "#{filename}-#{count}.data"))
      # so here we know we created a file successfully and nobody else will take it
      break
   rescue Errno::EEXIST
      count += 1
   end
end

# now unlink the original tempfiles (they're still writeable until they're closed)
data_file.unlink

# ... write to data_file and close it ...

注意:此方法不适用于 Windows 系统。对我来说没问题,但读者要注意。

在我的测试中,这个方法的可靠性很高。但是,是否有更加简单明了的方法呢?


2个回答

3
我会使用SecureRandom
也许类似这样:

p SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"

或者

p SecureRandom.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"

你可以指定长度,并且可以依靠几乎不可能发生碰撞的概率

我考虑使用 uuid,但它在1.8.7中不存在,而这正是该网站当前所在的版本。我想我可以使用十六进制,但出于可能有点迂腐的原因,我更喜欢一个保证安全的方法。我自由地承认,我的磁盘失败的可能性比SecureRandom在几千个文件名中重复的可能性更高。所以,为你点赞 :) - gwcoffey
1
没错。也许你可以将它附加到你现有的方法上,为一些廉价的保险。另外,根据文件系统的限制,你可能能够使它达到数百个字符长。在提交代码时购买一张彩票,这样你应该就没问题了... - Brad Werth

1

经过一番搜索,我终于找到了答案。当然,显而易见的方法是查看Tempfile本身的操作。我只是假设它是原生代码,但事实并非如此。例如,1.8.7版本的源代码可以在这里找到

正如您所看到的,Tempfile使用了一个明显未记录的文件模式File::EXCL。因此,我的代码可以大大简化:

# make a unique filename
time = Time.now
filename = "#{time.to_i}-#{sprintf('%06d', time.usec)}"

data_file = nil
count = 1
loop do
   begin
      data_file = File.open(File.join(UPLOAD_BASE, "#{filename}-#{count}.data"), File::RDWR|File::CREAT|File::EXCL)
      break
   rescue Errno::EEXIST
      count += 1
   end
end

# ... write to data_file and close it ...

更新 现在我看到这个问题已经在之前的帖子中被解决了:

如何在ruby中只有在文件不存在时才打开文件进行写入

所以也许整个问题都应该标记为重复。


1
唯一的问题可能是这有点确定性,所以如果这是个问题,你仍然可能希望“稍微随机一下”。 - Brad Werth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接