# frozen_string_literal: truerequire"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=226# max filename size on file system is 255, minus room for timestamp, pid, and random characters appended by Tempfile (used by atomic write)FILEPATH_MAX_SIZE=900# max is 1024, plus some roomGITKEEP_FILES=[".gitkeep",".keep"].freezedefinitialize(cache_path,**options)super(options)@cache_path=cache_path.to_send# Advertise cache versioning support.defself.supports_cache_versioning?trueend# 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=(Dir.children(cache_path)-GITKEEP_FILES)FileUtils.rm_r(root_dirs.collect{|f|File.join(cache_path,f)})rescueErrno::ENOENT,Errno::ENOTEMPTYend# 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|entry=read_entry(fname,**options)delete_entry(fname,**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)endendendprivatedefread_entry(key,**options)ifpayload=read_serialized_entry(key,**options)entry=deserialize_entry(payload)entryifentry.is_a?(Cache::Entry)endenddefread_serialized_entry(key,**)File.binread(key)ifFile.exist?(key)rescue=>errorlogger.error("FileStoreError (#{error}): #{error.message}")ifloggernilenddefwrite_entry(key,entry,**options)write_serialized_entry(key,serialize_entry(entry,**options),**options)enddefwrite_serialized_entry(key,payload,**options)returnfalseifoptions[:unless_exist]&&File.exist?(key)ensure_cache_path(File.dirname(key))File.atomic_write(key,cache_path){|f|f.write(payload)}trueenddefdelete_entry(key,**options)ifFile.exist?(key)beginFile.delete(key)delete_empty_directories(File.dirname(key))truerescue# Just in case the error was caused by another process deleting the file first.raiseifFile.exist?(key)falseendendend# Lock a file for a block so only one process can modify it at a time.deflock_file(file_name,&block)ifFile.exist?(file_name)File.open(file_name,"r+")do|f|f.flockFile::LOCK_EXyieldensuref.flockFile::LOCK_UNendelseyieldendend# Translate a key into a file path.defnormalize_key(key,options)key=superfname=URI.encode_www_form_component(key)iffname.size>FILEPATH_MAX_SIZEfname=ActiveSupport::Digest.hexdigest(key)endhash=Zlib.adler32(fname)hash,dir_1=hash.divmod(0x1000)dir_2=hash.modulo(0x1000)# Make sure file name doesn't exceed file system limits.iffname.length<FILENAME_MAX_SIZEfname_paths=fnameelsefname_paths=[]beginfname_paths<<fname[0,FILENAME_MAX_SIZE]fname=fname[FILENAME_MAX_SIZE..-1]enduntilfname.blank?endFile.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.children(dir).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.each_child(dir)do|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)numendendendendendend