105 lines
2.7 KiB
Ruby
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
|