From 2c8a134d4161ad9c83c2ae5332f933775e825b4e Mon Sep 17 00:00:00 2001 From: MeghdadFadaee Date: Sat, 13 Jun 2026 17:23:18 +0330 Subject: [PATCH] update --- README.md | 41 ++- index.html | 184 ++++++++++ landing.css | 978 ++++++++++++++++++++++++++++++++++++++++++++++++++++ landing.js | 16 + setup.sh | 332 ++++++++++++++++++ 5 files changed, 1549 insertions(+), 2 deletions(-) create mode 100644 index.html create mode 100644 landing.css create mode 100644 landing.js create mode 100755 setup.sh diff --git a/README.md b/README.md index 32dd809..98f9561 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,39 @@ -# nais -Nginx Autoindex Skin +# NAIS + +NAIS is a styled nginx autoindex page with convenient playlist download and +link-copying actions. + +[Website](https://meghdadfadaee.github.io/nais/) · +[GitHub](https://github.com/meghdadfadaee/nais) + +## Installation + +The interactive installer requires nginx, curl, root access through either the +current user or `sudo`, and an nginx service managed by `systemctl` or +`service`: + +```sh +curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh +``` + +It asks for: + +- `server_name`: one hostname or IP address. +- `root_path`: the absolute path whose files nginx should list. +- `assets_path`: where the NAIS CSS, JavaScript, and favicon are installed. + The default is `/var/www/nais-assets`. +- `certificates_path`: an optional directory containing `fullchain.pem` and + `privkey.pem`. + +Without a certificates path, the generated site serves HTTP on port 80. With a +certificates path, it serves HTTPS on port 443 and redirects HTTP to HTTPS. + +The installer detects common nginx virtual-host include directories. If none is +configured, it adds a managed `nais-sites/*.conf` include to nginx's main +configuration. It validates changes with `nginx -t` before reloading or starting +nginx. Re-running the installer refreshes the assets and replaces the managed +NAIS site configuration. + +The installer does not install nginx or obtain TLS certificates. +The nginx worker user must have permission to traverse and read `root_path`; the +installer does not change permissions on existing content. diff --git a/index.html b/index.html new file mode 100644 index 0000000..e22c3d8 --- /dev/null +++ b/index.html @@ -0,0 +1,184 @@ + + + + + + + + + + + + NAIS - Nginx Autoindex Skin + + + + + + + + + + +
+
+
+
Nginx autoindex, refined
+

Your files.
Better presented.

+

+ NAIS turns the default nginx directory listing into a fast, + focused, and modern file browser without changing how nginx works. +

+ +
+ curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh + +
+
+ +
+ +
+
+ +
files.example.com/releases/
+ +
+
+ Index of /releases/ +
+ + +
+
+
+ ../ + archive/ + latest/ + nais-v1.2.0.zip + checksums.txt + release-notes.pdf +

— nginx autoindex —

+
+
+
Lightweight. No framework.
+
+
+ +
+
3small assets
+
1install command
+
0runtime dependencies
+
100%nginx native
+
+ +
+
+ Why NAIS +

A directory listing people can actually use.

+

Small additions that make browsing and sharing files noticeably better.

+
+
+
+ 01 + +

Clear at a glance

+

Folders, parent navigation, and files use distinct colors so every directory is easy to scan.

+
+
+ 02 + +

Copy every link

+

Collect all file URLs from the current directory with one click.

+
+
+ 03 + +

Build playlists

+

Export media links as a ready-to-open playlist directly from the listing.

+
+
+ 04 + +

Setup that respects nginx

+

The installer detects common layouts, validates with nginx -t, and restores prior configuration if anything fails.

+
+
+
+ +
+
+ Get started +

From default to polished in one command.

+

+ Run the installer, answer four short prompts, and NAIS handles + the assets, nginx configuration, validation, and reload. +

+
    +
  • HTTP or optional HTTPS configuration
  • +
  • Automatic nginx include detection
  • +
  • Safe validation and rollback
  • +
+
+
+
+ +

install nais

+ +
+
$ curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh
+
+Server name: files.example.com
+Root path: /srv/files
+Assets path: /var/www/nais-assets
+Certificates path (optional):
+
+ Downloading NAIS assets...
+ Validating nginx configuration...
+ NAIS configured successfully.
+
+
+ +
+ +

