What is involved with changing attachment_fu's storage scheme? - ruby-on-rails

I have a rails application that is using attachment_fu. Currently, it is using :file_system for storage, but I want to change it to :s3, to allow for better scaling as more files get uploaded.
What is involved with this? I imagine that if I just switch the code to use :s3, all the old links will be broken. Do I need to just copy the existing files from the file system to S3? A google search hasn't turned up much on the topic.
I would prefer to move the existing files over to S3, so everything is in the same place, but if necessary, the old files can stay where they are, as long as new ones go to S3.
EDIT: So, it is not as simple as copying over the files to S3; the URLs are created using a different scheme. When they are stored in :file_system, the files end up in places like /public/photos/0000/0001/file.name, but the same file in :s3 might end up in 0/1/file.name. I think it is using the id something, and just padding it (or not) with zeros, but I'm not sure of that.

That's correct. The ids are padded using :file_system storage.
Instead of renaming all your files, you can alter the s3 backend module to use padded numbers as well.
Copy the partitioned_path method from file_system_backend.rb and put it in s3_backend.rb.
def partitioned_path(*args)
if respond_to?(:attachment_options) && attachment_options[:partition] == false
args
elsif attachment_options[:uuid_primary_key]
# Primary key is a 128-bit UUID in hex format. Split it into 2 components.
path_id = attachment_path_id.to_s
component1 = path_id[0..15] || "-"
component2 = path_id[16..-1] || "-"
[component1, component2] + args
else
path_id = attachment_path_id
if path_id.is_a?(Integer)
# Primary key is an integer. Split it after padding it with 0.
("%08d" % path_id).scan(/..../) + args
else
# Primary key is a String. Hash it, then split it into 4 components.
hash = Digest::SHA512.hexdigest(path_id.to_s)
[hash[0..31], hash[32..63], hash[64..95], hash[96..127]] + args
end
end
end
Modify s3_backend.rb's full_filename method to use the partitioned_path.
def full_filename(thumbnail = nil)
File.join(base_path, *partitioned_path(thumbnail_name_for(thumbnail)))
end
attachment_fu will now create paths with the same names as it did with the file_system backend, so you can just copy your files over to s3 without renaming everything.

In addition to nilbus' answer, I had to modify s3_backend.rb's base_path method to return an empty string, otherwise it would insert the attachment_path_id twice:
def base_path
return ''
end

What worked for me, in addition to nilbus's answer, was to modify s3_backend.rb's base_path method to still use the path_prefix (which is by default the table name):
def base_path
attachment_options[:path_prefix]
end
And also, I had to take the attachment_path_id from file_system_backend.rb and replace the one in s3_backend.rb, since otherwise partitioned_path always thought my Primary Key was a String:
def attachment_path_id
((respond_to?(:parent_id) && parent_id) || id) || 0
end

Thanks for all those responses which helped a lot. It worked for me too but I had to do this in order to have the :thumbnail_class option working :
def full_filename(thumbnail = nil)
prefix = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
File.join(prefix, *partitioned_path(thumbnail_name_for(thumbnail)))
end

Related

Is there a more elegant way to perform a multiple include? selection in ruby?

I came across this issue in a project last night, while writing a helper to pick an icon depending on a file extension, and was wondering if there was a better (more "ruby") way of handling it?
The code currently goes something like:
def choose_icon(myfile)
icon = "default-icon"
if ["*.doc","*.docx",".txt","*.dot"].include? File.extname(myfile)
icon = "doc-icon"
end
if ["*.xls","*.xlsx","*.xlt"].include? File.extname(myfile)
icon = "sheet-icon"
end
if ["*.mp3","*.aac","*.aiff","*.wav"].include? File.extname(myfile)
icon = "audio-icon"
end
if ["*.mov","*.m4a","*.wmv"].include? File.extname(myfile)
icon = "movie-icon"
end
icon # return the chosen icon
end
This somehow feels a little clumsy and inelegant to me, and I was struggling to find a better way to do it in Ruby.
(Note: The above example is REALLY simplified and the actual code is far longer and looks far more untidy.)
It would be really cool if the 'case' construct would work like this:
def choose_icon(myfile)
case File.extname(myfile)
when ["*.doc","*.docx",".txt","*.dot"].include?
"doc-icon"
when ["*.xls","*.xlsx","*.xlt"].include?
"sheet-icon"
when ["*.mp3","*.aac","*.aiff","*.wav"].include?
"audio-icon"
when ["*.mov","*.m4a","*.wmv"].include?
"movie-icon"
else
"default-icon"
end
end
But of course, that doesn't work. It is so much easier to read though, so I was wondering if I had missed some other method of doing a multiple comparison against a collection of options that would restore some elegance and readability to my code?
You almost got it right. Just leave off the square brackets and the include? and it will work. I think the asterisks should not be necessary either since File.extname returns the extension with just a dot.
def choose_icon(myfile)
case File.extname(myfile)
when '.doc', '.docx', '.txt', '.dot'
'doc-icon'
when '.xls', '.xlsx', '.xlt'
'sheet-icon'
when '.mp3', '.aac', '.aiff', '.wav'
'audio-icon'
when '.mov', '.m4a', '.wmv'
'movie-icon'
else
'default-icon'
end
end
You could use a hash:
h = [*(%w| .doc .docx .txt .dot |).product(["doc-icon"]),
*(%w| .xls .xlsx .xlt |).product(["sheet-icon"]),
*(%w| .aac .aiff .wav |).product(["audio-icon"]),
*(%w| .mov .m4a .wmv |).product(["movie-icon"])].to_h
#=> {".doc"=>"default-icon", ".docx"=>"default-icon",
# ".txt"=>"default-icon", ".dot"=>"default-icon",
# ".xls"=>"sheet-icon" , ".xlsx"=>"sheet-icon",
# ".xlt"=>"sheet-icon" , ".aac"=>"audio-icon",
# ".aiff"=>"audio-icon" , ".wav"=>"audio-icon",
# ".mov"=>"movie-icon" , ".m4a"=>"movie-icon",
# ".wmv"=>"movie-icon"}
h.default = "default-icon"
h[File.extname("myfile.wav")]
#=> "audio-icon"
h[File.extname("./mydir/myfile.abc")]
#=> "default-icon"

