| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- 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
|