require 'noumenon/content_repository' require 'yaml' require 'fileutils' # A content repository which uses YAML files within the filesystem for storage. # # @example The structure of a filesystem repository # index.yml # |- about # |- index.yml # |- team.yml # |- company.yml # contact.yml # # @example A piece of content # template: team_member # name: Jon Wood # position: Head Honcho of Awesomeness # bio: Jon's just awesome, we'll leave it at that. # # @api public class Noumenon::ContentRepository::FileSystem < Noumenon::ContentRepository # @param [ Hash ] options A hash of options. # @option options [ String ] :path The path to access the repository at. # @api public def initialize(options = {}) unless options.key? :path raise ArgumentError.new("You must provide a path to the content repository: Noumenon::ContentRepository::FileSystem.new(path: '/tmp')") end super options end # Loads a piece of content from the repository. # # @param [ String ] path The path to load from. # @param [ Boolean ] check_for_index Whether sub-directories of the same name should be checked for an index.yml file # @return [ Hash, #each, nil ] The piece of content, or nil if it could not be found. # @api public def get(path, check_for_index = true) if content = read_file("#{path}.yml") YAML.load(content).symbolize_keys elsif check_for_index return get("#{path}/index", false) end end # Saves a piece of content to the repsitory. # # @see Noumenon::Repository#put # @api public def put(path, content = {}) create_tree(path) path = File.join(path, "index") if File.exist?(repository_path(path)) write_file "#{path}.yml", content.symbolize_keys.to_yaml end # Return any content items below the path specified. # # @see Noumenon::Repository#children # @api public def children(root = "/", depth = 1, include_root = true) root.gsub! /\/$/, '' pattern = File.join(repository_path(root), "*") items = Dir.glob(pattern).collect { |i| i.gsub(repository_path("/"), "/"). gsub(".yml", "") }.reject { |i| i.gsub(root, "").split("/").size > depth + 1 } items.delete(File.join(root, "index")) unless include_root items.collect { |item| path = item == "" ? "/" : item.gsub("index", "") # Loading every item probably isn't scalable, but it'll do for now. We can add caching # at a later date if needed. page = get(path).merge({ path: path }) page[:children] = children(page[:path], depth - 1, false) if depth - 1 > 0 page }.sort { |a, b| a[:path] <=> b[:path] } end private # Return the on-disk path to a repository item. def repository_path(path) File.join(options[:path], path) end # Creates any neccesary directories, and moves files that conflict with newly created # directories to index.yml def create_tree(path) path_on_disk = File.dirname(repository_path(path)) unless File.exists?(path_on_disk) FileUtils.mkdir_p(path_on_disk) create_indexes(path) end end def write_file(path, content) path_on_disk = repository_path(path) directory = File.dirname(path_on_disk) FileUtils.mkdir_p(directory) unless File.exist?(directory) File.open(path_on_disk, "w") { |f| f.print content } end def read_file(path) begin File.read(repository_path(path)) rescue Errno::ENOENT => e nil end end # Moves conflicting YAML files into directory/index.yml # # For example given # # /foo # /foo.yml # # /foo.yml will be moved to /foo/index.yml def create_indexes(path) sub_path = options[:path] path.split("/").each do |directory| move_to_index File.join(sub_path, directory) sub_path = File.join(sub_path, directory) end end def move_to_index(path) path_on_disk = path yaml_file = "#{path_on_disk}.yml" if File.exist? yaml_file FileUtils.mv yaml_file, File.join("#{path_on_disk}/index.yml") end end end