In Rails, how do I determine if two URLs are equal?

If I have two URLs in Rails, (whether they be in string form or URI objects) what's the best way to determine if they are equal? It seems like a fairly simple problem, but I need the solution to work even if one of the URLs is relative and the other is absolute, or if one of the URLs has different parameters than the other.
I already looked at What is the best way in Rails to determine if two (or more) given URLs (as strings or hash options) are equal? (and several other questions), but the question was pretty old and the suggested solution doesn't work the way I need it to.
Provided you have url1 and url2 being some string containing a URL :
def is_same_controller_and_action?(url1, url2)
hash_url1 = Rails.application.routes.recognize_path(url1)
hash_url2 = Rails.application.routes.recognize_path(url2)
[:controller, :action].each do |key|
return false if hash_url1[key] != hash_url2[key]
end
return true
end
1) convert URL to canonical form
In my current project I am using addressable gem in order to do that:
def to_canonical(url)
uri = Addressable::URI.parse(url)
uri.scheme = "http" if uri.scheme.blank?
host = uri.host.sub(/\www\./, '') if uri.host.present?
path = (uri.path.present? && uri.host.blank?) ? uri.path.sub(/\www\./, '') : uri.path
uri.scheme.to_s + "://" + host.to_s + path.to_s
rescue Addressable::URI::InvalidURIError
nil
rescue URI::Error
nil
end
Example:
> to_canonical('www.example.com') => 'http://example.com'
> to_canonical('http://example.com') => 'http://example.com'
2) compare your URLs: canonical_url1 == canonical_url2
UPD:
Does it work with sub-domains? - No. I mean, we cannot say that translate.google.com and google.com are equal. Of course, you can modify it depending on your needs.
Checkout the addressable gem and specifically the normalize method (and its documentation), and the heuristic_parse method (and its documentation). I've used it in the past and found it to be very robust.
Addressable even handles URLs with unicode characters in them:
uri = Addressable::URI.parse("http://www.詹姆斯.com/")
uri.normalize
#=> #<Addressable::URI:0xc9a4c8 URI:http://www.xn--8ws00zhy3a.com/>

Hash with indifferent access

I have a non-Rails project in which I am loading some settings from a YAML file:
config = YAML::load File.open("#{LOG_ROOT}/config/database.yml")
I can only access this hash like config["host"], config["username"] etc.
I want indifferent access so I can use both :host and "host".
The reason is, that one of the gems in the project to which I am passing this hash seems to be accessing it using symbols and it fails currently.
What is the best way to create a hash with indifferent access in this scenario?
You lose nothing except a few kB of disk space by installing the Active Support gem. In your code, you require only the function you want:
require 'active_support/core_ext/hash/indifferent_access'
That way, you can be sure you are not getting anything else to mess up your namespace.
Let the config hash return the value for the stringified version of the key:
config = {"host"=>"value1", "Username"=>"Tom"}
config.default_proc = proc{|h, k| h.key?(k.to_s) ? h[k.to_s] : nil}
p config[:host] #=> "value1"
The default_proc runs everytime when a key is not found in the hash. Note this is only half of indifferent access: config["host"] will result in nil if the key :host is present. If that has to work too:
config.default_proc = proc do |h, k|
case k
when String then sym = k.to_sym; h[sym] if h.key?(sym)
when Symbol then str = k.to_s; h[str] if h.key?(str)
end
end
See the comments about limitations of this approach (tltr: separate values for :a and 'a' are possible, does not take into account Hash.delete and others).

Recursive method not returning to previous line position in caller

