diff --git a/lib/gemstash/storage.rb b/lib/gemstash/storage.rb index 9d574756..dcc19d26 100644 --- a/lib/gemstash/storage.rb +++ b/lib/gemstash/storage.rb @@ -1,4 +1,5 @@ require "gemstash" +require "digest" require "pathname" require "fileutils" require "yaml" @@ -36,11 +37,20 @@ def path_valid?(path) #:nodoc: class Resource - attr_accessor :name + attr_accessor :name, :folder def initialize(folder, name) @base_path = folder @name = name - @folder = File.join(@base_path, @name) + # Avoid odd characters in paths, in case of issues with the file system + safe_name = @name.gsub(/[^a-zA-Z0-9_]/, "_") + # Use a trie structure to avoid file system limits causing too many files in 1 folder + # Downcase to avoid issues with case insensitive file systems + trie_parents = safe_name[0...3].downcase.split("") + # The digest is included in case the name differs only by case + # Some file systems are case insensitive, so such collisions will be a problem + digest = Digest::MD5.hexdigest(@name) + child_folder = "#{safe_name}-#{digest}" + @folder = File.join(@base_path, *trie_parents, child_folder) end def exist? diff --git a/spec/gemstash/storage_spec.rb b/spec/gemstash/storage_spec.rb index dd88d661..150631bd 100644 --- a/spec/gemstash/storage_spec.rb +++ b/spec/gemstash/storage_spec.rb @@ -73,5 +73,36 @@ expect(resource.content).to eq(content) end end + + context "with resource name that is unique by case only" do + let(:first_resource_id) { "SomeResource" } + let(:second_resource_id) { "someresource" } + + it "stores the content separately" do + storage.resource(first_resource_id).save("first content") + storage.resource(second_resource_id).save("second content") + expect(storage.resource(first_resource_id).load.content).to eq("first content") + expect(storage.resource(second_resource_id).load.content).to eq("second content") + end + + it "uses different downcased paths to avoid issues with case insensitive file systems" do + first_resource = storage.resource(first_resource_id) + second_resource = storage.resource(second_resource_id) + expect(first_resource.folder.downcase).to_not eq(second_resource.folder.downcase) + end + end + + context "with resource name that includes odd characters" do + let(:resource_id) { ".=$&resource" } + + it "stores and retrieves the data" do + storage.resource(resource_id).save("odd name content") + expect(storage.resource(resource_id).load.content).to eq("odd name content") + end + + it "doesn't include the odd characters in the path" do + expect(storage.resource(resource_id).folder).to_not match(/[.=$&]/) + end + end end end