diff --git a/.env.template b/.env.template index 03990820..7468e158 100644 --- a/.env.template +++ b/.env.template @@ -50,10 +50,11 @@ ######################### ## Database URL -## When using SQLite, this is the path to the DB file, and it defaults to -## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this -## must be set to a local sqlite3 file path. -# DATABASE_URL=data/db.sqlite3 +## When using SQLite, this should use the sqlite:// prefix followed by the path +## to the DB file. It defaults to sqlite://%DATA_FOLDER%/db.sqlite3. +## Bare paths without the sqlite:// prefix are supported for backwards compatibility, +## but only if the database file already exists. +# DATABASE_URL=sqlite://data/db.sqlite3 ## When using MySQL, specify an appropriate connection URI. ## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html # DATABASE_URL=mysql://user:password@host[:port]/database_name diff --git a/src/config.rs b/src/config.rs index 6ff09467..d2f897a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -507,7 +507,7 @@ make_config! { /// Data folder |> Main data folder data_folder: String, false, def, "data".to_string(); /// Database URL - database_url: String, false, auto, |c| format!("{}/db.sqlite3", c.data_folder); + database_url: String, false, auto, |c| format!("sqlite://{}/db.sqlite3", c.data_folder); /// Icon cache folder icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder); /// Attachments folder @@ -929,14 +929,17 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { { use crate::db::DbConnType; let url = &cfg.database_url; - if DbConnType::from_url(url)? == DbConnType::Sqlite && url.contains('/') { - let path = std::path::Path::new(&url); - if let Some(parent) = path.parent() { - if !parent.is_dir() { - err!(format!( - "SQLite database directory `{}` does not exist or is not a directory", - parent.display() - )); + if DbConnType::from_url(url)? == DbConnType::Sqlite { + let file_path = url.strip_prefix("sqlite://").unwrap_or(url); + if file_path.contains('/') { + let path = std::path::Path::new(file_path); + if let Some(parent) = path.parent() { + if !parent.is_dir() { + err!(format!( + "SQLite database directory `{}` does not exist or is not a directory", + parent.display() + )); + } } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index d2ed9479..7b989e44 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -272,13 +272,32 @@ impl DbConnType { #[cfg(not(postgresql))] err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled") - //Sqlite - } else { + // Sqlite (explicit) + } else if url.len() > 7 && &url[..7] == "sqlite:" { #[cfg(sqlite)] return Ok(DbConnType::Sqlite); #[cfg(not(sqlite))] - err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled") + err!("`DATABASE_URL` is a SQLite URL, but the 'sqlite' feature is not enabled") + + // No recognized scheme — assume legacy bare-path SQLite, but the database file must already exist. + // This prevents misconfigured URLs (typos, quoted strings) from silently creating a new empty SQLite database. + } else { + #[cfg(sqlite)] + { + if std::path::Path::new(url).exists() { + return Ok(DbConnType::Sqlite); + } + err!(format!( + "`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://) \ + and no existing SQLite database was found at '{url}'. \ + If you intend to use SQLite, use an explicit `sqlite://` prefix in your `DATABASE_URL`. \ + Otherwise, check your DATABASE_URL for typos or quoting issues." + )) + } + + #[cfg(not(sqlite))] + err!("`DATABASE_URL` does not match any known database scheme (mysql://, postgresql://, sqlite://)") } } @@ -390,11 +409,12 @@ pub fn backup_sqlite() -> Result { let db_url = CONFIG.database_url(); if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::Sqlite).unwrap_or(false) { - // Since we do not allow any schema for sqlite database_url's like `file:` or `sqlite:` to be set, we can assume here it isn't - // This way we can set a readonly flag on the opening mode without issues. - let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?; + // Strip the sqlite:// prefix if present to get the raw file path + let file_path = db_url.strip_prefix("sqlite://").unwrap_or(&db_url); + // Open a read-only connection for the backup + let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{file_path}?mode=ro"))?; - let db_path = std::path::Path::new(&db_url).parent().unwrap(); + let db_path = std::path::Path::new(file_path).parent().unwrap(); let backup_file = db_path .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) .to_string_lossy()