Skip to content

Commit e3fb2f8

Browse files
authored
Introduce SQLPAGE_CONFIGURATION_DIRECTORY (#243)
* Introduce SQLPAGE_CONFIGURATION_DIRECTORY Makes the configuration directory configurable instead of it being hardcoded to "./sqlpage/" * logging
1 parent c8dc7bc commit e3fb2f8

File tree

11 files changed

+91
-38
lines changed

11 files changed

+91
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
- ![screenshot](https://github.com/lovasoa/SQLpage/assets/552629/df085592-8359-4fed-9aeb-27a2416ab6b8)
2020
- **Multiple page layouts** : The page layout is now configurable from the [shell component](https://sql.ophir.dev/documentation.sql?component=shell#component). 3 layouts are available: `boxed` (the default), `fluid` (full width), and `horizontal` (with boxed contents but a full-width header).
2121
- ![horizontal layout screenshot](https://github.com/lovasoa/SQLpage/assets/552629/3c0fde36-7bf6-414e-b96f-c8880a2fc786)
22+
- New `SQLPAGE_CONFIGURATION_DIRECTORY` environment variable to set the configuration directory from the environment.
23+
The configuration directory is where SQLPage looks for the `sqlpage.json` configuration file, for the `migrations` and `templates` directories, and the `on_connect.sql` file. It used to be hardcoded to `./sqlpage/`, which made each SQLPage invokation dependent on the [current working directory](https://en.wikipedia.org/wiki/Working_directory).
24+
Now you can, for instance, set `SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage/` in your environment, and SQLPage will look for its configuration files in `/etc/sqlpage`, which is a more standard location for configuration files in a Unix environment.
25+
- The official docker image now sets `SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage/` by default, and changes the working directory to `/var/www/` by default.
2226

2327
## 0.18.3 (2024-02-03)
2428

Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ RUN touch src/main.rs && \
3131

3232
FROM busybox:glibc
3333
RUN addgroup --gid 1000 --system sqlpage && \
34-
adduser --uid 1000 --system --no-create-home --ingroup sqlpage sqlpage
34+
adduser --uid 1000 --system --no-create-home --ingroup sqlpage sqlpage && \
35+
mkdir -p /etc/sqlpage && \
36+
touch /etc/sqlpage/sqlpage.db && \
37+
chown -R sqlpage:sqlpage /etc/sqlpage/sqlpage.db
3538
ENV SQLPAGE_WEB_ROOT=/var/www
36-
WORKDIR /etc
39+
ENV SQLPAGE_CONFIGURATION_DIRECTORY=/etc/sqlpage
40+
WORKDIR /var/www
3741
COPY --from=builder /usr/src/sqlpage/sqlpage.bin /usr/local/bin/sqlpage
3842
COPY --from=builder /usr/src/sqlpage/libgcc_s.so.1 /lib/libgcc_s.so.1
3943
USER sqlpage

configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Here are the available configuration options and their default values:
1818
| `database_connection_acquire_timeout_seconds` | 10 | How long to wait when acquiring a database connection from the pool before giving up and returning an error. |
1919
| `sqlite_extensions` | | An array of SQLite extensions to load, such as `mod_spatialite` |
2020
| `web_root` | `.` | The root directory of the web server, where the `index.sql` file is located. |
21+
| `configuration_directory` | `./sqlpage/` | The directory where the `sqlpage.json` file is located. This is used to find the path to `templates/`, `migrations/`, and `on_connect.sql`. Obviously, this configuration parameter can be set only through environment variables, not through the `sqlpage.json` file itself in order to find the `sqlpage.json` file. |
2122
| `allow_exec` | false | Allow usage of the `sqlpage.exec` function. Do this only if all users with write access to sqlpage query files and to the optional `sqlpage_files` table on the database are trusted. |
2223
| `max_uploaded_file_size` | 5242880 | Maximum size of uploaded files in bytes. Defaults to 5 MiB. |
2324
| `https_domain` | | Domain name to request a certificate for. Setting this parameter will automatically make SQLPage listen on port 443 and request an SSL certificate. The server will take a little bit longer to start the first time it has to request a certificate. |

src/app_config.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ use serde::{Deserialize, Deserializer};
55
use std::net::{SocketAddr, ToSocketAddrs};
66
use std::path::PathBuf;
77

8-
#[cfg(not(feature = "lambda-web"))]
9-
const DEFAULT_DATABASE_DIR: &str = "sqlpage";
108
#[cfg(not(feature = "lambda-web"))]
119
const DEFAULT_DATABASE_FILE: &str = "sqlpage.db";
1210

@@ -36,9 +34,14 @@ pub struct AppConfig {
3634
#[serde(default = "default_database_connection_acquire_timeout_seconds")]
3735
pub database_connection_acquire_timeout_seconds: f64,
3836

37+
/// The directory where the .sql files are located. Defaults to the current directory.
3938
#[serde(default = "default_web_root")]
4039
pub web_root: PathBuf,
4140

41+
/// The directory where the sqlpage configuration file is located. Defaults to `./sqlpage`.
42+
#[serde(default = "configuration_directory")]
43+
pub configuration_directory: PathBuf,
44+
4245
/// Set to true to allow the `sqlpage.exec` function to be used in SQL queries.
4346
/// This should be enabled only if you trust the users writing SQL queries, since it gives
4447
/// them the ability to execute arbitrary shell commands on the server.
@@ -93,9 +96,27 @@ impl AppConfig {
9396
}
9497
}
9598

99+
/// The directory where the `sqlpage.json` file is located.
100+
/// Determined by the `SQLPAGE_CONFIGURATION_DIRECTORY` environment variable
101+
fn configuration_directory() -> PathBuf {
102+
std::env::var("SQLPAGE_CONFIGURATION_DIRECTORY")
103+
.or_else(|_| std::env::var("CONFIGURATION_DIRECTORY"))
104+
.map_or_else(|_| PathBuf::from("sqlpage"), PathBuf::from)
105+
}
106+
107+
fn cannonicalize_if_possible(path: &std::path::Path) -> PathBuf {
108+
path.canonicalize().unwrap_or_else(|_| path.to_owned())
109+
}
110+
96111
pub fn load() -> anyhow::Result<AppConfig> {
112+
let configuration_directory = &configuration_directory();
113+
log::debug!(
114+
"Loading configuration from {:?}",
115+
cannonicalize_if_possible(configuration_directory)
116+
);
117+
let config_file = configuration_directory.join("sqlpage");
97118
Config::builder()
98-
.add_source(config::File::with_name("sqlpage/sqlpage").required(false))
119+
.add_source(config::File::from(config_file).required(false))
99120
.add_source(env_config())
100121
.add_source(env_config().prefix("SQLPAGE"))
101122
.build()?
@@ -135,14 +156,14 @@ fn default_database_url() -> String {
135156

136157
#[cfg(not(feature = "lambda-web"))]
137158
{
138-
let cwd = std::env::current_dir().unwrap_or_default();
139-
let old_default_db_path = cwd.join(DEFAULT_DATABASE_FILE);
140-
let default_db_path = cwd.join(DEFAULT_DATABASE_DIR).join(DEFAULT_DATABASE_FILE);
159+
let config_dir = cannonicalize_if_possible(&configuration_directory());
160+
let old_default_db_path = PathBuf::from(DEFAULT_DATABASE_FILE);
161+
let default_db_path = config_dir.join(DEFAULT_DATABASE_FILE);
141162
if let Ok(true) = old_default_db_path.try_exists() {
142163
log::warn!("Your sqlite database in {old_default_db_path:?} is publicly accessible through your web server. Please move it to {default_db_path:?}.");
143164
return prefix + old_default_db_path.to_str().unwrap();
144165
} else if let Ok(true) = default_db_path.try_exists() {
145-
log::debug!("Using the default datbase file in {default_db_path:?}.");
166+
log::debug!("Using the default database file in {default_db_path:?}.");
146167
return prefix + default_db_path.to_str().unwrap();
147168
}
148169
// Create the default database file if we can

src/filesystem.rs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::webserver::ErrorWithStatus;
22
use crate::webserver::{make_placeholder, Database};
3-
use crate::AppState;
3+
use crate::{AppState, TEMPLATES_DIR};
44
use anyhow::Context;
55
use chrono::{DateTime, Utc};
66
use sqlx::any::{AnyKind, AnyStatement, AnyTypeInfo};
@@ -39,7 +39,7 @@ impl FileSystem {
3939
since: DateTime<Utc>,
4040
priviledged: bool,
4141
) -> anyhow::Result<bool> {
42-
let local_path = self.safe_local_path(path, priviledged)?;
42+
let local_path = self.safe_local_path(app_state, path, priviledged)?;
4343
let local_result = file_modified_since_local(&local_path, since).await;
4444
match (local_result, &self.db_fs_queries) {
4545
(Ok(modified), _) => Ok(modified),
@@ -75,7 +75,7 @@ impl FileSystem {
7575
path: &Path,
7676
priviledged: bool,
7777
) -> anyhow::Result<Vec<u8>> {
78-
let local_path = self.safe_local_path(path, priviledged)?;
78+
let local_path = self.safe_local_path(app_state, path, priviledged)?;
7979
let local_result = tokio::fs::read(&local_path).await;
8080
match (local_result, &self.db_fs_queries) {
8181
(Ok(f), _) => Ok(f),
@@ -91,18 +91,39 @@ impl FileSystem {
9191
}
9292
}
9393

94-
fn safe_local_path(&self, path: &Path, priviledged: bool) -> anyhow::Result<PathBuf> {
95-
for (i, component) in path.components().enumerate() {
96-
if let Component::Normal(c) = component {
97-
if !priviledged && i == 0 && c.eq_ignore_ascii_case("sqlpage") {
98-
anyhow::bail!(ErrorWithStatus {
99-
status: actix_web::http::StatusCode::FORBIDDEN,
100-
});
101-
}
102-
} else {
103-
anyhow::bail!(
94+
fn safe_local_path(
95+
&self,
96+
app_state: &AppState,
97+
path: &Path,
98+
priviledged: bool,
99+
) -> anyhow::Result<PathBuf> {
100+
if priviledged {
101+
// Templates requests are always made to the static TEMPLATES_DIR, because this is where they are stored in the database
102+
// but when serving them from the filesystem, we need to serve them from the `SQLPAGE_CONFIGURATION_DIRECTORY/templates` directory
103+
if let Ok(template_path) = path.strip_prefix(TEMPLATES_DIR) {
104+
let normalized = [
105+
&app_state.config.configuration_directory,
106+
Path::new("templates"),
107+
template_path,
108+
]
109+
.iter()
110+
.collect();
111+
log::trace!("Normalizing template path {path:?} to {normalized:?}");
112+
return Ok(normalized);
113+
}
114+
} else {
115+
for (i, component) in path.components().enumerate() {
116+
if let Component::Normal(c) = component {
117+
if i == 0 && c.eq_ignore_ascii_case("sqlpage") {
118+
anyhow::bail!(ErrorWithStatus {
119+
status: actix_web::http::StatusCode::FORBIDDEN,
120+
});
121+
}
122+
} else {
123+
anyhow::bail!(
104124
"Unsupported path: {path:?}. Path component '{component:?}' is not allowed."
105125
);
126+
}
106127
}
107128
}
108129
Ok(self.local_root.join(path))

src/lib.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ use std::path::PathBuf;
1919
use templates::AllTemplates;
2020
use webserver::Database;
2121

22-
pub const TEMPLATES_DIR: &str = "sqlpage/templates";
23-
pub const MIGRATIONS_DIR: &str = "sqlpage/migrations";
24-
pub const ON_CONNECT_FILE: &str = "sqlpage/on_connect.sql";
22+
/// `TEMPLATES_DIR` is the directory where .handlebars files are stored
23+
/// When a template is requested, it is looked up in `sqlpage/templates/component_name.handlebars` in the database,
24+
/// or in `$SQLPAGE_CONFIGURATION_DIRECTORY/templates/component_name.handlebars` in the filesystem.
25+
pub const TEMPLATES_DIR: &str = "sqlpage/templates/";
26+
pub const MIGRATIONS_DIR: &str = "migrations";
27+
pub const ON_CONNECT_FILE: &str = "on_connect.sql";
2528

2629
pub struct AppState {
2730
pub db: Database,

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async fn start() -> anyhow::Result<()> {
1616
let app_config = app_config::load()?;
1717
log::debug!("Starting with the following configuration: {app_config:#?}");
1818
let state = AppState::init(&app_config).await?;
19-
webserver::database::migrations::apply(&state.db).await?;
19+
webserver::database::migrations::apply(&app_config, &state.db).await?;
2020
log::debug!("Starting server...");
2121
let (r, _) = tokio::join!(
2222
webserver::http::run_server(&app_config, state),

src/templates.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ impl AllTemplates {
341341
) -> anyhow::Result<Arc<SplitTemplate>> {
342342
use anyhow::Context;
343343
let mut path: PathBuf =
344-
PathBuf::with_capacity(TEMPLATES_DIR.len() + name.len() + ".handlebars".len() + 2);
344+
PathBuf::with_capacity(TEMPLATES_DIR.len() + 1 + name.len() + ".handlebars".len());
345345
path.push(TEMPLATES_DIR);
346346
path.push(name);
347347
path.set_extension("handlebars");

src/webserver/database/connect.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,16 @@ impl Database {
8989
.acquire_timeout(Duration::from_secs_f64(
9090
config.database_connection_acquire_timeout_seconds,
9191
));
92-
pool_options = add_on_connection_handler(pool_options);
92+
pool_options = add_on_connection_handler(config, pool_options);
9393
pool_options
9494
}
9595
}
9696

97-
fn add_on_connection_handler(pool_options: PoolOptions<Any>) -> PoolOptions<Any> {
98-
let on_connect_file = std::env::current_dir()
99-
.unwrap_or_default()
100-
.join(ON_CONNECT_FILE);
97+
fn add_on_connection_handler(
98+
config: &AppConfig,
99+
pool_options: PoolOptions<Any>,
100+
) -> PoolOptions<Any> {
101+
let on_connect_file = config.configuration_directory.join(ON_CONNECT_FILE);
101102
if !on_connect_file.exists() {
102103
log::debug!("Not creating a custom SQL database connection handler because {on_connect_file:?} does not exist");
103104
return pool_options;

src/webserver/database/migrations.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ use sqlx::migrate::MigrateError;
77
use sqlx::migrate::Migration;
88
use sqlx::migrate::Migrator;
99

10-
pub async fn apply(db: &Database) -> anyhow::Result<()> {
11-
let migrations_dir = std::env::current_dir()
12-
.unwrap_or_default()
13-
.join(MIGRATIONS_DIR);
10+
pub async fn apply(config: &crate::app_config::AppConfig, db: &Database) -> anyhow::Result<()> {
11+
let migrations_dir = config.configuration_directory.join(MIGRATIONS_DIR);
1412
if !migrations_dir.exists() {
1513
log::info!(
1614
"Not applying database migrations because '{}' does not exist",

tests/index.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ async fn make_app_data() -> actix_web::web::Data<AppState> {
193193
init_log();
194194
let config = test_config();
195195
let state = AppState::init(&config).await.unwrap();
196-
let data = actix_web::web::Data::new(state);
197-
data
196+
197+
actix_web::web::Data::new(state)
198198
}
199199

200200
async fn req_path(

0 commit comments

Comments
 (0)