require'active_support/core_ext/marshal'require'active_support/core_ext/file/atomic'require'active_support/core_ext/string/conversions'require'uri/common'moduleActiveSupportmoduleCache# A cache store implementation which stores everything on the filesystem.## FileStore implements the Strategy::LocalCache strategy which implements# an in-memory cache inside of a block.classFileStore<Storeattr_reader:cache_pathDIR_FORMATTER="%03X"FILENAME_MAX_SIZE=228# max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)EXCLUDED_DIRS=['.','..'].freezedefinitialize(cache_path,options=nil)super(options)@cache_path=cache_path.to_sextendStrategy::LocalCacheenddefclear(options=nil)root_dirs=Dir.entries(cache_path).reject{|f|(EXCLUDED_DIRS+[".gitkeep"]).include?(f)}FileUtils.rm_r(root_dirs.collect{|f|File.join(cache_path,f)})enddefcleanup(options=nil)options=merged_options(options)search_dir(cache_path)do|fname|key=file_path_key(fname)entry=read_entry(key,options)delete_entry(key,options)ifentry&&entry.expired?endenddefincrement(name,amount=1,options=nil)file_name=key_file_path(namespaced_key(name,options))lock_file(file_name)dooptions=merged_options(options)ifnum=read(name,options)num=num.to_i+amountwrite(name,num,options)numelsenilendendenddefdecrement(name,amount=1,options=nil)file_name=key_file_path(namespaced_key(name,options))lock_file(file_name)dooptions=merged_options(options)ifnum=read(name,options)num=num.to_i-amountwrite(name,num,options)numelsenilendendenddefdelete_matched(matcher,options=nil)options=merged_options(options)instrument(:delete_matched,matcher.inspect)domatcher=key_matcher(matcher,options)search_dir(cache_path)do|path|key=file_path_key(path)delete_entry(key,options)ifkey.match(matcher)endendendprotecteddefread_entry(key,options)file_name=key_file_path(key)ifFile.exist?(file_name)File.open(file_name){|f|Marshal.load(f)}endrescue=>elogger.error("FileStoreError (#{e}): #{e.message}")ifloggernilenddefwrite_entry(key,entry,options)file_name=key_file_path(key)ensure_cache_path(File.dirname(file_name))File.atomic_write(file_name,cache_path){|f|Marshal.dump(entry,f)}trueenddefdelete_entry(key,options)file_name=key_file_path(key)ifFile.exist?(file_name)beginFile.delete(file_name)delete_empty_directories(File.dirname(file_name))truerescue=>e# Just in case the error was caused by another process deleting the file first.raiseeifFile.exist?(file_name)falseendendendprivate# Lock a file for a block so only one process can modify it at a time.deflock_file(file_name,&block)# :nodoc:ifFile.exist?(file_name)File.open(file_name,'r+')do|f|beginf.flockFile::LOCK_EXyieldensuref.flockFile::LOCK_UNendendelseyieldendend# Translate a key into a file path.defkey_file_path(key)fname=URI.encode_www_form_component(key)hash=Zlib.adler32(fname)hash,dir_1=hash.divmod(0x1000)dir_2=hash.modulo(0x1000)fname_paths=[]# Make sure file name doesn't exceed file system limits.beginfname_paths<<fname[0,FILENAME_MAX_SIZE]fname=fname[FILENAME_MAX_SIZE..-1]enduntilfname.blank?File.join(cache_path,DIR_FORMATTER%dir_1,DIR_FORMATTER%dir_2,*fname_paths)end# Translate a file path into a key.deffile_path_key(path)fname=path[cache_path.to_s.size..-1].split(File::SEPARATOR,4).lastURI.decode_www_form_component(fname,Encoding::UTF_8)end# Delete empty directories in the cache.defdelete_empty_directories(dir)returnifFile.realpath(dir)==File.realpath(cache_path)ifDir.entries(dir).reject{|f|EXCLUDED_DIRS.include?(f)}.empty?Dir.delete(dir)rescuenildelete_empty_directories(File.dirname(dir))endend# Make sure a file path's directories exist.defensure_cache_path(path)FileUtils.makedirs(path)unlessFile.exist?(path)enddefsearch_dir(dir,&callback)returnif!File.exist?(dir)Dir.foreach(dir)do|d|nextifEXCLUDED_DIRS.include?(d)name=File.join(dir,d)ifFile.directory?(name)search_dir(name,&callback)elsecallback.callnameendendendendendend