Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates 2024-11 #89

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ rev = "f46fbd5450505644ed9970cef1ae14164699981f"
[dependencies.ontodev_valve]
# path = "../ontodev_demo/valve.rs"
git = "https://github.com/ontodev/valve.rs"
rev = "824fbd79cd5ff787d8863186ccda09d0a0b56eb2"
rev = "94504e2604c3266d5e6a2abed18348b4e6a2ce4d"

[dependencies.ontodev_sqlrest]
git = "https://github.com/ontodev/sqlrest.rs"
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test-code: src/javascript/build/main.js
cargo test

.PHONY: test-docs
test-docs:
test-docs: target/debug/nanobot
PATH="$${PATH}:$$(pwd)/target/debug"; tesh --debug false ./doc

.PHONY: test
Expand Down Expand Up @@ -136,11 +136,12 @@ endif
BINARY_PATH := build/$(BINARY)

# Build a Linux binary using Musl instead of GCC.
target/x86_64-unknown-linux-musl/release/nanobot: src/*.rs
target/x86_64-unknown-linux-musl/release/nanobot: Cargo.* src/*.rs
docker pull clux/muslrust:stable
docker run \
-v cargo-cache:/root/.cargo/registry \
-v $$PWD:/volume \
-v /home/knocean/valve.rs:/valve.rs \
--rm -t clux/muslrust:stable \
cargo build --release

Expand Down
161 changes: 161 additions & 0 deletions doc/cgi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# CGI: Common Gateway Interface

Nanobot can be run as a
[CGI script](https://en.wikipedia.org/wiki/Common_Gateway_Interface).
This is an old-fashioned but simple and flexible
way to deploy a web application.
It's particularly easy to call use Nanobot CGI from a "wrapper" server,
written in another language, such as Python.

To run Nanobot as a CGI script
you just execute the `nanobot` binary,
with some specific environment variables
and optional `STDIN` input,
and it will return an HTTP response on `STDOUT`.

The CGI environment variables are:

- `GATEWAY_INTERFACE` set to `CGI/1.1` -- required
- `REQUEST_METHOD` for the HTTP method, defaults to `GET`
- `PATH_INFO` with the path part of the URL, defaults to `/table`
- `QUERY_STRING` with the query string part of the URL, which is optional

Nanobot also checks these environment variables:

- `NANOBOT_READONLY`: when `TRUE` no editing is supporting; defaults to `FALSE`

Nanobot will return an
[HTTP response](https://en.wikipedia.org/wiki/HTTP#HTTP/1.1_response_messages),
with a status line,
zero or more lines of HTTP headers,
and blank line,
and an optional message body
which will usually contain the HTML or JSON response content.

Nanobot's CGI mode works by
starting the same HTTP server used for `nanobot serve` on a random port,
executing the request,
and printing the response to `STDOUT`.
This is much less efficient than a long-running server,
but it's very simple
and works well enough for low volumes of traffic.

You can test Nanobot CGI from the command-line like so:

```console tesh-session="cgi"
$ nanobot init
Initialized a Nanobot project
$ GATEWAY_INTERFACE=CGI/1.1 PATH_INFO=/table.txt nanobot
status: 200 OK
content-type: text/plain
content-length: 291
date: ...

table path type description
table src/schema/table.tsv table All of the tables in this project.
column src/schema/column.tsv column Columns for all of the tables.
datatype src/schema/datatype.tsv datatype Datatypes for all of the columns
```

We can POST a new row using CGI and form contents as `STDIN`:

```console tesh-session="cgi"
$ export GATEWAY_INTERFACE=CGI/1.1
$ REQUEST_METHOD=POST PATH_INFO=/table nanobot <<< 'action=submit&table=foo'
...
$ PATH_INFO=/table.txt nanobot
status: 200 OK
content-type: text/plain
content-length: 337
date: ...

table path type description
table src/schema/table.tsv table All of the tables in this project.
column src/schema/column.tsv column Columns for all of the tables.
datatype src/schema/datatype.tsv datatype Datatypes for all of the columns
foo
```

When `NANOBOT_READONLY` is `TRUE`,
POSTing will not work,
and the WebUI will not include buttons for editing actions.

```console tesh-session="cgi"
$ export NANOBOT_READONLY=TRUE
$ REQUEST_METHOD=POST PATH_INFO=/table nanobot <<< 'action=submit&table=bar'
status: 403 Forbidden
content-type: text/html; charset=utf-8
content-length: 13
date: ...

403 Forbidden
```

## Python

You can run Nanobot CGI from any language that can "shell out" to another process.
In Python, you can use the `subprocess` module, like so:

```python
import subprocess

result = subprocess.run(
['bin/nanobot')],
env={
'GATEWAY_INTERFACE': 'CGI/1.1',
'REQUEST_METHOD': 'GET',
'PATH_INFO': path,
'QUERY_STRING': request.query_string,
},
input=request.body.getvalue().decode('utf-8'),
text=True,
capture_output=True
)
print(result.stdout)
```

If you're already running a Python server,
such as Flask or Bottle,
that provides a `request` and `response`,
then you can "wrap" Nanobot inside the Python server
with code similar to this:

```python
import subprocess
from bottle import request, response

def run_cgi(path):
# Run nanobot as a CGI script.
result = subprocess.run(
['bin/nanobot')],
env={
'GATEWAY_INTERFACE': 'CGI/1.1',
'REQUEST_METHOD': 'GET',
'PATH_INFO': path,
'QUERY_STRING': request.query_string,
},
input=request.body.getvalue().decode('utf-8'),
text=True,
capture_output=True
)
# Parse the HTTP response: status, headers, blank line, body.
reading_headers = True
body = []
for line in result.stdout.splitlines():
# Watch for the blank line that separates HTTP headers from the body.
if reading_headers and line.strip() == '':
reading_headers = False
continue
# Add each HTTP header to the `response`.
if reading_headers:
name, value = line.split(': ', 1)
if name == 'status':
response.status = value
else:
response.set_header(name, value)
# Add all remanining lines to the `response` body.
else:
body.append(line)
# The `response` is set, so just return the body.
return '\n'.join(body)
```
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Config {
pub pool: Option<AnyPool>,
pub valve: Option<Valve>,
pub valve_path: String,
pub editable: bool,
pub create_only: bool,
pub asset_path: Option<String>,
pub template_path: Option<String>,
Expand Down Expand Up @@ -319,6 +320,7 @@ impl Config {
.unwrap_or_default()
.path
.unwrap_or("src/schema/table.tsv".into()),
editable: true,
create_only: false,
asset_path: {
match user.assets.unwrap_or_default().path {
Expand Down Expand Up @@ -376,6 +378,11 @@ impl Config {
self
}

pub fn editable(&mut self, value: bool) -> &mut Config {
self.editable = value;
self
}

pub fn create_only(&mut self, value: bool) -> &mut Config {
self.create_only = value;
self
Expand Down
31 changes: 15 additions & 16 deletions src/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ async fn get_page(
for col_config in column_configs.iter() {
let key = col_config.column.to_string();
let mut cmap_entry = json!(col_config).as_object_mut().unwrap().clone();
let sql_type = toolkit::get_sql_type_from_global_config(&valve.config, table, &key, &pool);
let sql_type =
toolkit::get_sql_type_from_global_config(&valve.config, table, &key, &valve.db_kind);

// Get table.column that use this column as a foreign key constraint
// and insert as "links".
Expand Down Expand Up @@ -410,19 +411,14 @@ async fn get_page(
let end = select.offset.unwrap_or(0) + cell_rows.len();

// Start with the VALVE table config, minus 'column' and 'column_order'.
let this_table_config = valve.config.table.get(&unquoted_table).unwrap();
let this_table_config = json!(this_table_config);
let mut this_table_config = this_table_config.as_object().unwrap().clone();
this_table_config.remove("column");
this_table_config.remove("column_order");
// Try to get Nanobot table config: will fail for "message" and "history" tables.
let mut this_table = config
.table
.iter()
.filter(|x| x.get("table").unwrap() == &json!(unquoted_table))
.next()
.unwrap_or(&this_table_config)
.clone();
let this_table = json!(table_config);
let mut this_table = this_table.as_object().unwrap().clone();
this_table.remove("column");
this_table.remove("column_order");
this_table.insert(
"editable".to_string(),
json!(table_config.options.contains("edit")),
);
this_table.insert("table".to_string(), json!(unquoted_table.clone()));
this_table.insert("href".to_string(), json!(unquoted_table.clone()));
this_table.insert("start".to_string(), json!(select.offset.unwrap_or(0) + 1));
Expand Down Expand Up @@ -606,6 +602,7 @@ async fn get_page(
let result: Value = json!({
"page": {
"project_name": "Nanobot",
"editable": config.editable,
"tables": tables,
"title": unquoted_table,
"url": select2.to_url().unwrap_or_default(),
Expand Down Expand Up @@ -766,7 +763,8 @@ pub fn get_undo_message(config: &Config) -> Option<String> {
}
Some(valve) => valve,
};
let change = block_on(valve.get_change_to_undo()).ok()??;
let changes = block_on(valve.get_changes_to_undo(1)).ok()?;
let change = changes.into_iter().nth(1)?;
Some(String::from(format!("Undo '{}'", change.message)))
}

Expand All @@ -779,7 +777,8 @@ pub fn get_redo_message(config: &Config) -> Option<String> {
}
Some(valve) => valve,
};
let change = block_on(valve.get_change_to_redo()).ok()??;
let changes = block_on(valve.get_changes_to_redo(1)).ok()?;
let change = changes.into_iter().nth(1)?;
Some(String::from(format!("Redo '{}'", change.message)))
}

Expand Down
4 changes: 2 additions & 2 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,11 @@ pub async fn init(config: &mut Config) -> Result<String, String> {
};

// Create and/or load tables into database
match &config.valve {
match &mut config.valve {
None => unreachable!("Valve is not initialized."),
Some(valve) => {
if config.create_only {
if let Err(e) = valve.create_all_tables().await {
if let Err(e) = valve.ensure_all_tables_created(&vec![]).await {
return Err(format!(
"VALVE error while creating from {}: {:?}",
valve_path, e
Expand Down
Loading