Make the simple thing look good.

+

A focused autoindex theme, built from plain CSS and JavaScript.

+ Explore on GitHub +
+
+ + + + diff --git a/landing.css b/landing.css new file mode 100644 index 0000000..39e0764 --- /dev/null +++ b/landing.css @@ -0,0 +1,978 @@ +:root { + --bg: #0f111a; + --bg-deep: #0a0c13; + --surface: #151926; + --surface-bright: #1a1f30; + --row-hover: #1f2440; + --text: #e6e6eb; + --muted: #8b93a7; + --accent: #7aa2f7; + --folder: #9ece6a; + --parent: #f7768e; + --brand: #f7d759; + --border: #2a2f4a; + --page-width: 1180px; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + overflow-x: hidden; + background: var(--bg); + color: var(--text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.6; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +a { + color: inherit; +} + +.ambient { + position: absolute; + z-index: -1; + width: 500px; + height: 500px; + border-radius: 50%; + filter: blur(130px); + opacity: .14; + pointer-events: none; +} + +.ambient-one { + top: -220px; + right: -120px; + background: var(--accent); +} + +.ambient-two { + top: 550px; + left: -350px; + background: var(--folder); +} + +.site-header, +main, +footer { + width: min(calc(100% - 40px), var(--page-width)); + margin-inline: auto; +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 84px; + border-bottom: 1px solid rgba(42, 47, 74, .7); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 11px; + color: var(--text); + font-size: 18px; + font-weight: 800; + letter-spacing: .12em; + text-decoration: none; +} + +.brand img { + width: 42px; + height: 42px; +} + +.site-nav { + display: flex; + align-items: center; + gap: 30px; + color: var(--muted); + font-size: 14px; + font-weight: 600; +} + +.site-nav a { + text-decoration: none; + transition: color .2s ease; +} + +.site-nav a:hover { + color: var(--text); +} + +.site-nav .github-link { + padding: 8px 14px; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); +} + +.hero { + display: grid; + grid-template-columns: minmax(0, .88fr) minmax(520px, 1.12fr); + align-items: center; + gap: 70px; + min-height: 720px; + padding: 74px 0 92px; +} + +.eyebrow, +.section-kicker { + color: var(--accent); + font-size: 12px; + font-weight: 800; + letter-spacing: .16em; + text-transform: uppercase; +} + +.eyebrow { + display: flex; + align-items: center; + gap: 10px; +} + +.eyebrow span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--folder); + box-shadow: 0 0 0 5px rgba(158, 206, 106, .1), 0 0 18px rgba(158, 206, 106, .55); +} + +.hero h1 { + margin: 22px 0 22px; + font-size: clamp(54px, 6vw, 84px); + line-height: .98; + letter-spacing: -.065em; +} + +.hero h1 strong { + color: var(--accent); + font-weight: 800; +} + +.hero-text { + max-width: 560px; + margin: 0; + color: var(--muted); + font-size: 18px; +} + +.hero-actions { + display: flex; + gap: 12px; + margin-top: 34px; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 0 20px; + border: 1px solid transparent; + border-radius: 9px; + font-size: 14px; + font-weight: 750; + text-decoration: none; + transition: transform .2s ease, background .2s ease, border-color .2s ease; +} + +.button:hover { + transform: translateY(-2px); +} + +.button-primary { + background: var(--accent); + color: #0c1020; +} + +.button-primary:hover { + background: #96b8ff; +} + +.button-secondary { + border-color: var(--border); + background: rgba(21, 25, 38, .7); + color: var(--text); +} + +.button-secondary:hover { + border-color: var(--accent); + background: var(--surface-bright); +} + +.quick-command { + display: flex; + align-items: center; + gap: 10px; + max-width: 560px; + margin-top: 22px; + padding: 10px 10px 10px 14px; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(10, 12, 19, .65); +} + +.quick-command code { + min-width: 0; + overflow: hidden; + color: var(--muted); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.copy-button, +.terminal-bar button { + flex: 0 0 auto; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface); + color: var(--text); + cursor: pointer; + font: inherit; + font-size: 11px; + font-weight: 700; +} + +.copy-button { + padding: 6px 9px; +} + +.copy-button:hover, +.terminal-bar button:hover { + border-color: var(--accent); +} + +.preview-wrap { + position: relative; + perspective: 1200px; +} + +.preview-glow { + position: absolute; + inset: 12% 5% -2%; + z-index: -1; + border-radius: 50%; + background: rgba(122, 162, 247, .28); + filter: blur(65px); +} + +.browser { + overflow: hidden; + border: 1px solid #343b5b; + border-radius: 14px; + background: var(--bg-deep); + box-shadow: 0 35px 80px rgba(0, 0, 0, .38); + transform: rotateY(-4deg) rotateX(1deg); + animation: enter .8s ease-out both; +} + +.browser-bar { + display: grid; + grid-template-columns: 80px 1fr 80px; + align-items: center; + min-height: 48px; + padding: 0 14px; + border-bottom: 1px solid var(--border); + background: #121520; +} + +.traffic-lights { + display: flex; + gap: 6px; +} + +.traffic-lights span, +.terminal-bar > div span { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--parent); +} + +.traffic-lights span:nth-child(2), +.terminal-bar > div span:nth-child(2) { + background: var(--brand); +} + +.traffic-lights span:nth-child(3), +.terminal-bar > div span:nth-child(3) { + background: var(--folder); +} + +.address { + overflow: hidden; + padding: 5px 12px; + border: 1px solid #252a3f; + border-radius: 6px; + background: #0c0f17; + color: #737b91; + font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +} + +.lock { + color: var(--folder); +} + +.browser-menu { + justify-self: end; + color: var(--muted); + font-size: 12px; + letter-spacing: 2px; +} + +.file-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 68px; + padding: 0 22px; + border-bottom: 1px solid var(--border); + background: var(--surface); + font-size: 14px; + font-weight: 650; +} + +.file-header div { + display: flex; + gap: 7px; +} + +.file-header button { + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: transparent; + color: var(--text); + font-size: 9px; +} + +.file-list { + padding: 10px 0 20px; +} + +.file-list a { + display: flex; + align-items: center; + gap: 13px; + padding: 9px 22px; + color: var(--accent); + font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; + text-decoration: none; + transition: background .15s ease, color .15s ease; +} + +.file-list a:hover { + background: var(--row-hover); + color: #fff; +} + +.file-list .folder { + color: var(--folder); + font-weight: 700; +} + +.file-list .parent { + color: var(--parent); + font-weight: 700; +} + +.file-icon { + width: 9px; + height: 9px; + border-radius: 2px; + background: currentColor; + opacity: .9; +} + +.file-list p { + margin: 17px 22px 0; + color: var(--muted); + font: 9px ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.status-pill { + position: absolute; + right: -18px; + bottom: -25px; + display: flex; + align-items: center; + gap: 8px; + padding: 9px 13px; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(21, 25, 38, .92); + box-shadow: 0 15px 35px rgba(0, 0, 0, .3); + color: var(--muted); + font-size: 11px; + font-weight: 700; + backdrop-filter: blur(10px); +} + +.status-pill span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--folder); + box-shadow: 0 0 12px var(--folder); +} + +.metrics { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-block: 1px solid var(--border); +} + +.metrics div { + display: flex; + flex-direction: column; + gap: 2px; + padding: 28px; + border-right: 1px solid var(--border); +} + +.metrics div:last-child { + border-right: 0; +} + +.metrics strong { + color: var(--text); + font-size: 24px; +} + +.metrics span { + color: var(--muted); + font-size: 11px; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; +} + +.section { + padding: 130px 0 0; +} + +.section-heading { + max-width: 650px; + margin-bottom: 44px; +} + +.section h2, +.cta h2 { + margin: 12px 0 15px; + font-size: clamp(36px, 5vw, 58px); + line-height: 1.08; + letter-spacing: -.05em; +} + +.section-heading p, +.install-copy > p, +.cta p { + margin: 0; + color: var(--muted); + font-size: 17px; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.feature-card { + position: relative; + min-height: 300px; + overflow: hidden; + padding: 32px; + border: 1px solid var(--border); + border-radius: 14px; + background: linear-gradient(145deg, rgba(26, 31, 48, .9), rgba(17, 20, 31, .9)); + transition: border-color .25s ease, transform .25s ease; +} + +.feature-card:hover { + border-color: #424b72; + transform: translateY(-3px); +} + +.feature-card-wide { + grid-row: span 2; + min-height: 616px; +} + +.feature-number { + color: #626b83; + font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.feature-card h3 { + position: absolute; + bottom: 72px; + margin: 0; + font-size: 22px; + letter-spacing: -.025em; +} + +.feature-card p { + position: absolute; + right: 32px; + bottom: 27px; + left: 32px; + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.feature-visual { + position: absolute; + inset: 85px 45px auto; + display: grid; + gap: 14px; + padding: 30px; + border: 1px solid var(--border); + border-radius: 12px; + background: #0d1018; + transform: rotate(-3deg); +} + +.rows-visual span { + width: 78%; + height: 10px; + border-radius: 3px; + background: var(--accent); + box-shadow: -18px 0 0 -3px currentColor; +} + +.rows-visual .pink { + width: 42%; + background: var(--parent); + color: var(--parent); +} + +.rows-visual .green { + width: 65%; + background: var(--folder); + color: var(--folder); +} + +.rows-visual .short { + width: 48%; +} + +.rows-visual .blue { + color: var(--accent); +} + +.feature-icon { + position: absolute; + top: 76px; + left: 32px; + width: 62px; + height: 62px; + border: 1px solid var(--border); + border-radius: 16px; + background: var(--bg-deep); + box-shadow: 0 15px 35px rgba(0, 0, 0, .25); +} + +.link-icon::before, +.link-icon::after { + position: absolute; + width: 25px; + height: 10px; + border: 3px solid var(--accent); + border-radius: 10px; + content: ""; + transform: rotate(-35deg); +} + +.link-icon::before { + top: 18px; + left: 12px; +} + +.link-icon::after { + right: 11px; + bottom: 17px; +} + +.play-icon::before { + position: absolute; + top: 19px; + left: 23px; + border-top: 12px solid transparent; + border-bottom: 12px solid transparent; + border-left: 18px solid var(--folder); + content: ""; +} + +.install-card { + grid-column: 1 / -1; + grid-row: auto; + min-height: 360px; +} + +.terminal-mini { + position: absolute; + top: 78px; + right: 32px; + left: 32px; + display: grid; + gap: 16px; + padding: 20px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-deep); + font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.terminal-mini div { + display: flex; + gap: 5px; +} + +.terminal-mini i { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--parent); +} + +.terminal-mini i:nth-child(2) { + background: var(--brand); +} + +.terminal-mini i:nth-child(3), +.terminal-mini code:last-child b { + background: var(--folder); + color: var(--folder); +} + +.terminal-mini b { + color: var(--accent); +} + +.install-section { + display: grid; + grid-template-columns: .8fr 1.2fr; + align-items: center; + gap: 90px; +} + +.check-list { + display: grid; + gap: 12px; + margin: 28px 0 0; + padding: 0; + color: var(--text); + font-size: 13px; + list-style: none; +} + +.check-list li::before { + margin-right: 10px; + color: var(--folder); + content: "✓"; +} + +.terminal { + overflow: hidden; + border: 1px solid #343b5b; + border-radius: 14px; + background: var(--bg-deep); + box-shadow: 0 30px 70px rgba(0, 0, 0, .3); +} + +.terminal-bar { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + padding: 11px 14px; + border-bottom: 1px solid var(--border); + background: #121520; +} + +.terminal-bar > div { + display: flex; + gap: 6px; +} + +.terminal-bar p { + margin: 0; + color: var(--muted); + font-size: 10px; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; +} + +.terminal-bar button { + justify-self: end; + padding: 5px 9px; +} + +.terminal pre { + overflow-x: auto; + margin: 0; + padding: 30px; + color: var(--text); + font: 12px/2 ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.terminal .prompt { + color: var(--accent); +} + +.terminal .muted { + color: var(--muted); +} + +.terminal .success { + color: var(--folder); +} + +.cta { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 130px; + padding: 90px 20px; + border: 1px solid var(--border); + border-radius: 16px; + background: + radial-gradient(circle at 50% 0%, rgba(122, 162, 247, .18), transparent 48%), + var(--surface); + text-align: center; +} + +.cta h2 { + margin-top: 24px; +} + +.cta .button { + margin-top: 30px; +} + +footer { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + margin-top: 48px; + padding: 28px 0 36px; + border-top: 1px solid var(--border); + color: var(--muted); + font-size: 12px; +} + +.footer-brand { + font-size: 14px; +} + +.footer-brand img { + width: 34px; + height: 34px; +} + +footer p { + margin: 0; +} + +footer > a:last-child { + justify-self: end; + text-decoration: none; +} + +footer > a:last-child:hover { + color: var(--text); +} + +@keyframes enter { + from { + opacity: 0; + transform: translateY(18px) rotateY(-4deg) rotateX(1deg); + } + to { + opacity: 1; + transform: translateY(0) rotateY(-4deg) rotateX(1deg); + } +} + +@media (max-width: 980px) { + .hero { + grid-template-columns: 1fr; + gap: 70px; + padding-top: 80px; + } + + .hero-copy { + max-width: 650px; + } + + .preview-wrap { + max-width: 700px; + margin-inline: auto; + } + + .install-section { + grid-template-columns: 1fr; + gap: 45px; + } +} + +@media (max-width: 700px) { + .site-header, + main, + footer { + width: min(calc(100% - 28px), var(--page-width)); + } + + .site-header { + min-height: 72px; + } + + .site-nav { + gap: 14px; + } + + .site-nav a:not(.github-link) { + display: none; + } + + .hero { + min-height: auto; + padding: 65px 0 80px; + } + + .hero h1 { + font-size: clamp(48px, 15vw, 68px); + } + + .hero-text { + font-size: 16px; + } + + .hero-actions { + flex-wrap: wrap; + } + + .quick-command { + display: none; + } + + .browser { + transform: none; + } + + .browser-bar { + grid-template-columns: 58px 1fr 24px; + } + + .file-header { + align-items: flex-start; + flex-direction: column; + padding-block: 15px; + } + + .file-list a { + padding-inline: 16px; + } + + .status-pill { + right: 8px; + } + + .metrics { + grid-template-columns: repeat(2, 1fr); + } + + .metrics div { + padding: 20px; + border-bottom: 1px solid var(--border); + } + + .metrics div:nth-child(2) { + border-right: 0; + } + + .metrics div:nth-child(n+3) { + border-bottom: 0; + } + + .section { + padding-top: 95px; + } + + .feature-grid { + grid-template-columns: 1fr; + } + + .feature-card, + .feature-card-wide, + .install-card { + grid-column: auto; + grid-row: auto; + min-height: 330px; + } + + .feature-card-wide { + min-height: 450px; + } + + .install-card { + min-height: 390px; + } + + .install-card h3 { + bottom: 115px; + } + + .terminal pre { + padding: 22px; + font-size: 10px; + } + + .cta { + margin-top: 95px; + padding-block: 65px; + } + + footer { + grid-template-columns: 1fr auto; + } + + footer p { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + } +} diff --git a/landing.js b/landing.js new file mode 100644 index 0000000..2ad2dd9 --- /dev/null +++ b/landing.js @@ -0,0 +1,16 @@ +const installCommand = 'curl -fsSL https://meghdadfadaee.github.io/nais/setup.sh | sh'; + +document.querySelectorAll('[data-copy-command]').forEach((button) => { + button.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(installCommand); + const label = button.querySelector('.copy-label'); + label.textContent = 'Copied'; + window.setTimeout(() => { + label.textContent = 'Copy'; + }, 1800); + } catch { + window.prompt('Copy the install command:', installCommand); + } + }); +}); diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..53172a1 --- /dev/null +++ b/setup.sh @@ -0,0 +1,332 @@ +#!/bin/sh + +set -eu + +BASE_URL=${NAIS_BASE_URL:-https://meghdadfadaee.github.io/nais} +DEFAULT_ASSETS_PATH=/var/www/nais-assets +MANAGED_INCLUDE='include nais-sites/*.conf;' + +say() { + printf '%s\n' "$*" +} + +fail() { + printf 'nais setup: %s\n' "$*" >&2 + exit 1 +} + +need_command() { + command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" +} + +prompt() { + prompt_text=$1 + default_value=${2-} + + if [ -n "$default_value" ]; then + printf '%s [%s]: ' "$prompt_text" "$default_value" >/dev/tty + else + printf '%s: ' "$prompt_text" >/dev/tty + fi + + IFS= read -r answer &2 + fi +} + +trap cleanup EXIT +trap 'exit 1' HUP INT TERM + +if ! ( : /dev/tty ) 2>/dev/null; then + fail "/dev/tty is required for interactive setup" +fi +need_command curl +need_command nginx +need_command awk +need_command sed +need_command mktemp + +if [ "$(id -u)" -ne 0 ]; then + need_command sudo + sudo -v || fail "root privileges are required to configure nginx" +fi + +SERVER_NAME=$(prompt "Server name") +[ -n "$SERVER_NAME" ] || fail "server_name is required" +case "$SERVER_NAME" in + *[!A-Za-z0-9._:-]*) fail "server_name must be one hostname or IP address without whitespace" ;; +esac + +ROOT_PATH=$(prompt "Root path") +[ -n "$ROOT_PATH" ] || fail "root_path is required" +validate_absolute_path "$ROOT_PATH" "root_path" + +ASSETS_PATH=$(prompt "Assets path" "$DEFAULT_ASSETS_PATH") +validate_absolute_path "$ASSETS_PATH" "assets_path" + +CERTIFICATES_PATH=$(prompt "Certificates path (optional)") +if [ -n "$CERTIFICATES_PATH" ]; then + validate_absolute_path "$CERTIFICATES_PATH" "certificates_path" + run_root test -f "$CERTIFICATES_PATH/fullchain.pem" || + fail "certificate not found: $CERTIFICATES_PATH/fullchain.pem" + run_root test -f "$CERTIFICATES_PATH/privkey.pem" || + fail "certificate key not found: $CERTIFICATES_PATH/privkey.pem" +fi + +NGINX_BIN=$(command -v nginx) +NGINX_VERSION=$("$NGINX_BIN" -V 2>&1) +NGINX_PREFIX=$(printf '%s\n' "$NGINX_VERSION" | sed -n 's/.*--prefix=\([^ ]*\).*/\1/p') +NGINX_CONF=$(printf '%s\n' "$NGINX_VERSION" | sed -n 's/.*--conf-path=\([^ ]*\).*/\1/p') +[ -n "$NGINX_PREFIX" ] || NGINX_PREFIX=/etc/nginx +[ -n "$NGINX_CONF" ] || NGINX_CONF=$NGINX_PREFIX/conf/nginx.conf + +case "$NGINX_CONF" in + /*) ;; + *) NGINX_CONF=$NGINX_PREFIX/$NGINX_CONF ;; +esac +run_root test -f "$NGINX_CONF" || fail "nginx configuration not found: $NGINX_CONF" + +WORK_DIR=$(mktemp -d) +SITE_BACKUP=$WORK_DIR/site.conf.backup +MAIN_BACKUP=$WORK_DIR/nginx.conf.backup +GENERATED_CONFIG=$WORK_DIR/nais.conf +MAIN_CURRENT=$WORK_DIR/nginx.conf.current +MAIN_CHANGED=0 +MAIN_NEEDS_CHANGE=0 +SITE_EXISTED=0 +TRANSACTION_ACTIVE=0 +run_root cp "$NGINX_CONF" "$MAIN_CURRENT" +run_root chmod a+r "$MAIN_CURRENT" + +say "Downloading NAIS assets..." +for asset in autoindex.css autoindex.js favicon.ico; do + curl -fsSL "$BASE_URL/$asset" -o "$WORK_DIR/$asset" || + fail "failed to download $BASE_URL/$asset" +done + +ROOT_QUOTED=$(nginx_quote "$ROOT_PATH") +ASSETS_QUOTED=$(nginx_quote "$ASSETS_PATH") + +if [ -n "$CERTIFICATES_PATH" ]; then + CERT_QUOTED=$(nginx_quote "$CERTIFICATES_PATH/fullchain.pem") + KEY_QUOTED=$(nginx_quote "$CERTIFICATES_PATH/privkey.pem") + cat >"$GENERATED_CONFIG" <' ''; + sub_filter '' ''; + sub_filter_once on; + } + + location = /autoindex.css { root $ASSETS_QUOTED; } + location = /autoindex.js { root $ASSETS_QUOTED; } + location = /favicon.ico { root $ASSETS_QUOTED; } +} + +server { + listen 80; + server_name $SERVER_NAME; + return 301 https://$SERVER_NAME\$request_uri; +} +EOF +else + cat >"$GENERATED_CONFIG" <' ''; + sub_filter '' ''; + sub_filter_once on; + } + + location = /autoindex.css { root $ASSETS_QUOTED; } + location = /autoindex.js { root $ASSETS_QUOTED; } + location = /favicon.ico { root $ASSETS_QUOTED; } +} +EOF +fi + +ACTIVE_INCLUDE=$( + sed 's/[[:space:]]*#.*$//' "$MAIN_CURRENT" | + awk ' + $1 == "include" { + path = $2 + sub(/;$/, "", path) + gsub(/^"|"$/, "", path) + if (path ~ /sites-enabled\/\*$/) { print path; exit } + if (path ~ /conf\.d\/\*\.conf$/) { print path; exit } + if (path ~ /http\.d\/\*\.conf$/) { print path; exit } + if (path ~ /nais-sites\/\*\.conf$/) { print path; exit } + } + ' +) + +if [ -n "$ACTIVE_INCLUDE" ]; then + case "$ACTIVE_INCLUDE" in + /*) INCLUDE_PATTERN=$ACTIVE_INCLUDE ;; + *) INCLUDE_PATTERN=$NGINX_PREFIX/$ACTIVE_INCLUDE ;; + esac + SITE_DIR=${INCLUDE_PATTERN%/*} +else + SITE_DIR=$NGINX_PREFIX/nais-sites + cp "$MAIN_CURRENT" "$MAIN_BACKUP" + awk -v include_line=" $MANAGED_INCLUDE" ' + BEGIN { added = 0 } + !added && $0 ~ /^[[:space:]]*http[[:space:]]*\{/ { + print + print include_line + added = 1 + next + } + { print } + END { + if (!added) exit 42 + } + ' "$MAIN_CURRENT" >"$WORK_DIR/nginx.conf" || + fail "could not find the nginx http block in $NGINX_CONF" + MAIN_NEEDS_CHANGE=1 +fi + +SITE_CONFIG=$SITE_DIR/nais.conf +run_root mkdir -p "$ROOT_PATH" "$ASSETS_PATH" "$SITE_DIR" +mkdir -p "$WORK_DIR/assets-backup" + +for asset in autoindex.css autoindex.js favicon.ico; do + if run_root test -e "$ASSETS_PATH/$asset" && ! run_root test -f "$ASSETS_PATH/$asset"; then + fail "asset destination is not a regular file: $ASSETS_PATH/$asset" + fi + if run_root test -f "$ASSETS_PATH/$asset"; then + run_root cp "$ASSETS_PATH/$asset" "$WORK_DIR/assets-backup/$asset" + fi +done + +if run_root test -e "$SITE_CONFIG"; then + run_root test -f "$SITE_CONFIG" || fail "site config destination is not a regular file: $SITE_CONFIG" + run_root cp "$SITE_CONFIG" "$SITE_BACKUP" + SITE_EXISTED=1 +fi + +TRANSACTION_ACTIVE=1 +run_root chmod a+rx "$ASSETS_PATH" +for asset in autoindex.css autoindex.js favicon.ico; do + run_root cp "$WORK_DIR/$asset" "$ASSETS_PATH/$asset" + run_root chmod a+r "$ASSETS_PATH/$asset" +done +run_root cp "$GENERATED_CONFIG" "$SITE_CONFIG" +if [ "$MAIN_NEEDS_CHANGE" -eq 1 ]; then + MAIN_CHANGED=1 + run_root cp "$WORK_DIR/nginx.conf" "$NGINX_CONF" +fi + +say "Validating nginx configuration..." +if ! run_root "$NGINX_BIN" -t; then + fail "nginx validation failed" +fi + +if command -v systemctl >/dev/null 2>&1; then + if run_root systemctl is-active --quiet nginx; then + run_root systemctl reload nginx + else + run_root systemctl enable --now nginx + fi +elif command -v service >/dev/null 2>&1; then + run_root service nginx restart +else + fail "nginx configuration is installed, but no supported service manager was found" +fi + +TRANSACTION_ACTIVE=0 +say "NAIS configured successfully." +say "Site config: $SITE_CONFIG" +say "Assets: $ASSETS_PATH"