I have been trying to find a solution to this for some time. I have found questions and answers on recursion but nothing that seemed to fit this particular situation.
I have written a class which should go through the given folder and all subfolders and rename files and folders if a particular search pattern is found.
Everything works as expected the replaceAllInDir gets called, it replaces files and folders if needed. The next step then is to do the same for all subfolders within the given folder.
So a subfolder gets identified and replaceAllInDir gets called from within itself. Let's assum the particular subfolder called does not contain any subfolders. I would then expect that we return to the parent folder and continue looking for other subfolders. But instead control is not returned to the parent calling method and the program ends.
I am aware of other ways of solving the actual use case, but I cannot explain the behaviour of ruby.
class MultiFileAndFolderRename
attr_accessor :rootDir, :searchPattern, :replacePattern
def initialize(rootDir, searchPattern, replacePattern)
#rootDir = rootDir
#searchPattern = searchPattern
#replacePattern = replacePattern
end
def execute
replaceAllInDir(#rootDir)
end
def getValidDirEntries(dir)
dirList = Dir.entries(dir)
dirList.delete('.')
dirList.delete('..')
dirList
end
def replaceAllInDir(currentDir)
Dir.chdir(currentDir)
puts "Processing directory: " + Dir.pwd
dirList = getValidDirEntries(currentDir)
dirList.each { |dirEntry|
attemptRename(dirEntry)
}
dirList = getValidDirEntries(currentDir)
dirList.each { |dirEntry|
if File.directory?(dirEntry)
newDir = currentDir + '\\' + dirEntry
rntemp = MultiFileAndFolderRename.new(newDir, 'searchString', 'replaceString')
rntemp.replaceAllInDir(newDir)
end
}
end
def attemptRename(dirEntry)
if dirEntry.match(#searchPattern)
newname = dirEntry.to_s.sub(#searchPattern, #replacePattern)
FileUtils.mv(dirEntry.to_s, newname)
end
end
end
You have a bug. The first line of replaceAllInDir() is Dir.chdir(). chdir() changes the directory of the current process on a global scale. It's not call-stack dependent. So later when you move into a subdirectory and change into that, the change becomes permanent even if you return from the recursion.
You need to change back to the correct directory after any call to replaceAllInDir(). For example:
...
dirList.each { |dirEntry|
if File.directory?(dirEntry)
....
rntemp.replaceAllInDir(newDir)
Dir.chdir(currentDir) # <- Restore us back to the correct directory
end
}
I have tried your code, and I have found numerous errors in it. Perhaps if you fix them, your idea is working.
You should include in a library like that a part at the end that allows to call it from the shell: MultiFileAndFolderRename.new(ARGV[0], ARGV[1], ARGV[2]).execute if __FILE__ == $0 This ensures when you call the ruby code from the shell by ruby rename.rb test old new, your class will be instantiated, and the search and replace pattern will be set accordingly.
You shouldn't set the current directory, because that ensures that the line getValidDirEntries(currentDir) will not work. If you eg. call it for the directory test, and then change your current directory to test, inside the directory, getValidDirEntries('test') will not work like expected.
You should use only forward slashes instead of the double backward ones. So your code will work on Linux and Mac OS X as well.
When you instantiate the new instance of MultiFileAndFolderRename (which is not necessary), the arguments to the initializer are the wrong ones. Instead, you should use your current instance and just call self.replaceAllInDir(newDir) instead of rntemp = MultiFileAndFolderRename.new(newDir, 'searchString', 'replaceString');rntemp.replaceAllInDir(newDir).
I think the wrong instantiation is the major reason why it works not as expected, but the others should be fixed as well.

How to parse a yaml file into ruby hashs and/or arrays?

I need to load a yaml file into Hash,
What should I do?
I would use something like:
hash = YAML.load(File.read("file_path"))
A simpler version of venables' answer:
hash = YAML.load_file("file_path")
Use the YAML module:
http://ruby-doc.org/stdlib-1.9.3/libdoc/yaml/rdoc/YAML.html
node = YAML::parse( <<EOY )
one: 1
two: 2
EOY
puts node.type_id
# prints: 'map'
p node.value['one']
# prints key and value nodes:
# [ #<YAML::YamlNode:0x8220278 #type_id="str", #value="one", #kind="scalar">,
# #<YAML::YamlNode:0x821fcd8 #type_id="int", #value="1", #kind="scalar"> ]'
# Mappings can also be accessed for just the value by accessing as a Hash directly
p node['one']
# prints: #<YAML::YamlNode:0x821fcd8 #type_id="int", #value="1", #kind="scalar">
http://yaml4r.sourceforge.net/doc/page/parsing_yaml_documents.htm
You may run into a problem mentioned at this related question, namely, that the YAML file or stream specifies an object into which the YAML loader will attempt to convert the data into. The problem is that you will need a related Gem that knows about the object in question.
My solution was quite trivial and is provided as an answer to that question. Do this:
yamltext = File.read("somefile","r")
yamltext.sub!(/^--- \!.*$/,'---')
hash = YAML.load(yamltext)
In essence, you strip the object-classifier text from the yaml-text. Then you parse/load it.

Resources