Files
volumen/lib/volumen/store.rb
T

105 lines
2.7 KiB
Ruby

# frozen_string_literal: true
require "fileutils"
require "securerandom"
require_relative "post"
module Volumen
# Reads and writes Markdown posts in a content directory.
#
# Posts live either at `content_dir/<slug>.md` (default language) or at
# `content_dir/<lang>/<slug>.md` (per-language subdirectory). Missing
# `slug`/`lang` frontmatter is derived from the file path.
class Store
attr_reader :content_dir
def initialize(content_dir, default_lang: nil)
@content_dir = File.expand_path(content_dir.to_s)
@default_lang = default_lang
end
def all
markdown_files.filter_map { |path| load_post(path) }
end
def find(slug, lang: nil)
all.find do |post|
post.slug == slug && (lang.nil? || post.lang == lang || post.all_langs?)
end
end
def save(post)
target = post.path || default_path_for(post)
FileUtils.mkdir_p(File.dirname(target))
File.write(target, post.to_file)
post.path = target
post
end
def delete(slug, lang: nil)
post = find(slug, lang: lang)
File.delete(post.path) if post&.path
post
end
def media_dir
File.join(@content_dir, "media")
end
def store_upload(original_name, source)
FileUtils.mkdir_p(media_dir)
name = safe_media_name(original_name)
File.open(File.join(media_dir, name), "wb") { |file| IO.copy_stream(source, file) }
"/media/#{name}"
end
def media_file(name)
path = File.join(media_dir, File.basename(name.to_s))
return nil unless File.file?(path) && within?(media_dir, path)
path
end
private
def markdown_files
Dir.glob(File.join(@content_dir, "**", "*.md"))
end
def load_post(path)
post = Post.parse(File.read(path))
post.metadata["slug"] ||= File.basename(path, ".md")
post.metadata["lang"] ||= lang_from_path(path) || @default_lang
post.path = path
post
rescue StandardError => e
warn "volumen: skipping #{path}: #{e.message}"
nil
end
def lang_from_path(path)
parent = File.dirname(File.expand_path(path))
return nil if parent == @content_dir
File.basename(parent)
end
def default_path_for(post)
File.join(@content_dir, "#{post.slug}.md")
end
def safe_media_name(original)
base = File.basename(original.to_s)
ext = File.extname(base).gsub(/[^a-zA-Z0-9.]/, "").downcase
stem = File.basename(base, File.extname(base))
.gsub(/[^a-zA-Z0-9_-]+/, "-").gsub(/-+/, "-").downcase
stem = "file" if stem.empty?
"#{SecureRandom.hex(4)}-#{stem}#{ext}"
end
def within?(dir, path)
File.expand_path(path).start_with?(File.expand_path(dir) + File::SEPARATOR)
end
end
end