require'json'require'time'require'sprockets/manifest_utils'moduleSprockets# The Manifest logs the contents of assets compiled to a single directory. It# records basic attributes about the asset for fast lookup without having to# compile. A pointer from each logical path indicates which fingerprinted# asset is the current one.## The JSON is part of the public API and should be considered stable. This# should make it easy to read from other programming languages and processes# that don't have sprockets loaded. See `#assets` and `#files` for more# infomation about the structure.classManifestincludeManifestUtilsattr_reader:environment# Create new Manifest associated with an `environment`. `filename` is a full# path to the manifest json file. The file may or may not already exist. The# dirname of the `filename` will be used to write compiled assets to.# Otherwise, if the path is a directory, the filename will default a random# ".sprockets-manifest-*.json" file in that directory.## Manifest.new(environment, "./public/assets/manifest.json")#definitialize(*args)ifargs.first.is_a?(Base)||args.first.nil?@environment=args.shiftend@directory,@filename=args[0],args[1]# Whether the manifest file is using the old manifest-*.json naming convention@legacy_manifest=false# Expand paths@directory=File.expand_path(@directory)if@directory@filename=File.expand_path(@filename)if@filename# If filename is given as the second argif@directory&&File.extname(@directory)!=""@directory,@filename=nil,@directoryend# Default dir to the directory of the filename@directory||=File.dirname(@filename)if@filename# If directory is given w/o filename, pick a random manifest location@rename_filename=nilif@directory&&@filename.nil?@filename=find_directory_manifest(@directory)# If legacy manifest name autodetected, mark to rename on saveifFile.basename(@filename).start_with?("manifest")@rename_filename=File.join(@directory,generate_manifest_path)endendunless@directory&&@filenameraiseArgumentError,"manifest requires output filename"enddata={}beginifFile.exist?(@filename)data=json_decode(File.read(@filename))endrescueJSON::ParserError=>elogger.error"#{@filename} is invalid: #{e.class}#{e.message}"end@data=dataend# Returns String path to manifest.json file.attr_reader:filenamealias_method:path,:filenameattr_reader:directoryalias_method:dir,:directory# Returns internal assets mapping. Keys are logical paths which# map to the latest fingerprinted filename.## Logical path (String): Fingerprint path (String)## { "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",# "jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }#defassets@data['assets']||={}end# Returns internal file directory listing. Keys are filenames# which map to an attributes array.## Fingerprint path (String):# logical_path: Logical path (String)# mtime: ISO8601 mtime (String)# digest: Base64 hex digest (String)## { "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =># { 'logical_path' => "application.js",# 'mtime' => "2011-12-13T21:47:08-06:00",# 'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }#deffiles@data['files']||={}end# Public: Find all assets matching pattern set in environment.## Returns Enumerator of Assets.deffind(*args)unlessenvironmentraiseError,"manifest requires environment for compilation"endreturnto_enum(__method__,*args)unlessblock_given?paths,filters=args.flatten.partition{|arg|self.class.simple_logical_path?(arg)}filters=filters.map{|arg|self.class.compile_match_filter(arg)}environment=self.environment.cachedpaths.eachdo|path|environment.find_all_linked_assets(path)do|asset|yieldassetendendiffilters.any?environment.logical_pathsdo|logical_path,filename|iffilters.any?{|f|f.call(logical_path,filename)}environment.find_all_linked_assets(filename)do|asset|yieldassetendendendendnilend# Compile and write asset to directory. The asset is written to a# fingerprinted filename like# `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is# also inserted into the manifest file.## compile("application.js")#defcompile(*args)unlessenvironmentraiseError,"manifest requires environment for compilation"endfilenames=[]find(*args)do|asset|files[asset.digest_path]={'logical_path'=>asset.logical_path,'mtime'=>asset.mtime.iso8601,'size'=>asset.bytesize,'digest'=>asset.hexdigest,# Deprecated: Remove beta integrity attribute in next release.# Callers should DigestUtils.hexdigest_integrity_uri to compute the# digest themselves.'integrity'=>DigestUtils.hexdigest_integrity_uri(asset.hexdigest)}assets[asset.logical_path]=asset.digest_pathifalias_logical_path=self.class.compute_alias_logical_path(asset.logical_path)assets[alias_logical_path]=asset.digest_pathendtarget=File.join(dir,asset.digest_path)ifFile.exist?(target)logger.debug"Skipping #{target}, already exists"elselogger.info"Writing #{target}"asset.write_totargetendfilenames<<asset.filenameendsavefilenamesend# Removes file from directory and from manifest. `filename` must# be the name with any directory path.## manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")#defremove(filename)path=File.join(dir,filename)logical_path=files[filename]['logical_path']ifassets[logical_path]==filenameassets.delete(logical_path)endfiles.delete(filename)FileUtils.rm(path)ifFile.exist?(path)savelogger.info"Removed #{filename}"nilend# Cleanup old assets in the compile directory. By default it will# keep the latest version, 2 backups and any created within the past hour.## Examples## To force only 1 backup to be kept, set count=1 and age=0.## To only keep files created within the last 10 minutes, set count=0 and# age=600.#defclean(count=2,age=3600)asset_versions=files.group_by{|_,attrs|attrs['logical_path']}asset_versions.eachdo|logical_path,versions|current=assets[logical_path]versions.reject{|path,_|path==current}.sort_by{|_,attrs|# Sort by timestampTime.parse(attrs['mtime'])}.reverse.each_with_index.drop_while{|(_,attrs),index|_age=[0,Time.now-Time.parse(attrs['mtime'])].max# Keep if under age or within the count limit_age<age||index<count}.each{|(path,_),_|# Remove old assetsremove(path)}endend# Wipe directivedefclobberFileUtils.rm_r(directory)ifFile.exist?(directory)logger.info"Removed #{directory}"nilend# Persist manfiest back to FSdefsaveif@rename_filenamelogger.info"Renaming #{@filename} to #{@rename_filename}"FileUtils.mv(@filename,@rename_filename)@filename=@rename_filename@rename_filename=nilenddata=json_encode(@data)FileUtils.mkdir_pFile.dirname(@filename)PathUtils.atomic_write(@filename)do|f|f.write(data)endendprivatedefjson_decode(obj)JSON.parse(obj,create_additions: false)enddefjson_encode(obj)JSON.generate(obj)enddefloggerifenvironmentenvironment.loggerelselogger=Logger.new($stderr)logger.level=Logger::FATALloggerendendendend