13

In a Ruby on Rails app I am working on I allow users to upload files and want to give these files a short, random alphanumeric name. (Eg 'g7jf8' or '3bp76'). What is the best way to do this?

I sas thinking of generating a hash / encrypted string from the original filename and timestamp. Then query the database to double check it doesnt exist. If it does, generate another and repeat.

The issue i see with this approach is if there is high propability of duplicate strings, it could add quite a lote of datbase load.

  • 1
    There is also the potential (if improbable) race condition of two requests trying to add the same name at the same time. The database should have a unique constraint on that column and you should be prepared to catch `ActiveRecord::RecordNotUnique`. – mpartel Mar 17 '13 at 09:09
  • check http://stackoverflow.com/questions/5966910/generate-unique-random-string-with-letters-and-numbers-in-lower-case – sameera207 Mar 17 '13 at 09:29
  • Does the "random" name have a security purpose? If not, you have more options. – Neil Slater Mar 17 '13 at 09:43
  • It doesn't need to be secure, but may be used in a URL. Thanks for all the help so far. Many ideas to try and work on. –  Mar 17 '13 at 20:43

8 Answers8

12

I use this :)

def generate_token(column, length = 64)
  begin
    self[column] = SecureRandom.urlsafe_base64 length
  end while Model.exists?(column => self[column])
end

Replace Model by your model name

Benjamin Bouchet
  • 12,971
  • 2
  • 41
  • 73
10
SecureRandom.uuid

Will give you a globally unique String. http://en.m.wikipedia.org/wiki/Universally_unique_identifier

SecureRandom.hex 32

Will give a random String, but it's algorithm is not optimised for uniqueness. Of course the chance of collision with 32 digits, assuming true randomness, is basically theoretical. You could make 1 billion per second for 100 years and have only a 50% chance of a collision.

Chris Aitchison
  • 4,656
  • 1
  • 27
  • 43
6

Use Ruby's SecureRandom.hex function with optional number of character you wanted to generate.

Amit Patel
  • 15,609
  • 18
  • 68
  • 106
1

This will always produce new uniq 40 size alpha-numeric string, because it has Time stamp also.

loop do
  random_token = Digest::SHA1.hexdigest([Time.now, rand(111..999)].join)
  break random_token unless Model.exists?(column_name: random_token)
end

Note: Replace Model by your model_name and column_name by any existing column of your model.

zolter
  • 7,070
  • 3
  • 37
  • 51
deepak
  • 198
  • 2
  • 5
0

You can assign a unique id by incrementing it each time a new file is added, and convert that id into an encrypted string using OpenSSL::Cipher with a constant key that you save somewhere.

sawa
  • 165,429
  • 45
  • 277
  • 381
0

If you end up generating a hex or numeric digest, you can keep the code shorter by representing the number as e.g. Base 62:

# This is a lightweight base62 encoding for Ruby integers.
B62CHARS = ('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a

def base62_string nbr
  b62 = ''
  while nbr > 0
    b62 << B62CHARS[nbr % 62]
    nbr /= 62
  end
  b62.reverse
end

If it is important for you to restrict the character set used (for instance not have uppercase chars in file names), then this code can easily be adapted, provided you can find a way of feeding in a suitable random number.

If your file names are supposed to be semi-secure, you need to arrange that there are many more possible names than actual names in storage.

Neil Slater
  • 26,512
  • 6
  • 76
  • 94
  • This doesn't generate anything unique, which is what the OP asked for. Given the same `nbr` this will return the same string every time, and you need to pass a ridiculously large number to return a string of any significant size. Ex: `> base62_string 99999999999999999999 # => "1V973MbJYWoT"` – Chris Bloom May 03 '16 at 12:38
  • @Chrisbloom7: Agreed, this needs feeding in a number, which I don't explain how to obtain (but I do mention in the text this is required). Generating a random number of suitable size is trivial in Ruby though: `SecureRandom.random_number(2**128)`. The approach also works with sequences, hashes etc. The fact that it requires a huge input to make a short string is actually desirable to the OP, they asked for a short string – Neil Slater May 03 '16 at 14:00
0

It looks like you actually need a unique filenames, right? Why not forget about complex solutions and simply use Time#nsec?

t = Time.now        #=> 2007-11-17 15:18:03 +0900
"%10.9f" % t.to_f   #=> "1195280283.536151409"
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
0

You can use Time in miliseconds and than convert it into base 36 to reduce it length. and since it depends on time to it will be very unique.

Example: Time.now.to_f.to_s.gsub('.', '').ljust(17, '0').to_i.to_s(36) # => "4j26lna7g62"

Take a look at this answer: https://stackoverflow.com/a/72738840/7365329

Touqeer
  • 583
  • 1
  • 4
  • 19