-r for serverless rendering

pull/1/head
chris west 4 years ago
parent b527304e67
commit 822e1e0f26

@ -23,31 +23,31 @@
`phd` is a small, easy-to-use gopher server. `phd` is a small, easy-to-use gopher server.
Point it at a directory and it'll serve up all its text files, Point it at a directory and it'll serve up all the text files,
sub-directories, and binary files over Gopher. Any `.gph` files will sub-directories, and binary files over Gopher. Any `.gph` files will
be served up as [gopermaps][map] and executable `.gph` files will be be served up as [gopermaps][map] and executable `.gph` files will be
run as a program with their output served to the client, like the run as a program with their output served to the client, like the
glorious cgi-bin days of yore! glorious cgi-bin days of yore!
### special files: ### ~ special files ~
- **header.gph**: If it exists in a directory, its content will be - **header.gph**: If it exists in a directory, its content will be
shown above the directory's content. Put ASCII art in it. shown above the directory's content. Put ASCII art in it.
- **footer.gph**: Same, but will be shown below a directory's content. - **footer.gph**: Same, but will be shown below a directory's content.
- **index.gph**: Completely replaces a directory's content with what's - **index.gph**: Completely replaces a directory's content with what's
in this file. in this file.
- **??.gph**: Visiting gopher://yoursite/1/dog/ will try to render - **??.gph**: Visiting `gopher://yoursite/1/dog/` will try to render
`dog.gph` from disk. Visiting /1/dog.gph will render the raw content `dog.gph` from disk. Visiting `/1/dog.gph` will render the raw
of the .gph file. content of the .gph file.
- **.reverse**: If this exists, the directory contents will be listed - **.reverse**: If this exists, the directory contents will be listed
in reverse alphanumeric order. Useful for phloggin', if you date in reverse alphanumeric order. Useful for phloggin', if you date
your posts. your posts.
Any line in a `.gph` file that doesn't contain tabs (`\t`) and doesn't Any line in a `.gph` file that doesn't contain tabs (`\t`) will get an
start with an `i` will get an `i` automatically prefixed, turning it `i` automatically prefixed, turning it into a Gopher information item.
into a gopher information item.
Alternatively, phd supports [geomyidae][gmi] syntax: For your convenience, phd supports **[geomyidae][gmi]** syntax for
creating links:
This is an info line. This is an info line.
[1|This is a link|/help|server|port] [1|This is a link|/help|server|port]
@ -56,7 +56,10 @@ Alternatively, phd supports [geomyidae][gmi] syntax:
`server` and `port` will get translated into the server and port of `server` and `port` will get translated into the server and port of
the actively running server, eg `localhost` and `7070`. the actively running server, eg `localhost` and `7070`.
### dynamic content: Any line containing a tab character (`\t`) will be sent as-is to the
client, meaning you can write and serve up raw Gophermap files too.
### ~ dynamic content ~
Any `.gph` file that is marked **executable** with be run as if it Any `.gph` file that is marked **executable** with be run as if it
were a standalone program and its output will be sent to the client. were a standalone program and its output will be sent to the client.
@ -92,7 +95,7 @@ then:
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_| [INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
[INFO] |___/ |_| [INFO] |___/ |_|
### ruby on rails: ### ~ ruby on rails ~
`sh` is fun, but for serious work you need a serious scripting `sh` is fun, but for serious work you need a serious scripting
language like Ruby or PHP or Node.JS: language like Ruby or PHP or Node.JS:
@ -127,14 +130,17 @@ of Gopher!
isizes.gph 276B (null) 127.0.0.1 7070 isizes.gph 276B (null) 127.0.0.1 7070
isrc 224B (null) 127.0.0.1 7070 isrc 224B (null) 127.0.0.1 7070
## usage ## ~ usage ~
Usage:
phd [options] <root directory> phd [options] <root directory>
Options: Options:
-p, --port Port to bind to. -r, --render SELECTOR Render and print SELECTOR to stdout only.
-h, --host Hostname to use when generating links. -p, --port Port to bind to. [Default: {port}]
-h, --host Hostname for links. [Default: {host}]
Other flags: Other flags:
@ -146,9 +152,10 @@ of Gopher!
phd ./path/to/site # Serve directory over port 7070. phd ./path/to/site # Serve directory over port 7070.
phd -p 70 docs # Serve 'docs' directory on port 70 phd -p 70 docs # Serve 'docs' directory on port 70
phd -h gopher.com # Serve current directory over port 7070 phd -h gopher.com # Serve current directory over port 7070
# using hostname "gopher.com" # using hostname 'gopher.com'
phd -r / ./site # Render local gopher site to stdout.
## installation ## ~ installation ~
On macOS you can install with [Homebrew](https://brew.sh/): On macOS you can install with [Homebrew](https://brew.sh/):
@ -161,22 +168,25 @@ gopher://phkt.io/1/releases/phd and https://github.com/xvxx/phd/releases:
- [phd-v0.1.9-linux-armv7.tar.gz (Raspberry Pi)][1] - [phd-v0.1.9-linux-armv7.tar.gz (Raspberry Pi)][1]
- [phd-v0.1.9-macos.zip][2] - [phd-v0.1.9-macos.zip][2]
Just unzip/untar the `phd` program into your \$PATH and get going! Just unzip/untar the `phd` program into your `$PATH` and get going!
If you have **[cargo][rustup]**, you can install the crate directly:
cargo install phd
## development ## ~ development ~
cargo run -- ./path/to/gopher/site cargo run -- ./path/to/gopher/site
## resources ## ~ resources ~
- gopher://bitreich.org/1/scm/geomyidae/files.gph - gopher://bitreich.org/1/scm/geomyidae/files.gph
- https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap - https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
- https://gopher.zone/posts/how-to-gophermap/ - https://gopher.zone/posts/how-to-gophermap/
- [rfc 1436](https://tools.ietf.org/html/rfc1436) - [rfc 1436](https://tools.ietf.org/html/rfc1436)
## todo ## ~ todo ~
- [ ] script/serverless mode
- [ ] systemd config, or something - [ ] systemd config, or something
- [ ] TLS support - [ ] TLS support
- [ ] man page - [ ] man page
@ -187,4 +197,5 @@ Just unzip/untar the `phd` program into your \$PATH and get going!
[1]: https://github.com/xvxx/phd/releases/download/v0.1.9/phd-v0.1.9-linux-armv7.tar.gz [1]: https://github.com/xvxx/phd/releases/download/v0.1.9/phd-v0.1.9-linux-armv7.tar.gz
[2]: https://github.com/xvxx/phd/releases/download/v0.1.9/phd-v0.1.9-macos.zip [2]: https://github.com/xvxx/phd/releases/download/v0.1.9/phd-v0.1.9-macos.zip
[map]: https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu [map]: https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu
[gmi]: gopher://bitreich.org/1/scm/geomyidae [gmi]: http://r-36.net/scm/geomyidae/
[rustup]: https://rustup.rs

@ -5,17 +5,25 @@ const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7070; const DEFAULT_PORT: u16 = 7070;
fn main() { fn main() {
let args: Vec<String> = std::env::args().skip(1).collect(); let args = std::env::args().skip(1).collect::<Vec<_>>();
let mut args = args.iter();
let mut root = "."; let mut root = ".";
let mut iter = args.iter();
let mut host = DEFAULT_HOST; let mut host = DEFAULT_HOST;
let mut port = DEFAULT_PORT; let mut port = DEFAULT_PORT;
while let Some(arg) = iter.next() { let mut render = "";
while let Some(arg) = args.next() {
match arg.as_ref() { match arg.as_ref() {
"--version" | "-v" | "-version" => return print_version(), "--version" | "-v" | "-version" => return print_version(),
"--help" | "-help" => return print_help(), "--help" | "-help" => return print_help(),
"--render" | "-render" | "-r" => {
if let Some(path) = args.next() {
render = path;
} else {
render = "/";
}
}
"--port" | "-p" | "-port" => { "--port" | "-p" | "-port" => {
if let Some(p) = iter.next() { if let Some(p) = args.next() {
port = p port = p
.parse() .parse()
.map_err(|_| { .map_err(|_| {
@ -26,15 +34,15 @@ fn main() {
} }
} }
"-h" => { "-h" => {
if let Some(h) = iter.next() { if let Some(h) = args.next() {
host = h; host = &h;
} else { } else {
return print_help(); return print_help();
} }
} }
"--host" | "-host" => { "--host" | "-host" => {
if let Some(h) = iter.next() { if let Some(h) = args.next() {
host = h; host = &h;
} }
} }
_ => { _ => {
@ -42,12 +50,19 @@ fn main() {
eprintln!("unknown flag: {}", arg); eprintln!("unknown flag: {}", arg);
process::exit(1); process::exit(1);
} else { } else {
root = arg; root = &arg;
} }
} }
} }
} }
if !render.is_empty() {
return match phd::server::render(host, port, root, &render) {
Ok(out) => println!("{}", out),
Err(e) => eprintln!("{}", e),
};
}
if let Err(e) = phd::server::start(host, port, root) { if let Err(e) = phd::server::start(host, port, root) {
eprintln!("{}", e); eprintln!("{}", e);
} }
@ -61,13 +76,23 @@ fn print_help() {
Options: Options:
-p, --port Port to bind to. [Default: {port}] -r, --render SELECTOR Render and print SELECTOR to stdout only.
-h, --host Hostname when generating links. [Default: {host}] -p, --port Port to bind to. [Default: {port}]
-h, --host Hostname for links. [Default: {host}]
Other flags: Other flags:
-h, --help Print this screen. -h, --help Print this screen.
-v, --version Print phd version.", -v, --version Print phd version.
Examples:
phd ./path/to/site # Serve directory over port 7070.
phd -p 70 docs # Serve 'docs' directory on port 70
phd -h gopher.com # Serve current directory over port 7070
# using hostname 'gopher.com'
phd -r / ./site # Render local gopher site to stdout.
",
host = DEFAULT_HOST, host = DEFAULT_HOST,
port = DEFAULT_PORT, port = DEFAULT_PORT,
); );

@ -1,7 +1,7 @@
//! A simple multi-threaded Gopher server. //! A simple multi-threaded Gopher server.
use crate::{color, Request, Result}; use crate::{color, Request, Result};
use gophermap::{GopherMenu, ItemType}; use gophermap::ItemType;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
fs::{self, DirEntry}, fs::{self, DirEntry},
@ -62,7 +62,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
} }
/// Reads from the client and responds. /// Reads from the client and responds.
fn accept(stream: TcpStream, mut req: Request) -> Result<()> { fn accept(mut stream: TcpStream, mut req: Request) -> Result<()> {
let reader = BufReader::new(&stream); let reader = BufReader::new(&stream);
let mut lines = reader.lines(); let mut lines = reader.lines();
if let Some(Ok(line)) = lines.next() { if let Some(Ok(line)) = lines.next() {
@ -75,15 +75,24 @@ fn accept(stream: TcpStream, mut req: Request) -> Result<()> {
color::Reset color::Reset
); );
req.parse_request(&line); req.parse_request(&line);
write_response(&stream, req)?; write_response(&mut stream, req)?;
} }
Ok(()) Ok(())
} }
/// Render a response to a String.
pub fn render(host: &str, port: u16, root: &str, selector: &str) -> Result<String> {
let mut req = Request::from(host, port, root)?;
req.parse_request(&selector);
let mut out = vec![];
write_response(&mut out, req)?;
Ok(String::from_utf8_lossy(&out).into())
}
/// Writes a response to a client based on a Request. /// Writes a response to a client based on a Request.
fn write_response<'a, W>(w: &'a W, mut req: Request) -> Result<()> fn write_response<W>(w: &mut W, mut req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
@ -121,9 +130,9 @@ where
} }
/// Send a directory listing (menu) to the client based on a Request. /// Send a directory listing (menu) to the client based on a Request.
fn write_dir<'a, W>(w: &'a W, req: Request) -> Result<()> fn write_dir<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
if !fs_exists(&path) { if !fs_exists(&path) {
@ -144,7 +153,6 @@ where
)?; )?;
} }
let mut menu = GopherMenu::with_write(w);
let rel_path = req.relative_file_path(); let rel_path = req.relative_file_path();
// show directory entries // show directory entries
@ -161,8 +169,10 @@ where
rel_path.trim_end_matches('/'), rel_path.trim_end_matches('/'),
file_name.to_string_lossy() file_name.to_string_lossy()
); );
menu.write_entry( write!(
file_type(&entry), w,
"{}{}\t{}\t{}\t{}\r\n",
file_type(&entry).to_char(),
&file_name.to_string_lossy(), &file_name.to_string_lossy(),
&path, &path,
&req.host, &req.host,
@ -182,7 +192,8 @@ where
)?; )?;
} }
menu.end()?; write!(w, ".\r\n");
println!( println!(
"{}│{} Server reply:\t{}DIR {}{}{}", "{}│{} Server reply:\t{}DIR {}{}{}",
color::Green, color::Green,
@ -196,13 +207,13 @@ where
} }
/// Send a file to the client based on a Request. /// Send a file to the client based on a Request.
fn write_file<'a, W>(mut w: &'a W, req: Request) -> Result<()> fn write_file<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
let mut f = fs::File::open(&path)?; let mut f = fs::File::open(&path)?;
io::copy(&mut f, &mut w)?; io::copy(&mut f, w)?;
println!( println!(
"{}│{} Server reply:\t{}FILE {}{}{}", "{}│{} Server reply:\t{}FILE {}{}{}",
color::Green, color::Green,
@ -216,9 +227,9 @@ where
} }
/// Send a gophermap (menu) to the client based on a Request. /// Send a gophermap (menu) to the client based on a Request.
fn write_gophermap<'a, W>(mut w: &'a W, req: Request) -> Result<()> fn write_gophermap<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
@ -230,7 +241,7 @@ where
}; };
for line in reader.lines() { for line in reader.lines() {
w.write_all(gph_line_to_gopher(line, &req).as_bytes())?; write!(w, "{}", gph_line_to_gopher(line, &req))?;
} }
println!( println!(
"{}│{} Server reply:\t{}MAP {}{}{}", "{}│{} Server reply:\t{}MAP {}{}{}",
@ -298,9 +309,9 @@ fn gph_line_to_gopher(line: &str, req: &Request) -> String {
line line
} }
fn write_not_found<'a, W>(mut w: &'a W, req: Request) -> Result<()> fn write_not_found<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let line = format!("3Not Found: {}\t/\tnone\t70\r\n", req.selector); let line = format!("3Not Found: {}\t/\tnone\t70\r\n", req.selector);
println!( println!(
@ -310,7 +321,7 @@ where
req.relative_file_path(), req.relative_file_path(),
color::Reset, color::Reset,
); );
w.write_all(line.as_bytes())?; write!(w, "{}", line)?;
Ok(()) Ok(())
} }

Loading…
Cancel
Save