fix(build): render Nextcloud login page via askama, not include_str!(OUT_DIR)

The SvelteKit migration gated build.rs's static-dist/OUT_DIR generation
behind OXICLOUD_RUST_ASSETS=1 (early return), but login_v2_handler.rs
still embedded the page with
`include_str!(concat!(env!("OUT_DIR"), "/nextcloud-login.html"))`. With
OXICLOUD_RUST_ASSETS unset (the default), that file is never written to
OUT_DIR, so a clean `cargo build` failed to compile. (#489)

Migrate the page off include_str! to an askama template
(templates/nextcloud/login.html), mirroring the existing
DrivePickerTemplate in the same handler. This drops the only
compile-time dependency on the legacy build.rs pipeline, so the
OXICLOUD_RUST_ASSETS=1 CI workaround is no longer needed and is removed
from ci.yml, load-smoke.yml and load-nightly.yml.

Also fix the second failure on #489: with OXICLOUD_RUST_ASSETS=1 the
release pipeline panicked in copy_dir_recursive because `static/locales`
is now a symlink to frontend/static/locales. entry.file_type() reports
the link itself (not its target), so the symlinked directory was routed
to fs::copy and failed with "the source path is neither a regular file
nor a symlink to a regular file". Classify entries with fs::metadata,
which follows symlinks, so symlinked directories are traversed.

static/nextcloud-login.html is removed (its content moved into the
template; no other consumer) and dropped from build.rs HTML_INCLUDE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNCEMN6fC2xSmxqCVbstkd
This commit is contained in:
Claude
2026-06-19 17:13:49 +00:00
parent ddfc481616
commit 6023bca2e8
6 changed files with 38 additions and 31 deletions
-6
View File
@@ -14,12 +14,6 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
RUSTFLAGS: "-Dwarnings" RUSTFLAGS: "-Dwarnings"
DATABASE_URL: "postgres://postgres:postgres@localhost/oxicloud_test" DATABASE_URL: "postgres://postgres:postgres@localhost/oxicloud_test"
# Re-enable the legacy build.rs static-dist + OUT_DIR HTML pipeline.
# Required while login_v2_handler.rs still uses
# `include_str!(concat!(env!("OUT_DIR"), "/nextcloud-login.html"))`;
# without this, every cargo job fails to compile that file.
# Remove once the Nextcloud login page moves off include_str! (askama).
OXICLOUD_RUST_ASSETS: "1"
jobs: jobs:
-2
View File
@@ -22,8 +22,6 @@ on:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
# See ci.yml — required while login_v2_handler.rs uses include_str! against OUT_DIR.
OXICLOUD_RUST_ASSETS: "1"
jobs: jobs:
load: load:
-2
View File
@@ -18,8 +18,6 @@ on:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
# See ci.yml — required while login_v2_handler.rs uses include_str! against OUT_DIR.
OXICLOUD_RUST_ASSETS: "1"
jobs: jobs:
smoke: smoke:
+9 -3
View File
@@ -25,7 +25,6 @@ const HTML_INCLUDE: &[&str] = &[
"profile.html", "profile.html",
"admin.html", "admin.html",
"device-verify.html", "device-verify.html",
"nextcloud-login.html",
"share.html", "share.html",
]; ];
@@ -1239,14 +1238,21 @@ fn fnv_hash(data: &[u8]) -> String {
format!("{h:016x}") format!("{h:016x}")
} }
/// Recursively copy a directory tree. /// Recursively copy a directory tree, following symlinks.
///
/// `fs::metadata` (not `entry.file_type()`) is used to classify each entry so
/// that symlinked directories are traversed into rather than handed to
/// `fs::copy`. `static/locales` is a symlink to `frontend/static/locales`;
/// `entry.file_type()` reports the link itself, so the old code routed it to
/// the `fs::copy` branch and failed with "the source path is neither a regular
/// file nor a symlink to a regular file".
fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?; fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? { for entry in fs::read_dir(src)? {
let entry = entry?; let entry = entry?;
let src_path = entry.path(); let src_path = entry.path();
let dst_path = dst.join(entry.file_name()); let dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_dir() { if fs::metadata(&src_path)?.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?; copy_dir_recursive(&src_path, &dst_path)?;
} else { } else {
fs::copy(&src_path, &dst_path)?; fs::copy(&src_path, &dst_path)?;
+16 -5
View File
@@ -29,6 +29,14 @@ struct DrivePickerTemplate {
drives: Vec<DriveOption>, drives: Vec<DriveOption>,
} }
/// The Nextcloud Login Flow v2 "Grant Access" page. Rendered server-side via
/// askama (no template variables — the username/password are collected by the
/// embedded form) instead of `include_str!` so the build no longer depends on
/// the legacy `build.rs` static-asset pipeline / `OUT_DIR`.
#[derive(Template)]
#[template(path = "nextcloud/login.html")]
struct NextcloudLoginTemplate;
// Home identification is via `position_of_user_home_root_folder` from // Home identification is via `position_of_user_home_root_folder` from
// `domain::repositories::drive_repository` — a generic helper that // `domain::repositories::drive_repository` — a generic helper that
// keys off `drives.default_for_user == user_id` rather than folder // keys off `drives.default_for_user == user_id` rather than folder
@@ -36,7 +44,7 @@ struct DrivePickerTemplate {
// picker UX. // picker UX.
/// Serve an HTML page with a Content-Security-Policy header as defense-in-depth. /// Serve an HTML page with a Content-Security-Policy header as defense-in-depth.
fn html_with_csp(html: &'static str) -> Response { fn html_with_csp(html: String) -> Response {
( (
[( [(
header::CONTENT_SECURITY_POLICY, header::CONTENT_SECURITY_POLICY,
@@ -160,10 +168,13 @@ pub async fn handle_login_page(
return StatusCode::NOT_FOUND.into_response(); return StatusCode::NOT_FOUND.into_response();
} }
html_with_csp(include_str!(concat!( match NextcloudLoginTemplate.render() {
env!("OUT_DIR"), Ok(html) => html_with_csp(html),
"/nextcloud-login.html" Err(e) => {
))) tracing::error!(error = %e, "Login Flow v2: login page template render failed");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
} }
pub async fn handle_login_submit( pub async fn handle_login_submit(
@@ -19,40 +19,40 @@
</div> </div>
<div class="auth-logo-text">OxiCloud</div> <div class="auth-logo-text">OxiCloud</div>
</div> </div>
<h1 class="auth-title">Grant Access</h1> <h1 class="auth-title">Grant Access</h1>
<p class="auth-subtitle"> <p class="auth-subtitle">
A Nextcloud client is requesting access to your account. A Nextcloud client is requesting access to your account.
</p> </p>
<form class="auth-form" method="POST" id="login-flow-form"> <form class="auth-form" method="POST" id="login-flow-form">
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="user">Username</label> <label class="auth-label" for="user">Username</label>
<input <input
type="text" type="text"
id="user" id="user"
name="user" name="user"
class="auth-input" class="auth-input"
placeholder="Enter your username" placeholder="Enter your username"
required required
autocomplete="username" autocomplete="username"
autofocus autofocus
> >
</div> </div>
<div class="auth-input-group"> <div class="auth-input-group">
<label class="auth-label" for="password">Password</label> <label class="auth-label" for="password">Password</label>
<input <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
class="auth-input" class="auth-input"
placeholder="Enter your password" placeholder="Enter your password"
required required
autocomplete="current-password" autocomplete="current-password"
> >
</div> </div>
<button type="submit" class="auth-button" id="password-submit">Grant Access</button> <button type="submit" class="auth-button" id="password-submit">Grant Access</button>
</form> </form>
@@ -65,7 +65,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/js/views/nextcloud/login.js"></script> <script src="/js/views/nextcloud/login.js"></script>
</body> </body>
</html> </html>