Rename the project to dovenest

- Binary, UI, README, container, compose and CI names: filebrowser-ng
  becomes dovenest; new logo, a dove in a woven nest
- Clean break, no compatibility shims: env vars DOVENEST_*, cookies
  dovenest_session and dovenest_share_{id}, localStorage keys dovenest.*,
  container paths /var/lib/dovenest and /var/cache/dovenest, WebDAV and
  CalDAV realm "dovenest", WebAuthn RP name (the RP ID is unchanged)
- Unshipped CalDAV identifiers: dovenest.invalid addresses,
  urn:dovenest:sync: tokens, PRODID and principal UUID seed
- README credits filebrowser/filebrowser as the starting point

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AuthorKonata <konata@posteo.jp>
Date
Commitd3f65c46f08d23312532d8f8d824450f37b4537d
Parent67f31df
55 files changed, 188 insertions(+), 178 deletions(-)
▾M.gitignore
@@ -3,7 +3,7 @@
/web/dist
/server/dist
/web/cm6.js
filebrowser-ng
dovenest
*.sqlite
*.sqlite-wal
*.sqlite-shm
▾M.hearthforge-ci.toml
@@ -92,7 +92,7 @@ run_sh = "cd project && just test && cargo test -p web"
name = "build"
timeout = 2400
run_sh = "cd project && just build"
publish_file = ["/ci/cache/target/release/filebrowser-ng"]
publish_file = ["/ci/cache/target/release/dovenest"]
# `just e2e` would rebuild the frontend. The build step already staged
# server/dist, so run the embedded suite against it directly.
@@ -106,8 +106,8 @@ name = "release"
run_if = 'test -n "${CI_COMMIT_TAG}"'
run_sh = """
cd project
install -Dm755 "${CARGO_TARGET_DIR}/release/filebrowser-ng" \
"dist/filebrowser-ng-${CI_COMMIT_TAG}-x86_64-linux-musl"
install -Dm755 "${CARGO_TARGET_DIR}/release/dovenest" \
"dist/dovenest-${CI_COMMIT_TAG}-x86_64-linux-musl"
"""
publish_gzip = ["/ci/build/project/dist/"]
@@ -126,7 +126,7 @@ timeout = 900
run_sh = """
cd project
mkdir -p ci-bin
cp "${CARGO_TARGET_DIR}/release/filebrowser-ng" ci-bin/filebrowser-ng
cp "${CARGO_TARGET_DIR}/release/dovenest" ci-bin/dovenest
echo "$REGISTRY_PASSWORD" | podman-remote login "${CI_REGISTRY%%/*}" -u admin --password-stdin
▾MContainerfile
@@ -35,14 +35,14 @@ COPY . /src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
just build \
&& cp target/release/filebrowser-ng /filebrowser-ng
&& cp target/release/dovenest /dovenest
# ── prebuilt ─────────────────────────────────────────────────────────────────
# CI has the binary already. It puts it at ci-bin/filebrowser-ng and selects
# CI has the binary already. It puts it at ci-bin/dovenest and selects
# this stage with --build-arg BIN_STAGE=prebuilt. BuildKit and buildah do not
# build a stage nobody references, so a normal build needs no ci-bin/.
FROM scratch AS prebuilt
COPY ci-bin/filebrowser-ng /filebrowser-ng
COPY ci-bin/dovenest /dovenest
# ── runtime ──────────────────────────────────────────────────────────────────
FROM alpine:${ALPINE_VERSION}
@@ -52,15 +52,15 @@ ARG BIN_STAGE=build
# ffmpeg is only for video thumbnails, and it is most of the image: it pulls
# ~120 MiB of codec libraries against 8 MiB for the rest
RUN apk add --no-cache ca-certificates ffmpeg \
&& mkdir -p /data /var/lib/filebrowser /var/cache/filebrowser
&& mkdir -p /data /var/lib/dovenest /var/cache/dovenest
COPY --from=${BIN_STAGE} /filebrowser-ng /usr/local/bin/filebrowser-ng
COPY --from=${BIN_STAGE} /dovenest /usr/local/bin/dovenest
EXPOSE 8080
VOLUME ["/data", "/var/lib/filebrowser"]
VOLUME ["/data", "/var/lib/dovenest"]
HEALTHCHECK --interval=30s --start-period=10s --timeout=5s --retries=3 \
CMD wget -qO /dev/null http://127.0.0.1:8080/ || exit 1
ENTRYPOINT ["/usr/local/bin/filebrowser-ng"]
CMD ["--root", "/data", "--db", "/var/lib/filebrowser/db.sqlite", "--bind", "0.0.0.0"]
ENTRYPOINT ["/usr/local/bin/dovenest"]
CMD ["--root", "/data", "--db", "/var/lib/dovenest/db.sqlite", "--bind", "0.0.0.0"]
▾MREADME.md
@@ -1,7 +1,11 @@
# filebrowser-ng
# dovenest
A self-hosted web file browser. One static binary, one SQLite file, no
external services.
A self-hosted web file browser with calendars and contacts. One static
binary, one SQLite file, no external services.
The project started as a nod to
[filebrowser/filebrowser](https://github.com/filebrowser/filebrowser) and was
called filebrowser-ng until it grew CalDAV and CardDAV.
![Screenshot of the file browser](docs/screenshot.png)
@@ -61,7 +65,7 @@ docker compose up -d # or: podman compose up -d
| Host path | Container path | Content |
|--------------------|-----------------------|-------------------------------|
| `./data` | `/data` | The browsed files |
| `./filebrowser-db` | `/var/lib/filebrowser` | SQLite database: users, shares, settings |
| `./dovenest-db` | `/var/lib/dovenest` | SQLite database: users, shares, settings |
Both folders are created on first start. The image is Alpine plus the static
binary and exposes port 8080.
@@ -69,7 +73,7 @@ binary and exposes port 8080.
### Binary
```bash
filebrowser-ng --root /srv/files --db /var/lib/filebrowser/db.sqlite --bind 0.0.0.0
dovenest --root /srv/files --db /var/lib/dovenest/db.sqlite --bind 0.0.0.0
```
| Flag | Default | Description |
@@ -78,9 +82,9 @@ filebrowser-ng --root /srv/files --db /var/lib/filebrowser/db.sqlite --bind 0.0.
| `--db` | _(required)_ | SQLite file. Created when missing. |
| `--port` | `8080` | Listen port |
| `--bind` | `127.0.0.1` | Listen address. `0.0.0.0` exposes the server beyond localhost. |
| `--root-name` | folder name | Display name of the root folder. Also `FILEBROWSER_ROOT_NAME`. |
| `--public-url` | off | Public base URL, e.g. `https://files.example.com`. Share and WebDAV links are built from it instead of the browser's address, and an `https` scheme marks cookies `Secure` and binds passkeys to that origin. Also `FILEBROWSER_PUBLIC_URL`. |
| `--cache` | off | Folder for the thumbnail cache. Turns thumbnails on. Also `FILEBROWSER_CACHE`. |
| `--root-name` | folder name | Display name of the root folder. Also `DOVENEST_ROOT_NAME`. |
| `--public-url` | off | Public base URL, e.g. `https://files.example.com`. Share and WebDAV links are built from it instead of the browser's address, and an `https` scheme marks cookies `Secure` and binds passkeys to that origin. Also `DOVENEST_PUBLIC_URL`. |
| `--cache` | off | Folder for the thumbnail cache. Turns thumbnails on. Also `DOVENEST_CACHE`. |
Log level comes from `RUST_LOG` (`error`, `warn`, `info`, `debug`, `trace`).
@@ -88,7 +92,7 @@ Log level comes from `RUST_LOG` (`error`, `warn`, `info`, `debug`, `trace`).
The server speaks plain HTTP. Put a TLS-terminating proxy in front of it and
set `--public-url` to the address the browser uses, e.g.
`FILEBROWSER_PUBLIC_URL=https://files.example.com` in `compose.yml`. The
`DOVENEST_PUBLIC_URL=https://files.example.com` in `compose.yml`. The
server cannot see the proxy's TLS by itself, so without that URL it assumes
plain HTTP: cookies lose `Secure` and passkeys are bound to the wrong origin.
@@ -101,8 +105,8 @@ Grid view shows a small preview of an image or video instead of an icon.
Point `--cache` at a folder to turn them on:
```bash
filebrowser-ng --root /srv/files --db /var/lib/filebrowser/db.sqlite \
--cache /var/cache/filebrowser
dovenest --root /srv/files --db /var/lib/dovenest/db.sqlite \
--cache /var/cache/dovenest
```
Notes:
@@ -417,13 +421,13 @@ so nothing can reach anyone outside by mistake:
| Principal | Address |
|-----------|---------|
| Account | `<name>@filebrowser.invalid` |
| Room | `<name>@rooms.filebrowser.invalid` |
| Resource | `<name>@resources.filebrowser.invalid` |
| Account | `<name>@dovenest.invalid` |
| Room | `<name>@rooms.dovenest.invalid` |
| Resource | `<name>@resources.dovenest.invalid` |
A name with characters an email address cannot hold keeps them
percent-encoded: account `marc@example.com` becomes
`marc%40example.com@filebrowser.invalid`. A dot is encoded too when it
`marc%40example.com@dovenest.invalid`. A dot is encoded too when it
would lead, trail or repeat.
An outside address in an invitation is kept, but marked as not delivered
@@ -445,7 +449,7 @@ instances that collide are declined.
### Deleting an account, room or resource
The server removes the deleted one from everyone else's events in the same
step. Its address becomes `<name>-<number>@deleted.filebrowser.invalid`,
step. Its address becomes `<name>-<number>@deleted.dovenest.invalid`,
which reaches no one, and its attendee entries show status 3.7. The
display name stays. Copies of meetings it organized are marked cancelled.
Apps pick up the change at their next sync. A new account with the same
@@ -504,7 +508,7 @@ cargo install trunk
|------------------|------------------------------------------------------------------------|
| `just dev` | Backend on :8081 with a dev root under `.dev/` |
| `just dev-web` | Frontend on :8080 with hot reload, proxying `/api` to :8081 |
| `just build` | Release binary with the frontend embedded, `target/release/filebrowser-ng` |
| `just build` | Release binary with the frontend embedded, `target/release/dovenest` |
| `just run` | Build and run the release binary against the dev folders |
| `just test` | Server, API type and pimdav tests |
| `just e2e` | Build the real frontend and run the server suite against the embedded build |
▾Mapi-types/src/lib.rs
@@ -1,4 +1,4 @@
//! The HTTP wire contract of filebrowser-ng in one place.
//! The HTTP wire contract of dovenest in one place.
//!
//! Both the server (axum) and the web frontend (wasm `fetch`) import these
//! endpoint paths, query params and serde types, so the two sides cannot
▾Mcompose.yml
@@ -1,15 +1,15 @@
services:
filebrowser:
dovenest:
build: .
image: filebrowser-ng:latest
image: dovenest:latest
ports:
- "8080:8080"
environment:
RUST_LOG: info # server log level: trace/debug/info/warn/error
# FILEBROWSER_PUBLIC_URL: https://files.example.com # public address: base of share links; https here marks cookies Secure and binds passkeys to it
# FILEBROWSER_ROOT_NAME: Media # UI name of the root folder (default: its folder name, here "data")
# FILEBROWSER_CACHE: /var/cache/filebrowser # turns grid thumbnails on; ffmpeg is in the image
# DOVENEST_PUBLIC_URL: https://files.example.com # public address: base of share links; https here marks cookies Secure and binds passkeys to it
# DOVENEST_ROOT_NAME: Media # UI name of the root folder (default: its folder name, here "data")
# DOVENEST_CACHE: /var/cache/dovenest # turns grid thumbnails on; ffmpeg is in the image
volumes:
- ./data:/data # browsed files (server --root)
- ./filebrowser-db:/var/lib/filebrowser # sqlite db: users, shares, settings
# - ./filebrowser-cache:/var/cache/filebrowser # optional: keeps thumbnails across restarts
- ./dovenest-db:/var/lib/dovenest # sqlite db: users, shares, settings
# - ./dovenest-cache:/var/cache/dovenest # optional: keeps thumbnails across restarts
▾Mjustfile
@@ -1,4 +1,4 @@
# filebrowser-ng developer tasks
# dovenest developer tasks
dev-root := '.dev/root'
dev-db := '.dev/db.sqlite'
@@ -48,7 +48,7 @@ build-cm:
# Run the production binary against the dev folders.
run: dev-data build
./target/release/filebrowser-ng --root {{dev-root}} --db {{dev-db}} --cache {{dev-cache}}
./target/release/dovenest --root {{dev-root}} --db {{dev-db}} --cache {{dev-cache}}
# Server and pimdav test suites (unit + integration tests).
test:
▾Mpimdav/README.md
@@ -219,7 +219,7 @@ RRULE is valid and common. The occurrence cap protects the CPU instead.
### Sync
- A token is `urn:fbng:sync:{collection id}-{seq}`. The id makes a token
- A token is `urn:dovenest:sync:{collection id}-{seq}`. The id makes a token
of a deleted collection invalid for a new one at the same URL.
- A token of another collection, or one newer than the collection, fails
with `valid-sync-token`.
@@ -455,7 +455,7 @@ would get the same `mailto:` address and take the old one's place in every
meeting.
- Every ORGANIZER and ATTENDEE that names it gets the tombstone address
`mailto:<name>-<id>@deleted.filebrowser.invalid`. Its parameters stay,
`mailto:<name>-<id>@deleted.dovenest.invalid`. Its parameters stay,
CN among them.
- Such an ATTENDEE gets SCHEDULE-STATUS 3.7. A later message to it is not
delivered, with status 3.7 as for any unknown address in our domains.
▾Mpimdav/src/bundle.rs
@@ -12,7 +12,7 @@ use crate::text::{
escape_text, logical_lines, name, param, param_parts, unescape_text, unfold, value, value_start,
};
const PRODID: &str = "PRODID:-//filebrowser-ng//pimdav//EN";
const PRODID: &str = "PRODID:-//dovenest//pimdav//EN";
/// The properties a busy-only event keeps.
const BUSY_PROPS: [&str; 13] = [
▾Mpimdav/src/contact.rs
@@ -120,7 +120,7 @@ fn event(uid: &str, sign: &str, name: &str, year: Option<i32>, month: u32, day:
let summary = fold(&summary, "\r\n");
// A fixed DTSTAMP keeps the ETag stable while the contact is unchanged.
format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//filebrowser-ng//pimdav//EN\r\n\
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dovenest//pimdav//EN\r\n\
BEGIN:VEVENT\r\nUID:{uid}\r\nDTSTAMP:19700101T000000Z\r\n\
DTSTART;VALUE=DATE:{start:04}{month:02}{day:02}\r\nDURATION:P1D\r\n\
RRULE:{rule}\r\n{summary}TRANSP:TRANSPARENT\r\n\
▾Mpimdav/src/freebusy.rs
@@ -129,7 +129,7 @@ fn render(
) -> String {
let t = |d: DateTime<Utc>| d.format("%Y%m%dT%H%M%SZ").to_string();
let mut out = format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//filebrowser-ng//pimdav//EN\r\n{method}\
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dovenest//pimdav//EN\r\n{method}\
BEGIN:VFREEBUSY\r\n{props}DTSTAMP:{}\r\nDTSTART:{}\r\nDTEND:{}\r\n",
t(now),
t(range.start),
▾Mpimdav/tests/itip.rs
@@ -5,8 +5,8 @@ use chrono::{TimeZone, Utc};
use pimdav::calcard::icalendar::ICalendar;
use pimdav::itip::{self, Message, Method, Refused, Role};
const ALICE: &str = "mailto:alice@filebrowser.invalid";
const BOB: &str = "mailto:bob@filebrowser.invalid";
const ALICE: &str = "mailto:alice@dovenest.invalid";
const BOB: &str = "mailto:bob@dovenest.invalid";
const CAROL: &str = "mailto:carol@example.com";
fn is(who: &'static str) -> impl Fn(&str) -> bool {
@@ -386,7 +386,7 @@ fn sender_is_named_for_the_owner() {
fn rooms_answer_from_their_bookings() {
use pimdav::freebusy::{Busy, Period};
use pimdav::zone::Zone;
const ROOM: &str = "mailto:board@rooms.filebrowser.invalid";
const ROOM: &str = "mailto:board@rooms.dovenest.invalid";
let at = |d: u32, h: u32| Utc.with_ymd_and_hms(2026, 1, d, h, 0, 0).unwrap();
// The weekly meeting runs on 5, 12, 19 and 26 January, 10:00 to 11:00.
let copy = meeting(&format!("ATTENDEE;PARTSTAT=NEEDS-ACTION:{ROOM}\n"));
@@ -442,7 +442,7 @@ fn rooms_answer_from_their_bookings() {
#[test]
fn forget_a_deleted_principal() {
let gone = is(BOB);
let tomb = "mailto:bob-7@deleted.filebrowser.invalid";
let tomb = "mailto:bob-7@deleted.dovenest.invalid";
// In alice's meeting bob becomes a tombstone that reaches no one.
let org = format!(
▾Mpimdav/tests/principal.rs
@@ -14,14 +14,14 @@ fn search(body: &str) -> pimdav::principal::Search {
#[test]
fn property_search_and_calendarserver_search() {
let addresses = ["mailto:board@rooms.filebrowser.invalid".to_string()];
let addresses = ["mailto:board@rooms.dovenest.invalid".to_string()];
let room = Principal {
name: "board",
display: "Board Room",
addresses: &addresses,
kind: UserType::Room,
};
let alice_addresses = ["mailto:alice@filebrowser.invalid".to_string()];
let alice_addresses = ["mailto:alice@dovenest.invalid".to_string()];
let alice = Principal {
name: "alice",
display: "Alice",
@@ -68,7 +68,7 @@ fn directory_card_escapes_text() {
addresses: &[],
kind: UserType::Resource,
};
let text = card("urn:uuid:1", &p, "ops@resources.filebrowser.invalid");
let text = card("urn:uuid:1", &p, "ops@resources.dovenest.invalid");
assert!(text.contains(r"FN:Ops\; Night\, Shift"), "{text}");
assert!(text.contains("KIND:device\r\n"));
assert!(VCard::parse(&text).is_ok());
▾Mpimdav/tests/report.rs
@@ -357,7 +357,7 @@ fn bad_filters_are_valid_filter_conditions() {
#[test]
fn busy_time_follows_own_answer() {
const ME: &str = "mailto:room@rooms.filebrowser.invalid";
const ME: &str = "mailto:room@rooms.dovenest.invalid";
let me = |a: &str| a.eq_ignore_ascii_case(ME);
let body = format!(
"BEGIN:VEVENT\r\nUID:a\r\nDTSTART:20260101T100000Z\r\nDTEND:20260101T110000Z\r\nATTENDEE;PARTSTAT=DECLINED:{ME}\r\nEND:VEVENT\r\n\
@@ -386,9 +386,9 @@ fn outbox_free_busy_request() {
let body = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//t//EN\r\nMETHOD:REQUEST\r\n\
BEGIN:VFREEBUSY\r\nUID:fb-1\r\nDTSTAMP:20260101T000000Z\r\n\
DTSTART:20260105T000000Z\r\nDTEND:20260106T000000Z\r\n\
ORGANIZER:mailto:alice@filebrowser.invalid\r\n\
ATTENDEE:mailto:bob@filebrowser.invalid\r\n\
ATTENDEE:mailto:BOB@filebrowser.invalid\r\n\
ORGANIZER:mailto:alice@dovenest.invalid\r\n\
ATTENDEE:mailto:bob@dovenest.invalid\r\n\
ATTENDEE:mailto:BOB@dovenest.invalid\r\n\
ATTENDEE:mailto:dave@example.com\r\n\
END:VFREEBUSY\r\nEND:VCALENDAR\r\n";
let req = request(body.as_bytes()).unwrap();
@@ -396,10 +396,10 @@ fn outbox_free_busy_request() {
req.range,
utc("2026-01-05T00:00:00")..utc("2026-01-06T00:00:00")
);
assert_eq!(req.organizer, "mailto:alice@filebrowser.invalid");
assert_eq!(req.organizer, "mailto:alice@dovenest.invalid");
assert_eq!(
req.attendees,
["mailto:bob@filebrowser.invalid", "mailto:dave@example.com"]
["mailto:bob@dovenest.invalid", "mailto:dave@example.com"]
);
let busy = [pimdav::freebusy::Period {
@@ -412,7 +412,7 @@ fn outbox_free_busy_request() {
for line in [
"METHOD:REPLY",
"UID:fb-1",
"ATTENDEE:mailto:bob@filebrowser.invalid",
"ATTENDEE:mailto:bob@dovenest.invalid",
"FREEBUSY;FBTYPE=BUSY:20260105T100000Z/20260105T110000Z",
] {
assert!(got.contains(line), "{line} in {got}");
@@ -430,7 +430,7 @@ fn outbox_free_busy_request() {
for bad in [
body.replace("METHOD:REQUEST", "METHOD:PUBLISH"),
body.replace("DTEND:20260106T000000Z\r\n", ""),
body.replace("ORGANIZER:mailto:alice@filebrowser.invalid\r\n", ""),
body.replace("ORGANIZER:mailto:alice@dovenest.invalid\r\n", ""),
] {
let err = request(bad.as_bytes()).unwrap_err();
assert!(err.is(CALDAV, "valid-scheduling-message"), "{bad}");
▾Mserver/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2024"
license = "AGPL-3.0-or-later"
[[bin]]
name = "filebrowser-ng"
name = "dovenest"
path = "src/main.rs"
[dependencies]
@@ -94,5 +94,5 @@ optional = true
[features]
# Embed the frontend (built into server/dist by `just build`).
# Without this feature the server reads the frontend from disk at runtime
# (Trunk's output directory, or $FBNG_DIST), which is the dev flow.
# (Trunk's output directory, or $DOVENEST_DIST), which is the dev flow.
embedded = ["dep:rust-embed"]
▾Mserver/src/api/dav.rs
@@ -45,7 +45,7 @@ use crate::db::RootRow;
use crate::error::AppState;
/// The `WWW-Authenticate` realm. Clients show it in their password prompt.
const REALM: &str = "filebrowser-ng";
const REALM: &str = "dovenest";
// ---------------------------------------------------------------------------
// Routes
▾Mserver/src/api/pim.rs
@@ -62,7 +62,7 @@ const MAX_DEAD_PROPS: usize = 100;
/// The domain of the addresses users schedule with. `.invalid` is reserved
/// (RFC 2606), so nothing sent there can reach anyone.
pub(super) const MAIL_DOMAIN: &str = "filebrowser.invalid";
pub(super) const MAIL_DOMAIN: &str = "dovenest.invalid";
/// The ids of the generated collections, which no stored one has.
pub(super) const DIRECTORY: i64 = 0;
@@ -444,7 +444,7 @@ pub(super) fn etag_of(data: &[u8]) -> String {
/// A stable UUID per principal, for the `urn:uuid:` calendar user address.
pub(super) fn principal_uuid(id: i64) -> String {
let h = crate::hex(&Sha256::digest(format!("filebrowser-ng principal {id}"))[..16]);
let h = crate::hex(&Sha256::digest(format!("dovenest principal {id}"))[..16]);
format!(
"{}-{}-{}-{}-{}",
&h[..8],
@@ -1326,7 +1326,7 @@ fn privilege_set<'a>(names: impl IntoIterator<Item = (&'a str, &'a str)>) -> Ele
/// Carries the collection id, so a token handed out for a deleted
/// collection never matches the one that later takes its URL.
fn sync_token(id: i64, seq: i64) -> String {
format!("urn:fbng:sync:{id}-{seq}")
format!("urn:dovenest:sync:{id}-{seq}")
}
fn content_type(kind: PimKind, component: &str) -> String {
@@ -2362,7 +2362,7 @@ fn too_many() -> Response<Body> {
/// `(collection id, seq)` of a token [`sync_token`] made.
fn parse_sync_token(token: &str) -> Option<(i64, i64)> {
// The birthday calendar's id is negative.
let (id, seq) = token.strip_prefix("urn:fbng:sync:")?.rsplit_once('-')?;
let (id, seq) = token.strip_prefix("urn:dovenest:sync:")?.rsplit_once('-')?;
Some((id.parse().ok()?, seq.parse().ok()?))
}
▾Mserver/src/api/spa.rs
@@ -3,15 +3,15 @@ use axum::http::{HeaderMap, Method, StatusCode, header};
use axum::response::{IntoResponse, Response};
const DEV_HINT: &str = r#"<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>filebrowser-ng</title></head>
<html lang="en"><head><meta charset="utf-8"><title>dovenest</title></head>
<body style="font-family:system-ui;display:grid;place-items:center;min-height:100vh;margin:0;background:#f6f7f9">
<div style="max-width:520px;padding:24px;border:1px solid #e5e7eb;border-radius:12px;background:#fff">
<h1>filebrowser-ng</h1>
<h1>dovenest</h1>
<p>The frontend has not been built yet.</p>
<p>In development, run <code>just dev-web</code> in a second terminal and open
<code>http://localhost:8080</code> (Trunk serves the app and proxies <code>/api</code> to this backend).</p>
<p>If you hit this page directly from the backend, make sure Trunk's output
directory is visible — the server looks in <code>target/web</code> or <code>$FBNG_DIST</code>.</p>
directory is visible — the server looks in <code>target/web</code> or <code>$DOVENEST_DIST</code>.</p>
<p>For the production single binary: <code>just build</code>.</p>
</div></body></html>"#;
▾Mserver/src/archive.rs
@@ -375,7 +375,7 @@ mod tests {
let mut out = Vec::new();
let r = build(
ArchiveFormat::Zip,
Path::new("/nonexistent-filebrowser-ng-test-dir"),
Path::new("/nonexistent-dovenest-test-dir"),
"top",
&mut out,
);
@@ -384,7 +384,7 @@ mod tests {
let mut out = Vec::new();
let r = build(
ArchiveFormat::Tar,
Path::new("/nonexistent-filebrowser-ng-test-dir"),
Path::new("/nonexistent-dovenest-test-dir"),
"top",
&mut out,
);
▾Mserver/src/assets.rs
@@ -1,5 +1,5 @@
//! Static frontend assets: embedded at compile time (`--features embedded`)
//! or read from disk in the dev flow (Trunk's output dir or $FBNG_DIST).
//! or read from disk in the dev flow (Trunk's output dir or $DOVENEST_DIST).
use std::borrow::Cow;
#[cfg(not(feature = "embedded"))]
@@ -45,7 +45,7 @@ fn read(path: &str) -> Option<(Cow<'static, [u8]>, Option<String>)> {
#[cfg(not(feature = "embedded"))]
fn dev_dist_dir() -> PathBuf {
std::env::var_os("FBNG_DIST")
std::env::var_os("DOVENEST_DIST")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../web/dist"))
}
▾Mserver/src/auth.rs
@@ -6,7 +6,7 @@ use argon2::Argon2;
use argon2::password_hash::phc::PasswordHash;
use argon2::password_hash::{PasswordHasher, PasswordVerifier};
pub const COOKIE_NAME: &str = "fbng_session";
pub const COOKIE_NAME: &str = "dovenest_session";
/// 30 days.
pub const SESSION_MAX_AGE: u64 = 60 * 60 * 24 * 30;
@@ -236,7 +236,7 @@ pub fn clear_session_cookie(https: bool) -> String {
/// One cookie per share: a visitor may hold links to several protected
/// shares, and one shared name would let each unlock evict the last.
pub fn share_cookie_name(share_id: i64) -> String {
format!("fbng_share_{share_id}")
format!("dovenest_share_{share_id}")
}
/// Session cookie for an unlocked share. A session cookie (no `Max-Age`), so
@@ -275,7 +275,7 @@ mod tests {
#[test]
fn share_cookie_is_per_share_and_session_scoped() {
let c = share_cookie(7, "tok", false);
assert!(c.starts_with("fbng_share_7=tok;"));
assert!(c.starts_with("dovenest_share_7=tok;"));
assert!(c.contains("HttpOnly"));
// No Max-Age: the unlock must not outlive the browser session.
assert!(!c.contains("Max-Age"));
@@ -285,7 +285,9 @@ mod tests {
let mut h = HeaderMap::new();
h.insert(
header::COOKIE,
"fbng_share_7=abc; fbng_share_8=def".parse().unwrap(),
"dovenest_share_7=abc; dovenest_share_8=def"
.parse()
.unwrap(),
);
assert_eq!(parse_share_cookie(&h, 7).as_deref(), Some("abc"));
assert_eq!(parse_share_cookie(&h, 8).as_deref(), Some("def"));
@@ -354,7 +356,7 @@ mod tests {
#[test]
fn session_cookie_shape() {
let c = session_cookie("tok123", false);
assert!(c.starts_with("fbng_session=tok123;"));
assert!(c.starts_with("dovenest_session=tok123;"));
assert!(c.contains("Path=/"));
assert!(c.contains("HttpOnly"));
assert!(c.contains("SameSite=Lax"));
@@ -365,7 +367,7 @@ mod tests {
assert!(c.ends_with("; Secure"));
let c = clear_session_cookie(true);
assert!(c.starts_with("fbng_session=;"));
assert!(c.starts_with("dovenest_session=;"));
assert!(c.contains("Max-Age=0"));
assert!(c.contains("Secure"));
assert!(!clear_session_cookie(false).contains("Secure"));
@@ -376,7 +378,7 @@ mod tests {
let mut h = HeaderMap::new();
h.insert(
header::COOKIE,
"other=1; fbng_session=abc123; x=y".parse().unwrap(),
"other=1; dovenest_session=abc123; x=y".parse().unwrap(),
);
assert_eq!(parse_session_cookie(&h).as_deref(), Some("abc123"));
@@ -386,7 +388,7 @@ mod tests {
// Empty value → treated as absent.
let mut h = HeaderMap::new();
h.insert(header::COOKIE, "fbng_session=".parse().unwrap());
h.insert(header::COOKIE, "dovenest_session=".parse().unwrap());
assert_eq!(parse_session_cookie(&h), None);
assert_eq!(parse_session_cookie(&HeaderMap::new()), None);
@@ -395,7 +397,9 @@ mod tests {
let mut h = HeaderMap::new();
h.insert(
header::COOKIE,
"fbng_session=first; fbng_session=second".parse().unwrap(),
"dovenest_session=first; dovenest_session=second"
.parse()
.unwrap(),
);
assert_eq!(parse_session_cookie(&h).as_deref(), Some("first"));
@@ -403,7 +407,7 @@ mod tests {
let mut h = HeaderMap::new();
h.insert(
header::COOKIE,
"fbng_session2=x; Xfbng_session=y".parse().unwrap(),
"dovenest_session2=x; Xdovenest_session=y".parse().unwrap(),
);
assert_eq!(parse_session_cookie(&h), None);
}
▾Mserver/src/cli.rs
@@ -6,8 +6,8 @@ use webauthn_rs::prelude::Url;
#[derive(Parser, Debug)]
#[command(
name = "filebrowser-ng",
about = "A simple, noob-friendly web file browser",
name = "dovenest",
about = "A self-hosted file browser with WebDAV, CalDAV and CardDAV",
version
)]
pub struct Cli {
@@ -18,7 +18,7 @@ pub struct Cli {
/// Display name of the root folder in the UI. Defaults to the folder's
/// own name, which in a container is the mount point (e.g. "data").
#[arg(long, env = "FILEBROWSER_ROOT_NAME")]
#[arg(long, env = "DOVENEST_ROOT_NAME")]
pub root_name: Option<String>,
/// Path to the SQLite database file (required).
@@ -35,7 +35,7 @@ pub struct Cli {
/// Folder for the thumbnail cache. Without it thumbnails are off and
/// the grid shows icons only. Entries not used for a day are swept.
#[arg(long, env = "FILEBROWSER_CACHE")]
#[arg(long, env = "DOVENEST_CACHE")]
pub cache: Option<PathBuf>,
/// Public base URL of this server, e.g. `https://files.example.com`.
@@ -43,7 +43,7 @@ pub struct Cli {
/// scheme is what tells the server it sits behind a TLS-terminating
/// proxy. Without it links use whatever address the browser is connected
/// to, and the server assumes plain HTTP.
#[arg(long, env = "FILEBROWSER_PUBLIC_URL")]
#[arg(long, env = "DOVENEST_PUBLIC_URL")]
pub public_url: Option<Url>,
}
@@ -55,7 +55,7 @@ mod tests {
#[test]
fn defaults_applied() {
let c = Cli::try_parse_from(["filebrowser-ng", "--root", "/r", "--db", "/d"]).unwrap();
let c = Cli::try_parse_from(["dovenest", "--root", "/r", "--db", "/d"]).unwrap();
assert_eq!(c.root, PathBuf::from("/r"));
assert_eq!(c.db, PathBuf::from("/d"));
assert_eq!(c.port, 8080);
@@ -66,15 +66,7 @@ mod tests {
#[test]
fn flags_override_defaults() {
let c = Cli::try_parse_from([
"filebrowser-ng",
"--root",
"/r",
"--db",
"/d",
"--port",
"9000",
"--bind",
"0.0.0.0",
"dovenest", "--root", "/r", "--db", "/d", "--port", "9000", "--bind", "0.0.0.0",
])
.unwrap();
assert_eq!(c.port, 9000);
@@ -83,7 +75,7 @@ mod tests {
#[test]
fn required_args_enforced() {
assert!(Cli::try_parse_from(["filebrowser-ng", "--root", "/r"]).is_err());
assert!(Cli::try_parse_from(["filebrowser-ng"]).is_err());
assert!(Cli::try_parse_from(["dovenest", "--root", "/r"]).is_err());
assert!(Cli::try_parse_from(["dovenest"]).is_err());
}
}
▾Mserver/src/lib.rs
@@ -1,8 +1,8 @@
//! filebrowser-ng server.
//! dovenest server.
//!
//! Library target so the HTTP app can be exercised from integration tests
//! (via `tower::ServiceExt::oneshot`) without binding a real port. The
//! `filebrowser-ng` binary is a thin wrapper around [`run`].
//! `dovenest` binary is a thin wrapper around [`run`].
use std::net::SocketAddr;
use std::sync::Arc;
@@ -84,7 +84,7 @@ pub async fn run() -> anyhow::Result<()> {
.await
.with_context(|| format!("cannot bind to {addr}"))?;
tracing::info!(root = %cli.root.display(), "filebrowser-ng starting");
tracing::info!(root = %cli.root.display(), "dovenest starting");
tracing::info!(addr = %addr, "listening (pass --bind 0.0.0.0 to expose beyond localhost)");
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
▾Mserver/src/webauthn.rs
@@ -47,7 +47,7 @@ pub fn relying_party(
// one at all — the RP ID has to be a registrable domain.
let rp_id = origin.domain().ok_or_else(misconfigured)?;
WebauthnBuilder::new(rp_id, &origin)
.and_then(|b| b.rp_name("filebrowser-ng").build())
.and_then(|b| b.rp_name("dovenest").build())
.map_err(|e| {
tracing::error!(error = ?e, %origin, "cannot build the WebAuthn relying party");
misconfigured()
▾Mserver/tests/api_auth.rs
@@ -150,7 +150,7 @@ async fn logout_invalidates_session() {
assert!(
r.header("set-cookie")
.unwrap()
.starts_with("fbng_session=;")
.starts_with("dovenest_session=;")
);
// The old cookie no longer authenticates.
assert_eq!(
@@ -198,7 +198,7 @@ async fn disabled_user_loses_session_and_cannot_login() {
async fn root_name_is_configurable() {
let mut env = Env::new().await;
// Rebuild the router with a custom root name (what `--root-name` /
// `FILEBROWSER_ROOT_NAME` sets on the state).
// `DOVENEST_ROOT_NAME` sets on the state).
let state = Arc::new(server::error::AppState {
db: env.state.db.clone(),
root: env.state.root.clone(),
▾Mserver/tests/api_dav.rs
@@ -65,7 +65,7 @@ async fn unauthenticated_requests_get_a_basic_challenge() {
// Without the challenge a mount client never offers credentials.
assert_eq!(
r.header("www-authenticate").as_deref(),
Some("Basic realm=\"filebrowser-ng\", charset=\"UTF-8\"")
Some("Basic realm=\"dovenest\", charset=\"UTF-8\"")
);
}
▾Mserver/tests/api_embedded.rs
@@ -47,7 +47,7 @@ async fn embedded_frontend_is_complete() {
assert_eq!(r.header("cache-control").as_deref(), Some("no-cache"));
let html = r.text();
assert!(
html.contains("filebrowser-ng"),
html.contains("dovenest"),
"index.html does not look like the built frontend"
);
@@ -102,7 +102,7 @@ async fn embedded_frontend_is_complete() {
// Client routes fall back to index.html; unknown API paths stay 404.
let r = c.get("/some/client/route").await;
assert_eq!(r.status, StatusCode::OK);
assert!(r.text().contains("filebrowser-ng"));
assert!(r.text().contains("dovenest"));
let r = c.get("/api/unknown/endpoint").await;
assert_eq!(r.status, StatusCode::NOT_FOUND);
}
▾Mserver/tests/api_files.rs
@@ -904,7 +904,7 @@ async fn upload_in_two_halves(
.header("content-type", "multipart/form-data; boundary=B")
.header(
"cookie",
format!("fbng_session={}", admin.cookie.as_ref().unwrap()),
format!("dovenest_session={}", admin.cookie.as_ref().unwrap()),
)
.body(axum::body::Body::from_stream(stream))
.unwrap();
@@ -974,7 +974,7 @@ async fn upload_stopped_mid_part(env: &Env, admin: &Client) -> StatusCode {
.header("content-type", "multipart/form-data; boundary=B")
.header(
"cookie",
format!("fbng_session={}", admin.cookie.as_ref().unwrap()),
format!("dovenest_session={}", admin.cookie.as_ref().unwrap()),
)
.body(axum::body::Body::from_stream(stream))
.unwrap();
▾Mserver/tests/api_pim.rs
@@ -175,7 +175,7 @@ async fn discovery() {
);
// Only the mailto address, preferred, so Apple picks it as the identity.
let set = prop(&ms, p, CALDAV, "calendar-user-address-set").unwrap();
assert_eq!(hrefs_of(&set), ["mailto:alice@filebrowser.invalid"]);
assert_eq!(hrefs_of(&set), ["mailto:alice@dovenest.invalid"]);
let href = xml::child(&set, DAV, "href").unwrap();
assert_eq!(
href.attributes.get("preferred").map(String::as_str),
@@ -286,7 +286,7 @@ async fn other_users_are_off_limits() {
Some("admin")
);
let addresses = hrefs_of(&prop(&ms, p, CALDAV, "calendar-user-address-set").unwrap());
assert!(addresses.contains(&"mailto:admin@filebrowser.invalid".to_string()));
assert!(addresses.contains(&"mailto:admin@dovenest.invalid".to_string()));
assert!(prop(&ms, p, CARDDAV, "addressbook-home-set").is_none());
let r = req(&env, "PROPFIND", "/pim/principals/nobody/", &auth, &[], "").await;
@@ -902,7 +902,7 @@ async fn sync_collection() {
.await;
assert_eq!(statuses(&r).len(), 2);
for bad in ["urn:fbng:sync:999-1", "nonsense", &format!("{latest}0")] {
for bad in ["urn:dovenest:sync:999-1", "nonsense", &format!("{latest}0")] {
let r = req(&env, "REPORT", CAL, &auth, &[], &sync(bad, "")).await;
assert_eq!(
error_condition(&r),
@@ -1449,7 +1449,7 @@ async fn rooms_and_resources() {
Some("ROOM")
);
let addresses = hrefs_of(&prop(&ms, p, CALDAV, "calendar-user-address-set").unwrap());
assert!(addresses.contains(&"mailto:board@rooms.filebrowser.invalid".to_string()));
assert!(addresses.contains(&"mailto:board@rooms.dovenest.invalid".to_string()));
assert_eq!(
hrefs_of(&prop(&ms, p, CALDAV, "calendar-home-set").unwrap()),
["/pim/calendars/board/"]
▾Mserver/tests/api_pim_clients.rs
@@ -400,15 +400,15 @@ async fn addresses_and_logins_for_unusual_names() {
};
// One `@` and only characters valid in a local part.
let m = address(&env, marc.clone(), "marc@example.com").await;
assert_eq!(m, "mailto:marc%40example.com@filebrowser.invalid");
assert_eq!(m, "mailto:marc%40example.com@dovenest.invalid");
let dots = address(&env, basic("a..b", PW), "a..b").await;
assert_eq!(dots, "mailto:a%2E%2Eb@filebrowser.invalid");
assert_eq!(dots, "mailto:a%2E%2Eb@dovenest.invalid");
// An invitation to that address reaches the account.
let ics = format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//t//EN\r\nBEGIN:VEVENT\r\nUID:m1\r\nDTSTAMP:20260101T000000Z\r\n\
DTSTART:20261001T100000Z\r\nDTEND:20261001T110000Z\r\nSUMMARY:Meet\r\n\
ORGANIZER:mailto:alice@filebrowser.invalid\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@filebrowser.invalid\r\n\
ORGANIZER:mailto:alice@dovenest.invalid\r\nATTENDEE;PARTSTAT=ACCEPTED:mailto:alice@dovenest.invalid\r\n\
ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:{m}\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
);
let r = req(
▾Mserver/tests/api_pim_derived.rs
@@ -322,7 +322,7 @@ fn meeting(uid: &str, organizer: &str, attendees: &[&str], start: &str) -> Strin
}
fn addr(user: &str) -> String {
format!("mailto:{user}@filebrowser.invalid")
format!("mailto:{user}@dovenest.invalid")
}
#[tokio::test]
@@ -340,7 +340,7 @@ async fn deleted_principals_are_forgotten() {
.await;
assert_eq!(r.status, StatusCode::OK, "{}", r.text());
let room = r.json()["id"].as_i64().unwrap();
let atrium = "mailto:atrium@rooms.filebrowser.invalid";
let atrium = "mailto:atrium@rooms.dovenest.invalid";
let own = "/pim/calendars/alice/default/own.ics";
pim.put(
@@ -385,7 +385,7 @@ async fn deleted_principals_are_forgotten() {
assert!(!org.contains(atrium), "{org}");
let tombs: Vec<&str> = org
.lines()
.filter(|l| l.contains("@deleted.filebrowser.invalid"))
.filter(|l| l.contains("@deleted.dovenest.invalid"))
.collect();
assert_eq!(tombs.len(), 2, "{org}");
assert!(
▾Mserver/tests/api_pim_io.rs
@@ -279,7 +279,7 @@ async fn import_sends_nothing_and_keeps_uniqueness() {
let meeting = event(
"meet",
"Meeting",
"ORGANIZER:mailto:alice@filebrowser.invalid\r\nATTENDEE:mailto:bob@filebrowser.invalid\r\n",
"ORGANIZER:mailto:alice@dovenest.invalid\r\nATTENDEE:mailto:bob@dovenest.invalid\r\n",
);
let r = io.import(cal, &meeting).await;
assert_eq!(r["created"], 1, "{r}");
▾Mserver/tests/api_pim_schedule.rs
@@ -88,7 +88,7 @@ fn cal(user: &str) -> String {
}
fn addr(user: &str) -> String {
format!("mailto:{user}@filebrowser.invalid")
format!("mailto:{user}@dovenest.invalid")
}
fn unfold(s: &str) -> String {
@@ -371,9 +371,9 @@ async fn rooms_receive_bookings() {
)
.await;
assert_eq!(r.status, StatusCode::OK, "{}", r.text());
invite(&pim, &["mailto:board@rooms.filebrowser.invalid"]).await;
invite(&pim, &["mailto:board@rooms.dovenest.invalid"]).await;
let org = pim.req("alice", "GET", ALICE_EVENT, &[], "").await.text();
let room = "mailto:board@rooms.filebrowser.invalid";
let room = "mailto:board@rooms.dovenest.invalid";
assert_eq!(
attendee_param(&org, room, "SCHEDULE-STATUS").as_deref(),
Some("1.2"),
@@ -384,7 +384,7 @@ async fn rooms_receive_bookings() {
assert_eq!(members.len(), 1, "{members:?}");
}
const ROOM: &str = "mailto:board@rooms.filebrowser.invalid";
const ROOM: &str = "mailto:board@rooms.dovenest.invalid";
/// `days` from now at `hour` UTC, as an iCalendar date-time. Rooms only
/// check instances from now on.
▾Mserver/tests/api_pim_ui.rs
@@ -73,8 +73,8 @@ fn event(uid: &str, props: &str) -> String {
))
}
const ALICE: &str = "mailto:alice@filebrowser.invalid";
const BOB: &str = "mailto:bob@filebrowser.invalid";
const ALICE: &str = "mailto:alice@dovenest.invalid";
const BOB: &str = "mailto:bob@dovenest.invalid";
#[tokio::test]
async fn collections_can_be_created_changed_and_deleted() {
▾Mserver/tests/api_shares.rs
@@ -648,7 +648,7 @@ async fn a_password_locks_the_share_and_its_files() {
assert_eq!(r.status, StatusCode::OK, "unlock: {}", r.text());
let cookie = r.header("set-cookie").expect("unlock sets a cookie");
let share_id = s["id"].as_i64().unwrap();
assert!(cookie.starts_with(&format!("fbng_share_{share_id}=")));
assert!(cookie.starts_with(&format!("dovenest_share_{share_id}=")));
assert!(cookie.contains("HttpOnly"));
let value = cookie
.split(';')
@@ -660,7 +660,7 @@ async fn a_password_locks_the_share_and_its_files() {
.to_string();
// With the cookie, both the page and the files open.
let hdr: &[(&str, &str)] = &[("cookie", &format!("fbng_share_{share_id}={value}"))];
let hdr: &[(&str, &str)] = &[("cookie", &format!("dovenest_share_{share_id}={value}"))];
let r = anon
.raw(
axum::http::Method::GET,
▾Mserver/tests/api_spa.rs
@@ -1,9 +1,9 @@
//! SPA serving (dev mode): static assets, client-route fallback, API 404s,
//! method checks. Uses $FBNG_DIST (the dev-mode asset directory) via a
//! method checks. Uses $DOVENEST_DIST (the dev-mode asset directory) via a
//! tempdir so the test is independent of any built frontend.
//!
//! Disabled under `--features embedded` (assets come from rust-embed, not
//! $FBNG_DIST) — see `api_embedded.rs` for the production variant.
//! $DOVENEST_DIST) — see `api_embedded.rs` for the production variant.
#![cfg(not(feature = "embedded"))]
mod common;
@@ -29,12 +29,12 @@ async fn spa_fallback_and_api_guards() {
// Point the dev asset dir at a controlled tempdir.
//
// `FBNG_DIST` is process-global; only this test (the sole test in this
// `DOVENEST_DIST` is process-global; only this test (the sole test in this
// binary) touches it, and other test binaries are separate processes.
let dist = tempfile::tempdir().unwrap();
std::fs::write(dist.path().join("index.html"), "DIST-INDEX").unwrap();
std::fs::write(dist.path().join("app.css"), "body{}").unwrap();
unsafe { std::env::set_var("FBNG_DIST", dist.path()) };
unsafe { std::env::set_var("DOVENEST_DIST", dist.path()) };
// Root serves index.html.
let r = c.get("/").await;
@@ -88,10 +88,10 @@ async fn spa_fallback_and_api_guards() {
// Now with an *empty* dist dir → the friendly "build the frontend" hint.
let empty = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("FBNG_DIST", empty.path()) };
unsafe { std::env::set_var("DOVENEST_DIST", empty.path()) };
let r = c.get("/").await;
assert_eq!(r.status, StatusCode::OK);
assert!(r.text().contains("frontend has not been built"));
unsafe { std::env::remove_var("FBNG_DIST") };
unsafe { std::env::remove_var("DOVENEST_DIST") };
}
▾Mserver/tests/common/mod.rs
@@ -198,7 +198,7 @@ impl Client {
b = b.header(*k, *v);
}
if let Some(c) = &self.cookie {
b = b.header(header::COOKIE, format!("fbng_session={c}"));
b = b.header(header::COOKIE, format!("dovenest_session={c}"));
}
let req = b
.body(Body::from(body))
@@ -276,12 +276,12 @@ impl Client {
}
}
/// Extract the `fbng_session` cookie value from `Set-Cookie` headers.
/// Extract the `dovenest_session` cookie value from `Set-Cookie` headers.
pub fn session_cookie(r: &Resp) -> Option<String> {
for v in r.headers.get_all(header::SET_COOKIE) {
let s = v.to_str().ok()?;
let first = s.split(';').next().unwrap_or("");
if let Some(tok) = first.strip_prefix("fbng_session=")
if let Some(tok) = first.strip_prefix("dovenest_session=")
&& !tok.is_empty()
{
return Some(tok.to_string());
▾Mweb/bun.lock
@@ -3,7 +3,7 @@
"configVersion": 1,
"workspaces": {
"": {
"name": "filebrowser-ng-codemirror",
"name": "dovenest-codemirror",
"dependencies": {
"@codemirror/lang-cpp": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
▾Mweb/cm/wrapper.js
@@ -1,7 +1,7 @@
// CodeMirror 6 wrapper for filebrowser-ng.
// CodeMirror 6 wrapper for dovenest.
//
// Bundled to web/cm6.js (an IIFE) by `bun run build` and exposed on
// `window.__fbng_cm`. The Rust side drives it through this tiny surface, so
// `window.__dovenest_cm`. The Rust side drives it through this tiny surface, so
// swapping the editor later only requires re-implementing these functions.
import { EditorView, basicSetup } from "codemirror";
@@ -71,9 +71,9 @@ const appTheme = EditorView.theme({
});
// Positional arguments throughout, so the Rust side can bind these directly
// with #[wasm_bindgen(js_namespace = __fbng_cm)] instead of building an
// with #[wasm_bindgen(js_namespace = __dovenest_cm)] instead of building an
// options object.
window.__fbng_cm = {
window.__dovenest_cm = {
// Create an editor in `container`. Returns an opaque view handle.
create(container, value, filename, editable, onUpdate) {
const extensions = [appTheme, basicSetup];
▾Mweb/favicon.svg
@@ -1,7 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- filebrowser-ng mark: a folder with files dropping in. Keep in sync
with the Logo component in web/src/components/logo.rs. -->
<!-- dovenest mark: a dove in a woven nest. Keep in sync with the Logo
component in web/src/components/logo.rs. -->
<rect width="32" height="32" fill="#2563eb"/>
<path d="M6 8h7l3 4h10v14H6z" fill="#fff"/>
<path d="M11.5 17.5 16 22l4.5-4.5" fill="none" stroke="#2563eb" stroke-width="3"/>
<path d="M4 12.5l6 3.2c1.2-2.6 3.6-4 6.5-4 1.2 0 2.2.2 3 .6.4-2.2 2.1-3.6 4-3.6 1.6 0 2.8.9 3.3 2.2l2.2.9-2.1.7c-.2 1.2-.8 2.3-1.8 3.1 0 3.6-3.2 6.4-8 6.4H11c-3 0-5.5-2.9-7-9.5z" fill="#fff"/>
<path d="M11.5 16.5c3 2.2 7 2.3 10 .2" fill="none" stroke="#2563eb" stroke-width="1.4"/>
<circle cx="24" cy="11.2" r=".9" fill="#2563eb"/>
<path d="M3 20h26c-1 4.8-6.2 7.6-13 7.6S4 24.8 3 20z" fill="#fff"/>
<path d="M6 22.9l3.4 2.3 3.4-2.3 3.2 2.3 3.2-2.3 3.4 2.3 3.4-2.3" fill="none" stroke="#2563eb" stroke-width="1.5"/>
</svg>
▾Mweb/index.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>filebrowser-ng</title>
<title>dovenest</title>
<link
data-trunk
rel="icon"
@@ -18,7 +18,7 @@
// Apply a forced theme before first paint so light/dark users don't
// flash the opposite palette.
try {
var t = localStorage.getItem("fb-theme");
var t = localStorage.getItem("dovenest.theme");
if (t === "light" || t === "dark") document.documentElement.classList.add(t);
} catch (e) {}
</script>
@@ -28,6 +28,6 @@
data-wasm-opt="z"
data-wasm-opt-params="--enable-bulk-memory --enable-sign-ext --enable-nontrapping-float-to-int"
/>
<noscript>filebrowser-ng requires JavaScript.</noscript>
<noscript>dovenest requires JavaScript.</noscript>
</body>
</html>
▾Mweb/package.json
@@ -1,5 +1,5 @@
{
"name": "filebrowser-ng-codemirror",
"name": "dovenest-codemirror",
"private": true,
"type": "module",
"description": "Builds the vendored CodeMirror 6 bundle (web/cm6.js, a gitignored build artifact) used by the text preview/editor. Rebuild with: just build-cm (runs automatically with just build / dev-web / e2e).",
▾Mweb/src/api.rs
@@ -1,4 +1,4 @@
//! Typed HTTP client for the filebrowser-ng API.
//! Typed HTTP client for the dovenest API.
//!
//! Endpoint paths, query params and wire types all come from the shared
//! `api_types` crate (the same one the server's route table and handlers
@@ -628,7 +628,7 @@ pub fn trigger_download(url: &str, filename: &str) {
///
/// Returns the body and its `Content-Type` header value.
fn multipart_blob(parts: &[(String, web_sys::File)]) -> Result<(web_sys::Blob, String), ApiError> {
let boundary = format!("----fbng{}", random_boundary_suffix());
let boundary = format!("----dovenest{}", random_boundary_suffix());
let segments = js_sys::Array::new();
for (name, file) in parts {
let basename = escape_cd(name.rsplit('/').next().unwrap_or(name));
▾Mweb/src/cm.rs
@@ -1,7 +1,7 @@
//! Thin, swappable bindings to the vendored CodeMirror 6 bundle.
//!
//! The bundle (`web/cm6.js`, built from `web/cm/wrapper.js`) installs a
//! `window.__fbng_cm` global. This module is the *only* place that knows how
//! `window.__dovenest_cm` global. This module is the *only* place that knows how
//! to talk to it, so swapping the editor later means re-implementing just
//! these functions.
//!
@@ -14,7 +14,7 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = __fbng_cm, js_name = create, catch)]
#[wasm_bindgen(js_namespace = __dovenest_cm, js_name = create, catch)]
fn js_create(
container: &web_sys::Element,
value: &str,
@@ -23,17 +23,17 @@ extern "C" {
on_update: Option<&js_sys::Function>,
) -> Result<JsValue, JsValue>;
#[wasm_bindgen(js_namespace = __fbng_cm, catch)]
#[wasm_bindgen(js_namespace = __dovenest_cm, catch)]
pub fn destroy(view: &JsValue) -> Result<(), JsValue>;
/// Replace the whole document (used when (re)loading a file).
#[wasm_bindgen(js_namespace = __fbng_cm, js_name = setValue, catch)]
#[wasm_bindgen(js_namespace = __dovenest_cm, js_name = setValue, catch)]
pub fn set_value(view: &JsValue, text: &str) -> Result<(), JsValue>;
#[wasm_bindgen(js_namespace = __fbng_cm, js_name = getValue, catch)]
#[wasm_bindgen(js_namespace = __dovenest_cm, js_name = getValue, catch)]
fn js_get_value(view: &JsValue) -> Result<JsValue, JsValue>;
#[wasm_bindgen(js_namespace = __fbng_cm, catch)]
#[wasm_bindgen(js_namespace = __dovenest_cm, catch)]
pub fn focus(view: &JsValue) -> Result<(), JsValue>;
}
▾Mweb/src/components/logo.rs
@@ -1,4 +1,5 @@
//! The brand mark. Same artwork as `web/favicon.svg` — keep the two in sync.
//! The dovenest mark, a dove in a woven nest. Same artwork as
//! `web/favicon.svg`; keep the two in sync.
use leptos::prelude::*;
@@ -12,12 +13,18 @@ pub fn Logo(#[prop(optional)] class: Option<String>) -> impl IntoView {
view! {
<svg class=cls viewBox="0 0 32 32" aria-hidden="true">
<rect width="32" height="32" fill="#2563eb"/>
<path d="M6 8h7l3 4h10v14H6z" fill="#fff"/>
<path
d="M11.5 17.5 16 22l4.5-4.5"
d="M4 12.5l6 3.2c1.2-2.6 3.6-4 6.5-4 1.2 0 2.2.2 3 .6.4-2.2 2.1-3.6 4-3.6 1.6 0 2.8.9 3.3 2.2l2.2.9-2.1.7c-.2 1.2-.8 2.3-1.8 3.1 0 3.6-3.2 6.4-8 6.4H11c-3 0-5.5-2.9-7-9.5z"
fill="#fff"
/>
<path d="M11.5 16.5c3 2.2 7 2.3 10 .2" fill="none" stroke="#2563eb" stroke-width="1.4"/>
<circle cx="24" cy="11.2" r=".9" fill="#2563eb"/>
<path d="M3 20h26c-1 4.8-6.2 7.6-13 7.6S4 24.8 3 20z" fill="#fff"/>
<path
d="M6 22.9l3.4 2.3 3.4-2.3 3.2 2.3 3.2-2.3 3.4 2.3 3.4-2.3"
fill="none"
stroke="#2563eb"
stroke-width="3"
stroke-width="1.5"
/>
</svg>
}
▾Mweb/src/router.rs
@@ -387,9 +387,9 @@ fn sync() {
/// History state of an entry that [`set_open`] pushed. Closing that file
/// goes Back instead of adding an entry.
const OPEN_MARK: &str = "fbng-open";
const OPEN_MARK: &str = "dovenest-open";
/// History state of the entry a file was opened from.
const ORIGIN_MARK: &str = "fbng-origin";
const ORIGIN_MARK: &str = "dovenest-origin";
fn history() -> Option<web_sys::History> {
web_sys::window()?.history().ok()
▾Mweb/src/theme.rs
@@ -7,7 +7,7 @@
use crate::icons::IconName;
const STORAGE_KEY: &str = "fb-theme";
const STORAGE_KEY: &str = "dovenest.theme";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Theme {
▾Mweb/src/util.rs
@@ -230,7 +230,7 @@ pub enum ViewMode {
}
impl ViewMode {
const KEY: &'static str = "fbng.view";
const KEY: &'static str = "dovenest.view";
pub fn load() -> Self {
match storage_get(Self::KEY).as_deref() {
@@ -270,7 +270,7 @@ impl Default for SortSpec {
}
impl SortSpec {
const KEY: &'static str = "fbng.sort";
const KEY: &'static str = "dovenest.sort";
pub fn load() -> Self {
let raw = storage_get(Self::KEY).unwrap_or_default();
@@ -340,7 +340,7 @@ pub fn page_footer(
/// Page sizes the folder listing offers.
pub const PAGE_SIZES: [usize; 4] = [50, 100, 200, 500];
const PAGE_SIZE_KEY: &str = "fbng.page_size";
const PAGE_SIZE_KEY: &str = "dovenest.page_size";
pub fn load_page_size() -> usize {
storage_get(PAGE_SIZE_KEY)
▾Mweb/src/views/admin.rs
@@ -27,7 +27,7 @@ enum SettingsTab {
}
impl SettingsTab {
const KEY: &'static str = "fbng.settings-tab";
const KEY: &'static str = "dovenest.settings-tab";
fn parse(s: &str, is_admin: bool) -> Option<Self> {
match s {
▾Mweb/src/views/dialogs.rs
@@ -282,7 +282,7 @@ fn PromptOpDialog(
on_ok: Callback<()>,
on_close: Callback<()>,
) -> impl IntoView {
let input_id = "fbng-prompt-op-input";
let input_id = "dovenest-prompt-op-input";
let (error, set_error) = signal(Option::<String>::None);
let (busy, set_busy) = signal(false);
// The closure captures only Copy values, so the Enter-key and button
▾Mweb/src/views/login.rs
@@ -140,7 +140,7 @@ pub fn LoginView(
<div class="center-screen">
<div class="card auth-card">
<Logo class="auth-logo".to_string()/>
<h1>"filebrowser-ng"</h1>
<h1>"dovenest"</h1>
<p class="muted">
{move || match step.get() {
Step::NeedPasskey => i18n::t(i18n::k::SECOND_FACTOR_PASSKEY).to_string(),
▾Mweb/src/views/pim.rs
@@ -30,7 +30,7 @@ const PALETTE: [&str; 8] = [
];
/// The collections the lists do not show, by id.
const HIDDEN_KEY: &str = "fbng.pim-hidden";
const HIDDEN_KEY: &str = "dovenest.pim-hidden";
/// Collection ids hidden in the calendar and contact views.
pub fn load_hidden() -> HashSet<i64> {
▾Mweb/src/views/setup.rs
@@ -56,7 +56,7 @@ pub fn SetupView(
<div class="center-screen">
<div class="card auth-card">
<Logo class="auth-logo".to_string()/>
<h1>"filebrowser-ng"</h1>
<h1>"dovenest"</h1>
<p class="muted">{i18n::tr(i18n::k::SETUP_INTRO)}</p>
<form on:submit=on_submit>
<label class="field">
▾Mweb/src/views/shell.rs
@@ -314,7 +314,7 @@ pub fn ShellView(
<span class="nav-tab-name">{i18n::tr(i18n::k::LOG_OUT)}</span>
</button>
<div class="sidebar-version">
{format!("filebrowser-ng v{}", env!("CARGO_PKG_VERSION"))}
{format!("dovenest v{}", env!("CARGO_PKG_VERSION"))}
</div>
</div>
</aside>
▾Mweb/src/views/workspace.rs
@@ -300,7 +300,7 @@ impl FileWorkspace {
None => view! {
<a class="brand" href=home>
<Logo />
<span class="brand-name">"filebrowser-ng"</span>
<span class="brand-name">"dovenest"</span>
</a>
}
.into_any(),