Skip to content

Commit db114d2

Browse files
committed
Clone server URL - subdomain support #502
1 parent 15e2db4 commit db114d2

File tree

14 files changed

+102
-54
lines changed

14 files changed

+102
-54
lines changed

.dagger/.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/sdk/** linguist-generated
2+
tar

lib/src/db.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ use self::{
4848
},
4949
val_prop_sub_index::add_atom_to_valpropsub_index,
5050
};
51-
5251
use sled::{transaction::TransactionError, Transactional};
5352

5453
// A function called by the Store when a Commit is accepted
@@ -86,9 +85,9 @@ pub struct Db {
8685
/// The address where the db will be hosted, e.g. http://localhost/
8786
server_url: String,
8887
/// Endpoints are checked whenever a resource is requested. They calculate (some properties of) the resource and return it.
89-
endpoints: Vec<Endpoint>,
88+
endpoints: Arc<Vec<Endpoint>>,
9089
/// List of class extenders.
91-
class_extenders: Vec<ClassExtender>,
90+
class_extenders: Arc<Vec<ClassExtender>>,
9291
/// Function called whenever a Commit is applied.
9392
on_commit: Option<Arc<HandleCommit>>,
9493
/// Where the DB is stored on disk.
@@ -118,8 +117,8 @@ impl Db {
118117
prop_val_sub_index,
119118
server_url,
120119
watched_queries,
121-
endpoints: plugins::default_endpoints(),
122-
class_extenders: plugins::default_class_extenders(),
120+
endpoints: Arc::new(plugins::default_endpoints()),
121+
class_extenders: Arc::new(plugins::default_class_extenders()),
123122
on_commit: None,
124123
};
125124
migrate_maybe(&store).map(|e| format!("Error during migration of database: {:?}", e))?;
@@ -128,6 +127,15 @@ impl Db {
128127
Ok(store)
129128
}
130129

130+
/// Creates a clone of the store with a different server_url.
131+
/// This is useful for multi-tenant applications.
132+
/// Cloning is very cheap, as it only clones the pointers to the Sled trees.
133+
pub fn clone_with_url(&self, server_url: String) -> Db {
134+
let mut clone = self.clone();
135+
clone.server_url = server_url;
136+
clone
137+
}
138+
131139
/// Create a temporary Db in `.temp/db/{id}`. Useful for testing.
132140
/// Populates the database, creates a default agent, and sets the server_url to "http://localhost/".
133141
pub fn init_temp(id: &str) -> AtomicResult<Db> {

lib/src/plugins/bookmark.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ impl Parser {
339339
})]
340340
}
341341

342-
fn resolve_relative_path_handler(&self) -> Handler {
342+
fn resolve_relative_path_handler(&self) -> Handler<'_, '_> {
343343
vec![element!("*[src], *[href]", |el| {
344344
if let Some(src) = el.get_attribute("src") {
345345
el.set_attribute("src", &self.resolve_url(&src))?;
@@ -353,7 +353,7 @@ impl Parser {
353353
})]
354354
}
355355

356-
fn convert_svg_to_image_handler(&self) -> Handler {
356+
fn convert_svg_to_image_handler(&self) -> Handler<'_, '_> {
357357
vec![element!("svg", |el| {
358358
let id = el.get_attribute("id").ok_or("no id in SVG")?;
359359
let svg = self.svg_map.get(&id).ok_or("no SVG found with id")?;
@@ -370,7 +370,7 @@ impl Parser {
370370
})]
371371
}
372372

373-
fn simplify_link_text_handler(&self) -> Handler {
373+
fn simplify_link_text_handler(&self) -> Handler<'_, '_> {
374374
vec![element!("a *", |el| {
375375
let tag_name = el.tag_name().to_lowercase();
376376
if tag_name != "img" && tag_name != "picture" {
@@ -381,28 +381,28 @@ impl Parser {
381381
})]
382382
}
383383

384-
fn transform_figures_handler(&self) -> Handler {
384+
fn transform_figures_handler(&self) -> Handler<'_, '_> {
385385
vec![element!("figure", |el| {
386386
el.remove_and_keep_content();
387387
Ok(())
388388
})]
389389
}
390390

391-
fn transform_figcaptions_handler(&self) -> Handler {
391+
fn transform_figcaptions_handler(&self) -> Handler<'_, '_> {
392392
vec![element!("figcaption", |el| {
393393
el.set_tag_name("P")?;
394394
Ok(())
395395
})]
396396
}
397397

398-
fn unfold_sup_elements_handler(&self) -> Handler {
398+
fn unfold_sup_elements_handler(&self) -> Handler<'_, '_> {
399399
vec![element!("sup", |el| {
400400
el.remove_and_keep_content();
401401
Ok(())
402402
})]
403403
}
404404

405-
fn trim_link_text_handler(&self) -> Handler {
405+
fn trim_link_text_handler(&self) -> Handler<'_, '_> {
406406
vec![
407407
element!("a", |el| {
408408
self.anchor_text_buffer.borrow_mut().clear();

lib/src/resources.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ impl Resource {
606606
atoms
607607
}
608608

609+
#[cfg(feature = "rdf")]
609610
pub fn vec_to_n_triples(
610611
resources: &Vec<Resource>,
611612
store: &impl Storelike,

lib/src/storelike.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ impl ResourceResponse {
7676
}
7777
}
7878

79+
#[cfg(feature = "rdf")]
7980
pub fn to_n_triples(&self, store: &impl Storelike) -> AtomicResult<String> {
8081
match self {
8182
ResourceResponse::Resource(resource) => Ok(resource.to_n_triples(store)?),

server/src/config.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ pub struct Opts {
9898
/// Introduces random delays in the server, to simulate a slow connection. Useful for testing.
9999
#[clap(long, env = "ATOMIC_SLOW_MODE")]
100100
pub slow_mode: bool,
101+
102+
/// The base domain for multi-tenant hosting.
103+
/// If set, the server will allow serving subdomains of this domain (e.g. *.atomicserver.eu).
104+
#[clap(long, env = "ATOMIC_BASE_DOMAIN")]
105+
pub base_domain: Option<String>,
101106
}
102107

103108
#[derive(clap::ValueEnum, Clone, Debug)]
@@ -196,6 +201,35 @@ pub struct Config {
196201
pub search_index_path: PathBuf,
197202
/// If true, the initialization scripts will be ran (create first Drive, Agent, indexing, etc)
198203
pub initialize: bool,
204+
/// The base domain for multi-tenant hosting.
205+
pub base_domain: Option<String>,
206+
}
207+
208+
impl Config {
209+
/// Returns the server URL for a given request.
210+
/// If multi-tenancy is enabled and the host matches a subdomain of the base domain, it returns the host URL.
211+
pub fn get_server_url_for_request(&self, req: &actix_web::HttpRequest) -> String {
212+
if let Some(base) = &self.base_domain {
213+
if let Some(host) = req.head().headers.get("Host") {
214+
if let Ok(host_str) = host.to_str() {
215+
// Remove port if present
216+
let domain = host_str.split(':').next().unwrap_or(host_str);
217+
if domain.ends_with(base) {
218+
let schema =
219+
if let Some(proto) = req.head().headers.get("X-Forwarded-Proto") {
220+
proto.to_str().unwrap_or("http")
221+
} else if self.opts.https {
222+
"https"
223+
} else {
224+
"http"
225+
};
226+
return format!("{}://{}", schema, host_str);
227+
}
228+
}
229+
}
230+
}
231+
self.server_url.clone()
232+
}
199233
}
200234

201235
/// Parse .env and CLI options
@@ -289,6 +323,8 @@ pub fn build_config(opts: Opts) -> AtomicServerResult<Config> {
289323
format!("{}://{}:{}", schema, opts.domain, opts.port)
290324
};
291325

326+
let base_domain = opts.base_domain.clone();
327+
292328
Ok(Config {
293329
initialize,
294330
opts,
@@ -302,5 +338,6 @@ pub fn build_config(opts: Opts) -> AtomicServerResult<Config> {
302338
store_path,
303339
search_index_path,
304340
uploads_path,
341+
base_domain,
305342
})
306343
}

server/src/errors.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ impl std::fmt::Debug for AtomicServerError {
3131
}
3232
}
3333

34-
#[derive(Serialize)]
35-
pub struct AppErrorResponse {
36-
pub error: String,
37-
}
38-
3934
impl Error for AtomicServerError {}
4035

4136
impl ResponseError for AtomicServerError {

server/src/handlers/commit.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use atomic_lib::{commit::CommitOpts, parse::parse_json_ad_commit_resource, Commi
77
#[tracing::instrument(skip(appstate))]
88
pub async fn post_commit(
99
appstate: web::Data<AppState>,
10+
req: actix_web::HttpRequest,
1011
body: String,
1112
) -> AtomicServerResult<HttpResponse> {
1213
if appstate.config.opts.slow_mode {
@@ -15,9 +16,11 @@ pub async fn post_commit(
1516
let random_number = rng.gen_range(100..1000);
1617
tokio::time::sleep(tokio::time::Duration::from_millis(random_number)).await;
1718
}
18-
let store = &appstate.store;
19+
let server_url = appstate.config.get_server_url_for_request(&req);
20+
let store = appstate.store.clone_with_url(server_url);
1921
let mut builder = HttpResponse::Ok();
20-
let incoming_commit_resource = parse_json_ad_commit_resource(&body, store)?;
22+
23+
let incoming_commit_resource = parse_json_ad_commit_resource(&body, &store)?;
2124
let incoming_commit = Commit::from_resource(incoming_commit_resource)?;
2225
if !incoming_commit.subject.contains(
2326
&store

server/src/handlers/download.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ pub async fn handle_download(
2424
req: actix_web::HttpRequest,
2525
) -> AtomicServerResult<HttpResponse> {
2626
let headers = req.headers();
27-
let server_url = &appstate.config.server_url;
28-
let store = &appstate.store;
27+
let server_url = appstate.config.get_server_url_for_request(&req);
28+
let store = appstate.store.clone_with_url(server_url.clone());
2929

3030
// We replace `/download` with `/` to get the subject of the Resource.
3131
let subject = if let Some(pth) = path {

server/src/handlers/export.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ pub async fn handle_export(
2929
req: actix_web::HttpRequest,
3030
) -> AtomicServerResult<HttpResponse> {
3131
let headers = req.headers();
32-
let store = &appstate.store;
32+
let server_url = appstate.config.get_server_url_for_request(&req);
33+
let store = appstate.store.clone_with_url(server_url);
3334

3435
let Some(subject) = params.subject.clone() else {
3536
return Err("No subject provided".into());
@@ -45,7 +46,7 @@ pub async fn handle_export(
4546
match format.as_str() {
4647
"csv" => {
4748
let exporter = CSVExporter {
48-
store,
49+
store: &store,
4950
agent: &for_agent,
5051
display_refs_as_name,
5152
};

0 commit comments

Comments
 (0)