Skip to content

Commit

Permalink
Add 'clean' rake task (#33)
Browse files Browse the repository at this point in the history
* clean rake task

* Extract explaining method and improve style

* Pass count, use explaining kwargs

* Update output_path_test.rb

* Fix mtime of files

* Use different file names per test

* Enforce mtime

* Ensure files are removed after test

Co-authored-by: David Heinemeier Hansson <david@basecamp.com>
  • Loading branch information
brenogazzola and dhh committed Nov 25, 2021
1 parent 920730e commit 5cd9716
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 0 deletions.
61 changes: 61 additions & 0 deletions lib/propshaft/output_path.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require "propshaft/asset"

class Propshaft::OutputPath
attr_reader :path, :manifest

def initialize(path, manifest)
@path, @manifest = path, manifest
end

def clean(count, age)
asset_versions = files.group_by { |_, attrs| attrs[:logical_path] }
asset_versions.each do |logical_path, versions|
current = manifest[logical_path]

versions
.reject { |path, _| current && path == current }
.sort_by { |_, attrs| attrs[:mtime] }
.reverse
.each_with_index
.drop_while { |(_, attrs), index| fresh_version_within_limit(attrs[:mtime], count, expires_at: age, limit: index) }
.each { |(path, _), _| remove(path) }
end
end

def files
Hash.new.tap do |files|
all_files_from_tree(path).each do |file|
digested_path = file.relative_path_from(path)
logical_path, digest = extract_path_and_digest(digested_path)

files[digested_path.to_s] = {
logical_path: logical_path.to_s,
digest: digest,
mtime: File.mtime(file)
}
end
end
end

private
def fresh_version_within_limit(mtime, count, expires_at:, limit:)
modified_at = [ 0, Time.now - mtime ].max
modified_at < expires_at || limit < count
end

def remove(path)
FileUtils.rm(@path.join(path))
Propshaft.logger.info "Removed #{path}"
end

def all_files_from_tree(path)
path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child }
end

def extract_path_and_digest(digested_path)
digest = digested_path.to_s[/-([0-9a-f]{7,128})\.(?!digested)[^.]+\z/, 1]
path = digest ? digested_path.sub("-#{digest}", "") : digested_path

[path, digest]
end
end
6 changes: 6 additions & 0 deletions lib/propshaft/processor.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "propshaft/output_path"

class Propshaft::Processor
MANIFEST_FILENAME = ".manifest.json"

Expand All @@ -18,6 +20,10 @@ def clobber
FileUtils.rm_r(output_path) if File.exist?(output_path)
end

def clean
OutputPath.new(output_path, load_path.manifest).clean(2, 1.hour)
end

private
def ensure_output_path_exists
FileUtils.mkdir_p output_path
Expand Down
5 changes: 5 additions & 0 deletions lib/propshaft/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class Railtie < ::Rails::Railtie

desc "Remove config.assets.output_path"
task clobber: :environment do
Rails.application.assets.processor.clobber
end

desc "Removes old files in config.assets.output_path"
task clean: :environment do
Rails.application.assets.processor.clean
end

Expand Down
74 changes: 74 additions & 0 deletions test/propshaft/output_path_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require "test_helper"
require "minitest/mock"
require "propshaft/asset"
require "propshaft/load_path"
require "propshaft/output_path"

class Propshaft::OutputPathTest < ActiveSupport::TestCase
setup do
@manifest = {
".manifest.json": ".manifest.json",
"one.txt": "one-f2e1ec14d6856e1958083094170ca6119c529a73.txt"
}.stringify_keys
@output_path = Propshaft::OutputPath.new(Pathname.new("#{__dir__}/../fixtures/output"), @manifest)
end

test "files" do
files = @output_path.files

file = files["one-f2e1ec14d6856e1958083094170ca6119c529a73.txt"]
assert_equal "one.txt", file[:logical_path]
assert_equal "f2e1ec14d6856e1958083094170ca6119c529a73", file[:digest]
assert file[:mtime].is_a?(Time)
end

test "clean always keeps most current versions" do
@output_path.clean(0, 0)
assert @output_path.path.join(@manifest["one.txt"])
assert @output_path.path.join(@manifest[".manifest.json"])
end

test "clean keeps versions of assets that no longer exist" do
removed = output_asset("no-longer-in-manifest.txt", "current")
@output_path.clean(1, 0)
assert File.exists?(removed)
ensure
FileUtils.rm(removed) if File.exists?(removed)
end

test "clean keeps the correct number of versions" do
old = output_asset("by_count.txt", "old", created_at: Time.now - 300)
current = output_asset("by_count.txt", "current", created_at: Time.now - 180)

@output_path.clean(1, 0)

assert File.exists?(current)
assert_not File.exists?(old)
ensure
FileUtils.rm(old) if File.exists?(old)
FileUtils.rm(current) if File.exists?(current)
end

test "clean keeps all versions under a certain age" do
old = output_asset("by_age.txt", "old")
current = output_asset("by_age.txt", "current")

@output_path.clean(0, 3600)

assert File.exists?(current)
assert File.exists?(old)
ensure
FileUtils.rm(old) if File.exists?(old)
FileUtils.rm(current) if File.exists?(current)
end

private
def output_asset(filename, content, created_at: Time.now)
asset = Propshaft::Asset.new(nil, logical_path: filename)
asset.stub :content, content do
output_path = @output_path.path.join(asset.digested_path)
`touch -mt #{created_at.strftime('%y%m%d%H%M')} #{output_path}`
output_path
end
end
end

0 comments on commit 5cd9716

Please sign in to comment.