You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devdocs/lib/docs/storage/abstract_store.rb

199 lines
3.7 KiB

require 'pathname'
module Docs
class AbstractStore
class InvalidPathError < StandardError; end
class LockError < StandardError; end
include Instrumentable
def initialize(path)
path = Pathname.new(path).cleanpath
raise ArgumentError if path.relative?
@root_path = @working_path = path.freeze
end
def root_path
@root_path.to_s
end
def working_path
@working_path.to_s
end
def expand_path(path)
join_paths @working_path, path
end
def open(path, &block)
if block_given?
open_yield_close(path, &block)
else
set_working_path join_paths(@root_path, path)
end
end
def close
set_working_path @root_path
end
def read(path)
path = expand_path(path)
read_file(path) if file_exist?(path)
end
def write(path, value)
path = expand_path(path)
touch(path)
if file_exist?(path)
update(path, value)
else
create(path, value)
end
end
def delete(path)
path = expand_path(path)
if file_exist?(path)
destroy(path)
true
end
end
def exist?(path)
file_exist? expand_path(path)
end
def mtime(path)
path = expand_path(path)
file_mtime(path) if file_exist?(path)
end
def each(&block)
list_files(working_path, &block)
end
def replace(path = nil, &block)
if path
return open(path) { replace(&block) }
else
lock { track_touched { yield.tap { delete_untouched } } }
end
end
private
def read_file(path)
raise NotImplementedError
end
def create_file(path, value)
raise NotImplementedError
end
def update_file(path, value)
raise NotImplementedError
end
def delete_file(path)
raise NotImplementedError
end
def file_exist?(path)
raise NotImplementedError
end
def file_mtime(path)
raise NotImplementedError
end
def list_files(path, &block)
raise NotImplementedError
end
def set_working_path(path)
@working_path = Pathname.new(path).freeze if assert_unlocked
end
def join_paths(base, path)
base = Pathname.new(base).cleanpath
path = Pathname.new(path).cleanpath
path = base + path unless path.absolute?
unless File.join(path, '').start_with? File.join(base, '')
raise InvalidPathError, "Tried accessing #{path} outside #{base}"
end
path.to_s
end
def open_yield_close(path)
working_path_was = working_path
open(path)
yield
ensure
set_working_path working_path_was
end
def create(path, value)
instrument 'create.store', path: path do
create_file(path, value)
end
end
def update(path, value)
instrument 'update.store', path: path do
update_file(path, value)
end
end
def destroy(path)
instrument 'destroy.store', path: path do
delete_file(path)
end
end
def lock
assert_unlocked
@locked = true
yield
ensure
@locked = false
end
def assert_unlocked
raise LockError if @locked
true
end
def track_touched
@touched = []
yield
ensure
@touched = nil
end
def touch(path)
@touched << path if @touched
end
def touched?(path)
dir = File.join(path, '')
@touched.any? do |touched_path|
touched_path == path || touched_path.start_with?(dir)
end
end
def delete_untouched
return if @touched.empty?
each do |path|
destroy(path) unless touched?(path)
end
end
end
end