website-htmx-rustelo-code/scripts/content/copy-content-images.nu

270 lines
10 KiB
Text
Raw Normal View History

2026-07-10 03:44:13 +01:00
#!/usr/bin/env nu
# Content Image Copier
#
# Copies images/ directories from page-bundle posts to the public static tree
# so they are served over HTTP.
#
# Page-bundle layout (source):
# {content_root}/{type}/{lang}/{category}/{slug}/
# ├── index.md
# ├── index.ncl
# └── images/
# └── *.{png,jpg,webp,svg,gif,avif}
#
# After copy (destination):
# {public_root}/content/{type}/{lang}/{category}/{slug}/images/
#
# Markdown references use absolute paths:
# ![alt](/content/{type}/{lang}/{category}/{slug}/images/file.png)
#
# Usage:
# nu copy-content-images.nu [--content-dir PATH] [--public-dir PATH]
# [--type TYPE] [--lang LANG] [--dry-run] [--verbose]
#
# Environment:
# SITE_CONTENT_PATH — overrides --content-dir default
# SITE_PUBLIC_PATH — overrides --public-dir default
def main [
--content-dir: string = "" # Content root (default: site/content)
--public-dir: string = "" # Public assets root (default: site/public)
--type: string = "" # Filter to one content type
--lang: string = "" # Filter to one language
--dry-run # Print what would be copied without writing
--verbose # Print per-file details
] {
let content_root = resolve_env "SITE_CONTENT_PATH" $content_dir "site/content"
let public_root = resolve_env "SITE_PUBLIC_PATH" $public_dir "site/public"
if not ($content_root | path exists) {
error make { msg: $"Content directory not found: ($content_root)" }
}
print $"(ansi cyan)Content Image Copier(ansi reset)"
print $" content : ($content_root)"
print $" public : ($public_root)/content"
if $dry_run { print $"(ansi yellow)[dry-run](ansi reset)" }
let types = discover_dirs $content_root $type
if ($types | is-empty) { print "No content types found."; return }
mut total_copied = 0
mut total_skipped = 0
mut total_errors = 0
for ct in $types {
let langs = discover_dirs $"($content_root)/($ct)" $lang
for language in $langs {
let lang_dir = $"($content_root)/($ct)/($language)"
let subdirs = discover_dirs $lang_dir ""
for sub in $subdirs {
let sub_dir = $"($lang_dir)/($sub)"
# Check if this subdir is itself a page-bundle (flat layout: no category)
if ($"($sub_dir)/index.md" | path exists) and ($"($sub_dir)/images" | path exists) {
# Flat page-bundle: treat as cat="" slug=$sub
let result = copy_category_images $lang_dir $ct $language "" $public_root $dry_run $verbose
$total_copied = $total_copied + $result.copied
$total_skipped = $total_skipped + $result.skipped
$total_errors = $total_errors + $result.errors
# Flat bundles handled at lang_dir level — break to avoid duplicate processing
break
} else {
# Category layout: subdir contains page-bundles
let result = copy_category_images $sub_dir $ct $language $sub $public_root $dry_run $verbose
$total_copied = $total_copied + $result.copied
$total_skipped = $total_skipped + $result.skipped
$total_errors = $total_errors + $result.errors
}
}
}
# Copy language-neutral shared images from {ct}/_images/
let shared = copy_shared_images $ct $content_root $public_root $dry_run $verbose
$total_copied = $total_copied + $shared.copied
$total_skipped = $total_skipped + $shared.skipped
$total_errors = $total_errors + $shared.errors
}
print ""
print $"(ansi green)Done(ansi reset) — copied: ($total_copied) skipped: ($total_skipped) errors: ($total_errors)"
if $total_errors > 0 { exit 1 }
}
# Copy images/ from one category directory
def copy_category_images [
cat_dir: string,
ct: string,
lang: string,
cat: string,
public_root: string,
dry_run: bool,
verbose: bool,
] {
if not ($cat_dir | path exists) { return { copied: 0, skipped: 0, errors: 0 } }
# Find slug dirs that have an images/ subdir.
# Two layouts:
# page-bundle: {slug}/index.md exists (projects, activities)
# flat-file: {slug}.md exists in cat_dir (blog, recipes)
let bundles = (ls $cat_dir)
| where type == "dir"
| get name
| each { |p| $p | path basename }
| where { |d| not ($d | str starts-with ".") }
| where { |d|
($"($cat_dir)/($d)/images" | path exists) and (
($"($cat_dir)/($d)/index.md" | path exists) or
($"($cat_dir)/($d).md" | path exists)
)
}
if ($bundles | is-empty) { return { copied: 0, skipped: 0, errors: 0 } }
mut copied = 0
mut skipped = 0
mut errors = 0
for slug in $bundles {
let src_images = $"($cat_dir)/($slug)/images"
let dst_images = if $cat == "" {
$"($public_root)/content/($ct)/($lang)/($slug)/images"
} else {
$"($public_root)/content/($ct)/($lang)/($cat)/($slug)/images"
}
# Collect image files
let img_files = try {
(ls $src_images)
| where type == "file"
| where { |e|
let ext = ($e.name | path parse | get extension | str downcase)
($ext in ["png", "jpg", "jpeg", "webp", "svg", "gif", "avif"])
}
| get name
} catch { [] }
if ($img_files | is-empty) {
if $verbose { print $" skip ($ct)/($lang)/($cat)/($slug)/images — no image files" }
continue
}
if $verbose { print $" ($ct)/($lang)/($cat)/($slug): ($img_files | length) images" }
for img in $img_files {
let dst_file = $"($dst_images)/($img | path basename)"
# Skip if destination is identical (same size)
if ($dst_file | path exists) {
let src_size = (ls $img | get size | first)
let dst_size = (ls $dst_file | get size | first)
if $src_size == $dst_size {
if $verbose { print $" skip ($img | path basename) — unchanged" }
$skipped = $skipped + 1
continue
}
}
if $dry_run {
print $" [dry-run] ($img | path basename) → ($dst_file)"
$copied = $copied + 1
continue
}
let result = try {
mkdir $dst_images
cp $img $dst_file
"ok"
} catch { |e|
print $" (ansi red)ERROR(ansi reset) ($img | path basename): ($e.msg)"
"error"
}
if $result == "ok" {
if $verbose { print $" (ansi green)copy(ansi reset) ($img | path basename)" }
$copied = $copied + 1
} else {
$errors = $errors + 1
}
}
}
{ copied: $copied, skipped: $skipped, errors: $errors }
}
# Copy {ct}/_images/ tree to public — language-neutral shared images
def copy_shared_images [
ct: string,
content_root: string,
public_root: string,
dry_run: bool,
verbose: bool,
] {
let src_root = $"($content_root)/($ct)/_images"
let dst_root = $"($public_root)/content/($ct)/_images"
if not ($src_root | path exists) { return { copied: 0, skipped: 0, errors: 0 } }
mut copied = 0; mut skipped = 0; mut errors = 0
# Walk cat/slug dirs
let cats = (ls $src_root) | where type == "dir" | get name | each { |p| $p | path basename }
for cat in $cats {
let cat_dir = $"($src_root)/($cat)"
let slugs = (ls $cat_dir) | where type == "dir" | get name | each { |p| $p | path basename }
| where { |d| $d != "pending" }
for slug in $slugs {
let slug_dir = $"($cat_dir)/($slug)"
let dst_slug = $"($dst_root)/($cat)/($slug)"
let img_files = try {
(ls $slug_dir)
| where type == "file"
| where { |e|
let ext = ($e.name | path parse | get extension | str downcase)
($ext in ["png", "jpg", "jpeg", "webp", "svg", "gif", "avif"])
}
| get name
} catch { [] }
for img in $img_files {
let dst_file = $"($dst_slug)/($img | path basename)"
if ($dst_file | path exists) {
let ss = (ls $img | get size | first)
let ds = (ls $dst_file | get size | first)
if $ss == $ds { $skipped = $skipped + 1; continue }
}
if $dry_run {
if $verbose { print $" [dry-run] ($img | path basename) → ($dst_file)" }
$copied = $copied + 1; continue
}
let result = try { mkdir $dst_slug; cp $img $dst_file; "ok" } catch { |e|
print $" (ansi red)ERROR(ansi reset) ($img | path basename): ($e.msg)"; "error"
}
if $result == "ok" {
if $verbose { print $" (ansi green)copy(ansi reset) ($img | path basename)" }
$copied = $copied + 1
} else { $errors = $errors + 1 }
}
}
}
{ copied: $copied, skipped: $skipped, errors: $errors }
}
def resolve_env [var_name: string, cli_arg: string, fallback: string] {
if $cli_arg != "" { return $cli_arg }
try { $env | get $var_name } catch { $fallback }
}
def discover_dirs [parent: string, filter: string] {
if not ($parent | path exists) { return [] }
let all = (ls $parent)
| where type == "dir"
| get name
| each { |p| $p | path basename }
| where { |d| not ($d | str starts-with ".") }
| where { |d| not ($d | str starts-with "_") } # exclude _images, _shared, etc.
if $filter != "" { $all | where { |d| $d == $filter } } else { $all }
}