# frozen_string_literal: true require 'digest' module AceOfBase # Implements our database. class Storage include Enumerable STORAGE_DIR = File.expand_path('../../data/', __dir__) Error = Class.new(AceOfBase::Error) def initialize(storage_dir = STORAGE_DIR) @storage_dir = storage_dir ensure_data_dir_exists_and_is_writable! end def store(record) with_write_locked_file_for_record(record) do |file| file.write(RecordSerialiser.encode(record)) end end def retrieve(project, shot, version) with_read_locked_file(hash_for(project, shot, version)) do |file| RecordSerialiser.decode(file.read) end end def each Dir['*', base: storage_dir].each do |path| with_read_locked_file(path) do |file| yield RecordSerialiser.decode(file.read) end end end private attr_reader :storage_dir def ensure_data_dir_exists_and_is_writable! raise Error, format('Storage directory (%p) does not exist.', storage_dir) unless File.directory?(storage_dir) raise Error, format('Storage directory (%p) is not writable.', storage_dir) unless File.writable?(storage_dir) end def with_write_locked_file_for_record(record) target = File.join(storage_dir, hash_for(record.project, record.shot, record.version)) File.open(target, File::CREAT | File::WRONLY, 0o600) do |file| raise Error, 'Unable to acquire exclusive lock.' unless file.flock(File::LOCK_EX | File::LOCK_NB) result = yield file file.flock(File::LOCK_UN) result end end def with_read_locked_file(path) target = File.join(storage_dir, path) File.open(target, File::RDONLY) do |file| file.flock(File::LOCK_SH) result = yield file file.flock(File::LOCK_UN) result end rescue Errno::ENOENT raise Error, 'Record not found' end def hash_for(project, shot, version) Digest::SHA1.hexdigest("#{project}|#{shot}|#{version}") end end end