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<StoreprependStrategy::LocalCacheattr_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)FILEPATH_MAX_SIZE=900# max is 1024, plus some roomEXCLUDED_DIRS=['.','..'].freezeGITKEEP_FILES=['.gitkeep','.keep'].freezedefinitialize(cache_path,options=nil)super(options)@cache_path=cache_path.to_send# Deletes all items from the cache. In this case it deletes all the entries in the specified# file store directory except for .keep or .gitkeep. Be careful which directory is specified in your# config file when using +FileStore+ because everything in that directory will be deleted.defclear(options=nil)root_dirs=exclude_from(cache_path,EXCLUDED_DIRS+GITKEEP_FILES)FileUtils.rm_r(root_dirs.collect{|f|File.join(cache_path,f)})rescueErrno::ENOENTend# Preemptively iterates through all stored keys and removes the ones which have expired.defcleanup(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?endend# Increments an already existing integer value that is stored in the cache.# If the key is not found nothing is done.defincrement(name,amount=1,options=nil)modify_value(name,amount,options)end# Decrements an already existing integer value that is stored in the cache.# If the key is not found nothing is done.defdecrement(name,amount=1,options=nil)modify_value(name,-amount,options)enddefdelete_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(path,options)ifkey.match(matcher)endendendprotecteddefread_entry(key,options)ifFile.exist?(key)File.open(key){|f|Marshal.load(f)}endrescue=>elogger.error("FileStoreError (#{e}): #{e.message}")ifloggernilenddefwrite_entry(key,entry,options)returnfalseifoptions[:unless_exist]&&File.exist?(key)ensure_cache_path(File.dirname(key))File.atomic_write(key,cache_path){|f|Marshal.dump(entry,f)}trueenddefdelete_entry(key,options)ifFile.exist?(key)beginFile.delete(key)delete_empty_directories(File.dirname(key))truerescue=>e# Just in case the error was caused by another process deleting the file first.raiseeifFile.exist?(key)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.defnormalize_key(key,options)key=superfname=URI.encode_www_form_component(key)iffname.size>FILEPATH_MAX_SIZEfname=Digest::MD5.hexdigest(key)endhash=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)enddefkey_file_path(key)ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
`key_file_path` is deprecated and will be removed from Rails 5.1.
Please use `normalize_key` which will return a fully resolved key or nothing.
MESSAGEkeyend# 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)ifexclude_from(dir,EXCLUDED_DIRS).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.callnameendendend# Modifies the amount of an already existing integer value that is stored in the cache.# If the key is not found nothing is done.defmodify_value(name,amount,options)file_name=normalize_key(name,options)lock_file(file_name)dooptions=merged_options(options)ifnum=read(name,options)num=num.to_i+amountwrite(name,num,options)numendendend# Exclude entries from source directorydefexclude_from(source,excludes)Dir.entries(source).reject{|f|excludes.include?(f)}endendendend