mirror of https://github.com/freeCodeCamp/devdocs
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.
208 lines
3.8 KiB
208 lines
3.8 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 size(path)
|
|
path = expand_path(path)
|
|
file_size(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 file_size(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
|