270 lines
10 KiB
Text
270 lines
10 KiB
Text
|
|
#!/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:
|
||
|
|
# 
|
||
|
|
#
|
||
|
|
# 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 }
|
||
|
|
}
|