5177


ユーザがアップロードしたファイルのファイル名を決定する際、ファイル名が衝突しないように  
現在時刻（ミリ秒）からIDを発行するという方法を思いつきました。  
色々ためしてみて、下記のコードが完成したのですが、現在時刻の数値を62進数に変換しただけなので「p2dhXa6」や「p2d9Wco」のようにファイルの先頭文字がほぼp2で固定されてしまいます。

このままでも良いのですが、ファイル名で利用出来る文字を使って、衝突しないユニークかつ視覚的にもランダムであり、文字列長がより短いIDを生成する方法はございますでしょうか。

**現状のコード**

[code]

    def uniqid()
      def dec62(num)
        table = [('0'..'9'),('a'..'z'),('A'..'Z')].map{|c| c.to_a }.flatten
        num > 61 ? dec62(num/62)+table[num%62] : table[num%62]
      end
      dec62(Time.now.instance_eval { self.to_i * 1000 + (usec/1000) })
    end
    
[/code]


CC BY-SA 4.0
2015-01-23T18:21:55.097
5175
2020-07-31T07:09:29.093
2020-07-31T07:09:29.093
3060
5840
question
ruby
rubyで衝突しないユニークな文字列を生成するには？


# SecureRandom

SecureRandom を使うのが良いと思います。(`SecureRandom.uuid`
だとランダムに生成されたことが分かりやすいので、個人的には好みです。)

[code]

    require "securerandom"
    
    SecureRandom.hex
    # => "3366ab379a65448704fa7c7cab4b0843"
    SecureRandom.hex(32)
    # => "4ab79521bf6712c1737b55f8393816b00996de29bed69257c5f6635bea899f9a"
    SecureRandom.uuid
    # => "d1d145ae-2e1e-4d5f-8f0b-e962c47624e0"
    SecureRandom.urlsafe_base64
    # => "B7Yj6QUYaYtbM7gflqEbaQ"
    SecureRandom.urlsafe_base64(8)
    # => "8CBbCIhp8hA"
    
[/code]

URL-safe base64 (RFC 3548) は、英数以外に "-" と "_" (パディングを入れる場合には "=")
のみが使われるため、ファイル名としても "safe" です。

# Tempfile

一時ファイルを作るという目的だと、Tempfile も良いでしょう。ただし、ディレクトリは固定されます。(ディレクトリはシステムによって異なります。)

[code]

    require "tempfile"
    
    Tempfile.new("moemoe")
    # => #<File:/var/folders/sz/slkk1lv965lcpl8djdlq8qdc0000gn/T/moemoe20150124-19905-m2sbfw>
    
[/code]

# 連番

また、保存するためにユニークなファイル名を作りたいということであれば、連番で採番していくという方法もあります。たとえば、Ruby on Rails
(ActiveRecord) を利用しているなら、モデルの `id`
をベースにしてファイル名を作ると良いでしょう。(正確にいえば、環境や操作内容によって連番とならないこともありますが、ユニークであることは保証されます。)

[code]

    class UserFile < ActiveRecord::Base
      FILE_STORE_DIR = Rails.root + "store/user_files"
    
      def filename
        FILE_STORE_DIR + self.id.to_s
      end
    end
    
[/code]

[code]

    uf = UserFile.create
    uf.filename
    => #<Pathname:/User/fate/rails/hoge/store/user_files/1>
    
[/code]


CC BY-SA 3.0
2015-01-23T18:37:16.580
5177
2015-01-24T03:47:53.803
2015-01-24T03:47:53.803
76
76
5175
answer


単純にタイムスタンプ整数を逆順にしてみました。

[code]

    def uniqid()
      def dec62(num)
        table = [('0'..'9'),('a'..'z'),('A'..'Z')].map{|c| c.to_a }.flatten
        num > 61 ? dec62(num/62)+table[num%62] : table[num%62]
      end
      dec62(Time.now.instance_eval { self.to_i * 1000 + (usec/1000) }.to_s.reverse.to_i)
    end
    
[/code]

10回実行したらこんな感じでした。

1ZrpQzLz  
Vq7GJPP  
1rPEN7CV  
2gUedtTz  
Hn4q3VT  
2b61d6Yp  
Xxtz0d  
13Lw0qU9  
2BTlK0lb  
1fKRd23T


CC BY-SA 3.0
2015-01-23T23:43:47.970
5182
2015-01-23T23:43:47.970
3249
5175
answer


ファイル名として衝突しないようにするには、作ってみて衝突したら作りなおすという方法もあります。  
以下は Linux での例です。ナノ秒まで指定しているので、衝突することはまずないと思いますが、もし衝突した場合はリトライします。

[code]

    begin
      filename = Time.now.strftime("%Y%m%d%H%M%S%N")
      File.open(filename, File::WRONLY|File::CREAT|File::EXCL, 0666)
    rescue Errno::EEXIST
      # ファイルが存在してたらリトライ
      retry
    end
    
[/code]

複数サーバーや複数プロセス間で同時に動く場合は、サーバー名やプロセスIDをファイル名に含めれば衝突を回避することができます。


CC BY-SA 3.0
2015-01-24T03:14:03.977
5189
2015-08-30T16:40:45.263
2015-08-30T16:40:45.263
76
3249
5175
answer