Compare commits

...

50 Commits

Author SHA1 Message Date
chris west 5b02ccc1db chore: Release phd version 0.1.15 2 years ago
chris west 1e31a05f9d update cargo-release 2 years ago
chris west 006a3f58a5 update changelog 2 years ago
chris west 6761e1d738 update alphanumeric-sort dependency 2 years ago
chris west ae866b4609 update cargo install instructions 2 years ago
chris west 555504689f update gh actions 2 years ago
chris west a0ab2fa9c5
update status 2 years ago
chris west fb304bce9e (cargo-release) start next development iteration 0.1.15-dev 4 years ago
chris west be85890d84 (cargo-release) version 0.1.14 4 years ago
chris west 2e78294135 NO_COLOR support 4 years ago
chris west 0db0c7e149 update changelog on release 4 years ago
chris west 10ba1ca69d (cargo-release) start next development iteration 0.1.14-dev 4 years ago
chris west fe7e2e43e6 (cargo-release) version 0.1.13 4 years ago
chris west 4c0d707a7a update changelog 4 years ago
chris west 06c0a2d5ea also look for ? to start query string
fixes #3
4 years ago
chris west 954855fd80 update changelog 4 years ago
chris west 1cb71b3450 If -p is passed without --bind, bind to that port 4 years ago
chris west 4de39201ba how'd you get down there 4 years ago
chris west 4ee7008070 --no-color to hide colors in log messages 4 years ago
chris west de8607bee8 (cargo-release) start next development iteration 0.1.13-dev 4 years ago
chris west db0de166e8 (cargo-release) version 0.1.12 4 years ago
chris west 19e7579a4f update changelog for 0.1.12 4 years ago
chris west f3ff3fb3eb mention -b in readme 4 years ago
chris west 924c5b1ae9 clarify 4 years ago
chris west 68979f2d88 add --bind to -h 4 years ago
Richard Bradfield a07c921fee
Default to dual stack and add BIND arg
Change the default bind address to be `[::]` and allow the user to
specify the socket to bind on.

This is distinct from the HOST/PORT pair already configurable as phd
might be running behind a proxy where links need to be generated with a
different destination to the bound socket.
4 years ago
chris west f3bfbbd9f5 (cargo-release) start next development iteration 0.1.12-dev 4 years ago
chris west 3d19ac030c (cargo-release) version 0.1.11 4 years ago
chris west e7e0ff9294 update changelog 4 years ago
chris west dc1f48e917 basic manpage 4 years ago
chris west 3b0d9de144 (cargo-release) start next development iteration 0.1.11-dev 4 years ago
chris west d63f0e866b (cargo-release) version 0.1.10 4 years ago
chris west 85865968e3 hide info msgs in -r mode 4 years ago
chris west c0e0aa4548 add changelog 4 years ago
chris west 36cdefd32c use phetch's gopher Type enum 4 years ago
chris west 03b557312c bolder 4 years ago
chris west 822e1e0f26 -r for serverless rendering 4 years ago
chris west b527304e67 different indicator 4 years ago
chris west ee2d324a98 line 4 years ago
chris west 73a0016893 update badge url 4 years ago
chris west 63b12145ad badges 4 years ago
chris west 4d712e9a05 include fixture dir 4 years ago
chris west 497185c2e3 gh actions 4 years ago
chris west 4717780bfb (cargo-release) start next development iteration 0.1.10-dev 4 years ago
chris west ab890ec450 (cargo-release) version 0.1.9 4 years ago
chris west bc30c3a209 center logo 4 years ago
chris west 2714841316 format tweaks 4 years ago
chris west 49ae7abb63 always prefix lines without \t with i 4 years ago
chris west 67665c3327 avoid double // 4 years ago
chris west 9964d2c530 (cargo-release) start next development iteration 0.1.9-dev 4 years ago

@ -0,0 +1,73 @@
on:
push:
branches: [master]
pull_request:
branches: ["*"]
name: build
jobs:
test_macos:
name: Run Tests on macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy
override: true
- name: check
run: cargo check
- name: clippy
run: cargo clippy
- name: test
run: cargo test
- name: build
run: cargo build --release
test_ubuntu:
name: Run Tests on Ubuntu (x86_64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: check
run: cargo check
- name: clippy
run: cargo clippy
- name: test
run: cargo test
- name: build
run: cargo build --release

@ -0,0 +1,162 @@
name: Create Release
on:
push:
tags:
- "v*.*.*"
jobs:
build_armv7:
name: Build for armv7
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: armv7-unknown-linux-gnueabihf
override: true
- name: Build release
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --locked --target armv7-unknown-linux-gnueabihf
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phd.1 target/armv7-unknown-linux-gnueabihf/release && cd target/armv7-unknown-linux-gnueabihf/release && tar zcvf phd-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz phd phd.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phd-linux-armv7
path: target/armv7-unknown-linux-gnueabihf/release/phd-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz
build_linux:
name: Build for Linux x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Build release
run: cargo build --locked --release
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phd.1 target/release && cd target/release && tar zcvf phd-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz phd phd.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phd-linux-x86_64
path: target/release/phd-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz
build_macos:
name: Build for macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Build release
uses: actions-rs/cargo@v1
with:
command: build
args: --locked --release
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phd.1 target/release && cd target/release && zip -r phd-${{ steps.get_version.outputs.VERSION }}-macos.zip phd phd.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phd-macos
path: target/release/phd-${{ steps.get_version.outputs.VERSION }}-macos.zip
create:
name: Create Release
needs: [build_armv7, build_linux, build_macos]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Download macOS artifact
uses: actions/download-artifact@v1
with:
name: phd-macos
- name: Download Linux (x86_64) artifact
uses: actions/download-artifact@v1
with:
name: phd-linux-x86_64
- name: Download Linux (armv7) artifact
uses: actions/download-artifact@v1
with:
name: phd-linux-armv7
- name: Create Release
uses: softprops/action-gh-release@v1
with:
draft: true
prerelease: true
files: |
phd-macos/phd-${{ steps.get_version.outputs.VERSION }}-macos.zip
phd-linux-x86_64/phd-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz
phd-linux-armv7/phd-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz
body_path: CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -0,0 +1,62 @@
## v0.1.15
- Update `alphanumeric-sort` dependency so `cargo install phd` works again.
- Recommend `cargo install phd --locked` in the README
## v0.1.14
- If the `NO_COLOR` env variable is set, colors won't be printed to
the log. Same as starting with `--no-color`.
See https://no-color.org/.
## v0.1.13
- Added `--no-color` command line option to not display color when
logging.
- Slight change to binding behavior: if `-p` is passed without `-b`,
we'll try to bind to that port. To this easier: `phd -p 7777`
- Accept `?` as query string indicator, not just `TAB`. See #3.
## v0.1.12
`phd` now uses `-b` and `--bind` to set the host and port to
bind to. `-p` and `-h` are now strictly for URL generation.
This should hopefully make it easier to run `phd` behind a
proxy and still generate proper links.
Thanks to @bradfier for the patch!
## v0.1.11
`phd` now ships with a basic manual!
It will be installed via homebrew and (eventually) AUR.
For now you can view it by cloning the repository and running:
man ./doc/phd.1
Enjoy!
## v0.1.10
`phd` can now render a single page to stdout, instead of starting
as a server. Those of us in the biz refer to this as "serverless".
For example, if your Gopher site lives in `/srv/gopher` and you want
to render the main page, just run:
phd -r / /srv/gopher
This will print the raw Gopher menu to stdout!
To view the "/about" page, pass that selector:
phd -r / /srv/gopher
Edge computing is now Gopher-scale! Enjoy!
## v0.1.9
Switch to using GitHub Actions for release automation.

49
Cargo.lock generated

@ -1,81 +1,74 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "alphanumeric-sort"
version = "1.0.11"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0"
[[package]]
name = "content_inspector"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr",
]
[[package]]
name = "gophermap"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hermit-abi"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
dependencies = [
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"libc",
]
[[package]]
name = "libc"
version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
[[package]]
name = "memchr"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
[[package]]
name = "num_cpus"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
dependencies = [
"hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"hermit-abi",
"libc",
]
[[package]]
name = "phd"
version = "0.1.8"
version = "0.1.15"
dependencies = [
"alphanumeric-sort 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
"content_inspector 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"gophermap 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"alphanumeric-sort",
"content_inspector",
"shell-escape",
"threadpool",
]
[[package]]
name = "shell-escape"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170a13e64f2a51b77a45702ba77287f5c6829375b04a69cf2222acd17d0cfab9"
[[package]]
name = "threadpool"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
dependencies = [
"num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus",
]
[metadata]
"checksum alphanumeric-sort 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f37ce94154d73f6961f87571a3ab7814e1608f373bd55a933e3e771b6dd59fc4"
"checksum content_inspector 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
"checksum gophermap 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6ec2186bfad5a5dcbc9307dbc2d2444062300a836ae91b00dd80c3b71c34af3b"
"checksum hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
"checksum shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "170a13e64f2a51b77a45702ba77287f5c6829375b04a69cf2222acd17d0cfab9"
"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"

@ -1,6 +1,6 @@
[package]
name = "phd"
version = "0.1.8"
version = "0.1.15"
authors = ["chris west <c@xvxx.io>"]
license = "MIT"
edition = "2018"
@ -22,12 +22,11 @@ opt-level = 'z' # Optimize for size.
pre-release-replacements = [
{file="README.md", search="phd-v\\d+\\.\\d+\\.\\d+-", replace="{{crate_name}}-v{{version}}-"},
{file="README.md", search="/v\\d+\\.\\d+\\.\\d+/", replace="/v{{version}}/"},
{file="CHANGELOG.md", search="\\d+\\.\\d+\\.\\d+-dev", replace="{{version}}"},
]
dev-version-ext = "dev"
[dependencies]
content_inspector = "0.2.4"
threadpool = "1.7.1"
gophermap = "0.1.2"
alphanumeric-sort = "1.0.11"
alphanumeric-sort = "1.4"
shell-escape = "0.1.4"

@ -34,3 +34,15 @@ $(RELEASE): $(SOURCES)
# Build the debug version
$(DEBUG): $(SOURCES)
cargo build
# Build manual
.PHONY: manual
manual: doc/phd.1
doc/phd.1: doc/phd.1.md scdoc
scdoc < doc/phd.1.md > doc/phd.1
# Must have scdoc installed to build manual.
scdoc:
@which scdoc || (echo "scdoc(1) not found."; \
echo "please install to build the manual: https://repology.org/project/scdoc"; exit 1)

@ -4,39 +4,50 @@
| )| )| )
|__/ | / |__/
|
--> <p align="center"> <img src="./img/logo.png"> <br>
-->
<p align="center">
<img src="./img/logo.png"> <br>
<a href="https://github.com/xvxx/phd/releases">
<img src="https://img.shields.io/github/v/release/xvxx/phd?include_prereleases">
</a>
<a href="https://crates.io/crates/phd">
<img src="https://img.shields.io/crates/v/phd">
</a>
<a href="https://github.com/xvxx/phd/actions?query=workflow%3Abuild">
<img src="https://github.com/xvxx/phd/workflows/build/badge.svg">
</a>
</p>
---
`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
be served up as [gopermaps][map] and executable `.gph` files will be
be served up as [gophermaps][map] and executable `.gph` files will be
run as a program with their output served to the client, like the
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.
- **footer.gph**: Same, but will be shown below a directory's content.
- **index.gph**: Completely replaces a directory's content with what's
- **`footer.gph`**: Same, but will be shown below a directory's content.
- **`index.gph`**: Completely replaces a directory's content with what's
in this file.
- **??.gph**: Visiting gopher://yoursite/1/dog/ will try to render
`dog.gph` from disk. Visiting /1/dog.gph will render the raw content
of the .gph file.
- **.reverse**: If this exists, the directory contents will be listed
- **`??.gph`**: Visiting `gopher://yoursite/1/dog/` will try to render
`dog.gph` from disk. Visiting `/1/dog.gph` will render the raw
content of the .gph file.
- **`.reverse`**: If this exists, the directory contents will be listed
in reverse alphanumeric order. Useful for phloggin', if you date
your posts.
Any line in a `.gph` file that doesn't contain tabs (`\t`) and doesn't
start with an `i` will get an `i` automatically prefixed, turning it
into a gopher information item.
Any line in a `.gph` file that doesn't contain tabs (`\t`) will get an
`i` automatically prefixed, turning it 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.
[1|This is a link|/help|server|port]
@ -45,7 +56,10 @@ Alternatively, phd supports [geomyidae][gmi] syntax:
`server` and `port` will get translated into the server and port of
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
were a standalone program and its output will be sent to the client.
@ -81,7 +95,7 @@ then:
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
[INFO] |___/ |_|
### ruby on rails:
### ~ ruby on rails ~
`sh` is fun, but for serious work you need a serious scripting
language like Ruby or PHP or Node.JS:
@ -101,7 +115,7 @@ language like Ruby or PHP or Node.JS:
end
Now you can finally share the file sizes of a directory with the world
of Gopher!
of Gopher!
$ phetch -r 0.0.0.0:7070/1/sizes
i~ file sizes ~ (null) 127.0.0.1 7070
@ -116,14 +130,19 @@ of Gopher!
isizes.gph 276B (null) 127.0.0.1 7070
isrc 224B (null) 127.0.0.1 7070
## usage
## ~ usage ~
Usage:
phd [options] <root directory>
Options:
-p, --port Port to bind to.
-h, --host Hostname to use when generating links.
-r, --render SELECTOR Render and print SELECTOR to stdout only.
-h, --host HOST Hostname for links. [Default: {host}]
-p, --port PORT Port for links. [Default: {port}]
-b, --bind ADDRESS Socket address to bind to. [Default: {bind}]
--no-color Don't show colors in log messages.
Other flags:
@ -135,45 +154,52 @@ of Gopher!
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"
# 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/):
brew install xvxx/code/phd
Binaries for Linux, Mac, and Raspberry Pi are available at
Binaries for Linux, Mac, and Raspberry Pi are available at
gopher://phkt.io/1/releases/phd and https://github.com/xvxx/phd/releases:
- [phd-v0.1.8-linux-x86_64.tar.gz][0]
- [phd-v0.1.8-linux-armv7.tar.gz (Raspberry Pi)][1]
- [phd-v0.1.8-macos.zip][2]
- [phd-v0.1.15-linux-x86_64.tar.gz][0]
- [phd-v0.1.15-linux-armv7.tar.gz (Raspberry Pi)][1]
- [phd-v0.1.15-macos.zip][2]
Just unzip/untar the `phd` program into your `$PATH` and get going!
If you have **[cargo][rustup]**, you can install the crate directly:
Just unzip/untar the `phd` program into your $PATH and get going!
cargo install phd --locked
## development
## ~ development ~
cargo run -- ./path/to/gopher/site
## resources
## ~ resources ~
- gopher://bitreich.org/1/scm/geomyidae/files.gph
- https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
- https://gopher.zone/posts/how-to-gophermap/
- [rfc 1436](https://tools.ietf.org/html/rfc1436)
## todo
## ~ todo ~
- [ ] script/serverless mode
- [ ] systemd config, or something
- [ ] TLS support
- [ ] man page
- [ ] ipv6
- [ ] user input sanitization tests
[0]: https://github.com/xvxx/phd/releases/download/v0.1.8/phd-v0.1.8-linux-x86_64.tar.gz
[1]: https://github.com/xvxx/phd/releases/download/v0.1.8/phd-v0.1.8-linux-armv7.tar.gz
[2]: https://github.com/xvxx/phd/releases/download/v0.1.8/phd-v0.1.8-macos.zip
## ~ status ~
phd is no longer under active development, but the latest version works great.
[0]: https://github.com/xvxx/phd/releases/download/v0.1.15/phd-v0.1.15-linux-x86_64.tar.gz
[1]: https://github.com/xvxx/phd/releases/download/v0.1.15/phd-v0.1.15-linux-armv7.tar.gz
[2]: https://github.com/xvxx/phd/releases/download/v0.1.15/phd-v0.1.15-macos.zip
[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

@ -0,0 +1,206 @@
.\" Generated by scdoc 1.11.0
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "PHD" "1" "2020-06-27"
.P
.SH NAME
.P
phd - an estoeric gopher server
.P
.SH SYNOPSIS
.P
\fBphd\fR [\fIOPTIONS\fR] [\fISITE ROOT\fR]
.P
.SH DESCRIPTION
.P
\fBphd\fR is a small, easy-to-use gopher server.
.P
Point it at a directory and it'll serve up all the text files,
sub-directories, and binary files over Gopher. Any \fB.gph\fR files will
be served up as Gophermaps and executable \fB.gph\fR files will be
run as a program with their output served to the client, like the
glorious cgi-bin days of yore!
.P
Usually \fBphd\fR is started with a path to your Gopher site:
.P
.RS 4
phd /srv/gopher
.P
.RE
If no path is given, \fBphd\fR will use the current directory as the root
of your Gopher site.
.P
.SH OPTIONS
.P
\fB-r\fR \fISELECTOR\fR, \fB--render\fR \fISELECTOR\fR
.P
.RS 4
Rather than start as a server, render the \fISELECTOR\fR of the site using the options provided and print the raw response to \fBSTDOUT\fR.
.P
.RE
\fB-b\fR \fIADDRESS\fR, \fB--bind\fR \fIADDRESS\fR
.RS 4
Set the socket address to bind to, e.g. \fB127.0.0.1:7070\fR
.P
.RE
\fB-p\fR \fIPORT\fR, \fB--port\fR \fIPORT\fR
.RS 4
Set the \fIPORT\fR to use when generating Gopher links.
.P
.RE
\fB-h\fR \fIHOST\fR, \fB--host\fR \fIHOST\fR
.RS 4
Set the \fIHOST\fR to use when generating Gopher links.
.P
.RE
\fB-h\fR, \fB--help\fR
.RS 4
Print a help summary and exit.
.P
.RE
\fB-v\fR, \fB--version\fR
.RS 4
Print version information and exit.
.P
.RE
.SH SPECIAL FILES
.P
The following files have special behavior when present in a directory
that \fBphd\fR is tasked with serving:
.P
\fBheader.gph\fR
.RS 4
If it exists in a directory, its content will be shown above the directory's content. Put ASCII art in it.
.P
.RE
\fBfooter.gph\fR
.RS 4
Same, but will be shown below a directory's content.
.P
.RE
\fBindex.gph\fR
.RS 4
Completely replaces a directory's content with what's in this file.
.P
.RE
\fB??.gph\fR
.RS 4
Visiting \fBgopher://yoursite/1/dog/\fR will try to render \fBdog.gph\fR from disk. Visiting \fB/1/dog.gph\fR will render the raw content of the .gph file.
.P
.RE
\fB.reverse\fR
.RS 4
If this exists, the directory contents will be listed in reverse alphanumeric order. Useful for phloggin', if you date your posts.
.P
.RE
.SH GOPHERMAP SYNTAX
.P
Any line in a \fB.gph\fR file that doesn't contain tabs (\fBt\fR) will get an
\fBi\fR automatically prefixed, turning it into a Gopher information item.
.P
For your convenience, phd supports \fBgeomyidae\fR syntax for
creating links:
.P
.nf
.RS 4
This is an info line\&.
[1|This is a link|/help|server|port]
[h|URL Link|URL:https://noogle\&.com]
.fi
.RE
.P
\fBserver\fR and \fBport\fR will get translated into the server and port of
the actively running server, eg \fBlocalhost\fR and \fB7070\fR.
.P
Any line containing a tab character (\fBt\fR) will be sent as-is to the
client, meaning you can write and serve up raw Gophermap files too.
.P
.SH DYNAMIC CONTENT
.P
Any \fB.gph\fR file that is marked \fBexecutable\fR with be run as if it
were a standalone program and its output will be sent to the client.
It will be passed three arguments: the query string (if any), the
server's hostname, and the current port. Do with them what you will.
.P
For example:
.P
.nf
.RS 4
$ cat echo\&.gph
#!/bin/sh
echo "Hi, world! You said:" $1
echo "1Visit Gopherpedia / gopherpedia\&.com 70"
.fi
.RE
.P
Then:
.P
.nf
.RS 4
$ gopher-client gopher://localhost/1/echo?something
[INFO] Hi, world! You said: something
[LINK] Visit Gopherpedia
.fi
.RE
.P
Or more seriously:
.P
.nf
.RS 4
$ cat figlet\&.gph
#!/bin/sh
figlet $1
.fi
.RE
.P
then:
.P
.nf
.RS 4
$ gopher-client gopher://localhost/1/figlet?hi gopher
[INFO] _ _ _
[INFO] | |__ (_) __ _ ___ _ __ | |__ ___ _ __
[INFO] | '_ | | / _` |/ _ | '_ | '_ / _ '__|
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
[INFO] |_| |_|_| __, |___/| \&.__/|_| |_|___|_|
[INFO] |___/ |_|
.fi
.RE
.P
.SS RESOURCES
.P
geomyidae source code
.RS 4
gopher://bitreich.org/1/scm/geomyidae/files.gph
.P
.RE
Example Gophermap
.RS 4
https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
.P
.RE
Gophermaps
.RS 4
https://gopher.zone/posts/how-to-gophermap/
.P
.RE
RFC 1436:
.RS 4
https://tools.ietf.org/html/rfc1436
.P
.RE
.SH ABOUT
.P
\fBphd\fR is maintained by chris west and released under the MIT license.
.P
phd's Gopher hole:
.RS 4
\fIgopher://phkt.io/1/phd\fR
.RE
phd's webpage:
.RS 4
\fIhttps://github.com/xvxx/phd\fR

@ -0,0 +1,154 @@
PHD(1)
# NAME
phd - an estoeric gopher server
# SYNOPSIS
*phd* [_OPTIONS_] [_SITE ROOT_]
# DESCRIPTION
*phd* is a small, easy-to-use gopher server.
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
be served up as Gophermaps and executable *.gph* files will be
run as a program with their output served to the client, like the
glorious cgi-bin days of yore!
Usually *phd* is started with a path to your Gopher site:
phd /srv/gopher
If no path is given, *phd* will use the current directory as the root
of your Gopher site.
# OPTIONS
*-r* _SELECTOR_, *--render* _SELECTOR_
Rather than start as a server, render the _SELECTOR_ of the site using the options provided and print the raw response to *STDOUT*.
*-b* _ADDRESS_, *--bind* _ADDRESS_
Set the socket address to bind to, e.g. *127.0.0.1:7070*
*-p* _PORT_, *--port* _PORT_
Set the _PORT_ to use when generating Gopher links.
*-h* _HOST_, *--host* _HOST_
Set the _HOST_ to use when generating Gopher links.
*-h*, *--help*
Print a help summary and exit.
*-v*, *--version*
Print version information and exit.
# SPECIAL FILES
The following files have special behavior when present in a directory
that *phd* is tasked with serving:
*header.gph*
If it exists in a directory, its content will be shown above the directory's content. Put ASCII art in it.
*footer.gph*
Same, but will be shown below a directory's content.
*index.gph*
Completely replaces a directory's content with what's in this file.
*??.gph*
Visiting *gopher://yoursite/1/dog/* will try to render *dog.gph* from disk. Visiting */1/dog.gph* will render the raw content of the .gph file.
*.reverse*
If this exists, the directory contents will be listed in reverse alphanumeric order. Useful for phloggin', if you date your posts.
# GOPHERMAP SYNTAX
Any line in a *.gph* file that doesn't contain tabs (*\t*) will get an
*i* automatically prefixed, turning it into a Gopher information item.
For your convenience, phd supports *geomyidae* syntax for
creating links:
```
This is an info line.
[1|This is a link|/help|server|port]
[h|URL Link|URL:https://noogle.com]
```
*server* and *port* will get translated into the server and port of
the actively running server, eg *localhost* and *7070*.
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
were a standalone program and its output will be sent to the client.
It will be passed three arguments: the query string (if any), the
server's hostname, and the current port. Do with them what you will.
For example:
```
$ cat echo.gph
#!/bin/sh
echo "Hi, world! You said:" $1
echo "1Visit Gopherpedia / gopherpedia.com 70"
```
Then:
```
$ gopher-client gopher://localhost/1/echo?something
[INFO] Hi, world! You said: something
[LINK] Visit Gopherpedia
```
Or more seriously:
```
$ cat figlet.gph
#!/bin/sh
figlet $1
```
then:
```
$ gopher-client gopher://localhost/1/figlet?hi gopher
[INFO] _ _ _
[INFO] | |__ (_) __ _ ___ _ __ | |__ ___ _ __
[INFO] | '_ \| | / _` |/ _ \| '_ \| '_ \ / _ \ '__|
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
[INFO] |___/ |_|
```
## RESOURCES
geomyidae source code
gopher://bitreich.org/1/scm/geomyidae/files.gph
Example Gophermap
https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
Gophermaps
https://gopher.zone/posts/how-to-gophermap/
RFC 1436:
https://tools.ietf.org/html/rfc1436
# ABOUT
*phd* is maintained by chris west and released under the MIT license.
phd's Gopher hole:
_gopher://phkt.io/1/phd_
phd's webpage:
_https://github.com/xvxx/phd_

@ -1,9 +1,9 @@
/ |
___ (___ ___|
| )| )| )
|__/ | / |__/
|
/ |
___ (___ ___|
| )| )| )
|__/ | / |__/
|
~ browse source ~
(updated nightly)

@ -5,7 +5,24 @@
//! println!("{}Error: {}{}", color::Red, "Something broke.", color::Reset);
//! ```
use std::fmt;
use std::{
fmt,
sync::atomic::{AtomicBool, Ordering as AtomicOrdering},
};
/// Whether to show colors or not.
/// Defaults to true.
static SHOW_COLORS: AtomicBool = AtomicBool::new(true);
/// Hide colors.
pub fn hide_colors() {
SHOW_COLORS.swap(false, AtomicOrdering::Relaxed);
}
/// Are we showing colors are not?
pub fn showing_colors() -> bool {
SHOW_COLORS.load(AtomicOrdering::Relaxed)
}
macro_rules! color {
($t:ident, $code:expr) => {
@ -13,7 +30,11 @@ macro_rules! color {
pub struct $t;
impl fmt::Display for $t {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\x1b[{}m", $code)
if showing_colors() {
write!(f, "\x1b[{}m", $code)
} else {
write!(f, "")
}
}
}
};

@ -0,0 +1,127 @@
//! Gopher type "borrowed" from phetch.
use std::fmt;
/// Gopher types are defined according to RFC 1436.
#[allow(missing_docs)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Type {
Text, // 0
Menu, // 1
CSOEntity, // 2
Error, // 3
Binhex, // 4
DOSFile, // 5
UUEncoded, // 6
Search, // 7
Telnet, // 8
Binary, // 9
Mirror, // +
GIF, // g
Telnet3270, // T
HTML, // h
Image, // I
PNG, // p
Info, // i
Sound, // s
Document, // d
}
impl Type {
/// Is this an info line?
pub fn is_info(self) -> bool {
self == Type::Info
}
/// Text document?
pub fn is_text(self) -> bool {
self == Type::Text
}
/// HTML link?
pub fn is_html(self) -> bool {
self == Type::HTML
}
/// Telnet link?
pub fn is_telnet(self) -> bool {
self == Type::Telnet
}
/// Is this a link, ie something we can navigate to or open?
pub fn is_link(self) -> bool {
!self.is_info()
}
/// Is this something we can download?
pub fn is_download(self) -> bool {
match self {
Type::Binhex
| Type::DOSFile
| Type::UUEncoded
| Type::Binary
| Type::GIF
| Type::Image
| Type::PNG
| Type::Sound
| Type::Document => true,
_ => false,
}
}
/// Gopher Item Type to RFC char.
pub fn to_char(self) -> char {
match self {
Type::Text => '0',
Type::Menu => '1',
Type::CSOEntity => '2',
Type::Error => '3',
Type::Binhex => '4',
Type::DOSFile => '5',
Type::UUEncoded => '6',
Type::Search => '7',
Type::Telnet => '8',
Type::Binary => '9',
Type::Mirror => '+',
Type::GIF => 'g',
Type::Telnet3270 => 'T',
Type::HTML => 'h',
Type::Image => 'I',
Type::PNG => 'p',
Type::Info => 'i',
Type::Sound => 's',
Type::Document => 'd',
}
}
/// Create a Gopher Item Type from its RFC char code.
pub fn from(c: char) -> Option<Type> {
Some(match c {
'0' => Type::Text,
'1' => Type::Menu,
'2' => Type::CSOEntity,
'3' => Type::Error,
'4' => Type::Binhex,
'5' => Type::DOSFile,
'6' => Type::UUEncoded,
'7' => Type::Search,
'8' => Type::Telnet,
'9' => Type::Binary,
'+' => Type::Mirror,
'g' => Type::GIF,
'T' => Type::Telnet3270,
'h' => Type::HTML,
'I' => Type::Image,
'p' => Type::PNG,
'i' => Type::Info,
's' => Type::Sound,
'd' => Type::Document,
_ => return None,
})
}
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_char())
}
}

@ -11,6 +11,7 @@
#![allow(clippy::while_let_on_iterator)]
pub mod color;
pub mod gopher;
pub mod request;
pub mod server;

@ -1,21 +1,38 @@
use phd;
use std::process;
const DEFAULT_BIND: &str = "[::]:7070";
const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7070;
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 iter = args.iter();
let mut addr = DEFAULT_BIND;
let mut host = DEFAULT_HOST;
let mut port = DEFAULT_PORT;
while let Some(arg) = iter.next() {
let mut render = "";
while let Some(arg) = args.next() {
match arg.as_ref() {
"--version" | "-v" | "-version" => return print_version(),
"--help" | "-help" => return print_help(),
"--no-color" | "-no-color" => phd::color::hide_colors(),
"--render" | "-render" | "-r" => {
if let Some(path) = args.next() {
render = path;
} else {
render = "/";
}
}
"--bind" | "-b" | "-bind" => {
if let Some(a) = args.next() {
addr = a
}
}
"--port" | "-p" | "-port" => {
if let Some(p) = iter.next() {
if let Some(p) = args.next() {
port = p
.parse()
.map_err(|_| {
@ -26,15 +43,15 @@ fn main() {
}
}
"-h" => {
if let Some(h) = iter.next() {
host = h;
if let Some(h) = args.next() {
host = &h;
} else {
return print_help();
}
}
"--host" | "-host" => {
if let Some(h) = iter.next() {
host = h;
if let Some(h) = args.next() {
host = &h;
}
}
_ => {
@ -42,13 +59,32 @@ fn main() {
eprintln!("unknown flag: {}", arg);
process::exit(1);
} else {
root = arg;
root = &arg;
}
}
}
}
if let Err(e) = phd::server::start(host, port, root) {
// https://no-color.org/
if std::env::var("NO_COLOR").is_ok() {
phd::color::hide_colors()
}
// If port was given and socket wasn't, bind to that port.
let bind = if port != DEFAULT_PORT && addr == DEFAULT_BIND {
format!("[::]:{}", port).parse().unwrap()
} else {
addr.parse().unwrap()
};
if !render.is_empty() {
return match phd::server::render(host, port, root, &render) {
Ok(out) => print!("{}", out),
Err(e) => eprintln!("{}", e),
};
}
if let Err(e) = phd::server::start(bind, host, port, root) {
eprintln!("{}", e);
}
}
@ -61,15 +97,28 @@ fn print_help() {
Options:
-p, --port Port to bind to. [Default: {port}]
-h, --host Hostname when generating links. [Default: {host}]
-r, --render SELECTOR Render and print SELECTOR to stdout only.
-h, --host HOST Hostname for links. [Default: {host}]
-p, --port PORT Port for links. [Default: {port}]
-b, --bind ADDRESS Socket address to bind to. [Default: {bind}]
--no-color Don't show colors in log messages.
Other flags:
-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,
port = DEFAULT_PORT,
bind = DEFAULT_BIND,
);
}

@ -35,12 +35,11 @@ impl Request {
/// Path to the target file on disk requested by this request.
pub fn file_path(&self) -> String {
let mut path = self.root.to_string();
if !path.ends_with('/') {
path.push('/');
}
path.push_str(self.selector.replace("..", ".").trim_start_matches('/'));
path
format!(
"{}/{}",
self.root.to_string().trim_end_matches('/'),
self.selector.replace("..", ".").trim_start_matches('/')
)
}
/// Path to the target file relative to the server root.
@ -52,7 +51,11 @@ impl Request {
pub fn parse_request(&mut self, line: &str) {
self.query.clear();
self.selector.clear();
if let Some(i) = line.find('\t') {
if let Some((i, _)) = line
.chars()
.enumerate()
.find(|&(_, c)| c == '\t' || c == '?')
{
if line.len() > i {
self.query.push_str(&line[i + 1..]);
self.selector.push_str(&line[..i]);

@ -1,16 +1,16 @@
//! A simple multi-threaded Gopher server.
use crate::{color, Request, Result};
use gophermap::{GopherMenu, ItemType};
use crate::{color, gopher, Request, Result};
use std::{
cmp::Ordering,
fs::{self, DirEntry},
io::{self, prelude::*, BufReader, Read, Write},
net::{TcpListener, TcpStream},
net::{SocketAddr, TcpListener, TcpStream},
os::unix::fs::PermissionsExt,
path::Path,
process::Command,
str,
sync::atomic::{AtomicBool, Ordering as AtomicOrdering},
};
use threadpool::ThreadPool;
@ -24,19 +24,42 @@ const MAX_PEEK_SIZE: usize = 1024;
/// Files not displayed in directory listings.
const IGNORED_FILES: [&str; 3] = ["header.gph", "footer.gph", ".reverse"];
/// Whether to print info!() messages to stdout.
/// Defaults to true.
static SHOW_INFO: AtomicBool = AtomicBool::new(true);
/// Hide info! messages.
fn hide_info() {
SHOW_INFO.swap(false, AtomicOrdering::Relaxed);
}
/// Print status message to the server's stdout.
macro_rules! info {
($e:expr) => {
if SHOW_INFO.load(AtomicOrdering::Relaxed) {
println!("{}", $e);
}
};
($fmt:expr, $($args:expr),*) => {
info!(format!($fmt, $($args),*));
};
($fmt:expr, $($args:expr,)*) => {
info!(format!($fmt, $($args,)*));
};
}
/// Starts a Gopher server at the specified host, port, and root directory.
pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
let addr = format!("{}:{}", "0.0.0.0", port);
let listener = TcpListener::bind(&addr)?;
pub fn start(bind: SocketAddr, host: &str, port: u16, root: &str) -> Result<()> {
let listener = TcpListener::bind(&bind)?;
let full_root_path = fs::canonicalize(&root)?.to_string_lossy().to_string();
let pool = ThreadPool::new(MAX_WORKERS);
println!(
"{}┬ Listening {}on {}{}{} at {}{}{}",
info!(
"{}» Listening {}on {}{}{} at {}{}{}",
color::Yellow,
color::Reset,
color::Yellow,
addr,
bind,
color::Reset,
color::Blue,
full_root_path,
@ -44,7 +67,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
);
for stream in listener.incoming() {
let stream = stream?;
println!(
info!(
"{}┌ Connection{} from {}{}",
color::Green,
color::Reset,
@ -54,7 +77,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
let req = Request::from(host, port, root)?;
pool.execute(move || {
if let Err(e) = accept(stream, req) {
eprintln!("{}└ {}{}", color::Red, e, color::Reset);
info!("{}└ {}{}", color::Red, e, color::Reset);
}
});
}
@ -62,11 +85,11 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
}
/// 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 mut lines = reader.lines();
if let Some(Ok(line)) = lines.next() {
println!(
info!(
"{}│{} Client sent:\t{}{:?}{}",
color::Green,
color::Reset,
@ -75,15 +98,25 @@ fn accept(stream: TcpStream, mut req: Request) -> Result<()> {
color::Reset
);
req.parse_request(&line);
write_response(&stream, req)?;
write_response(&mut stream, req)?;
}
Ok(())
}
/// Render a response to a String.
pub fn render(host: &str, port: u16, root: &str, selector: &str) -> Result<String> {
hide_info();
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.
fn write_response<'a, W>(w: &'a W, mut req: Request) -> Result<()>
fn write_response<W>(w: &mut W, mut req: Request) -> Result<()>
where
&'a W: Write,
W: Write,
{
let path = req.file_path();
@ -121,9 +154,9 @@ where
}
/// 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
&'a W: Write,
W: Write,
{
let path = req.file_path();
if !fs_exists(&path) {
@ -144,12 +177,10 @@ where
)?;
}
let mut menu = GopherMenu::with_write(w);
let rel_path = req.relative_file_path();
// show directory entries
let mut reverse = path.to_string();
reverse.push_str("/.reverse");
let reverse = format!("{}/.reverse", path);
let paths = sort_paths(&path, fs_exists(&reverse))?;
for entry in paths {
let file_name = entry.file_name();
@ -157,11 +188,15 @@ where
if f.chars().nth(0) == Some('.') || IGNORED_FILES.contains(&f.as_ref()) {
continue;
}
let mut path = rel_path.clone();
path.push('/');
path.push_str(&file_name.to_string_lossy());
menu.write_entry(
file_type(&entry),
let path = format!(
"{}/{}",
rel_path.trim_end_matches('/'),
file_name.to_string_lossy()
);
write!(
w,
"{}{}\t{}\t{}\t{}\r\n",
file_type(&entry).to_char(),
&file_name.to_string_lossy(),
&path,
&req.host,
@ -169,11 +204,9 @@ where
)?;
}
let mut footer = path;
footer.push_str("/footer.gph");
let footer = format!("{}/footer.gph", path.trim_end_matches('/'));
if fs_exists(&footer) {
let mut sel = req.selector.clone();
sel.push_str("/footer.gph");
let sel = format!("{}/footer.gph", req.selector);
write_gophermap(
w,
Request {
@ -183,8 +216,9 @@ where
)?;
}
menu.end()?;
println!(
write!(w, ".\r\n");
info!(
"{}│{} Server reply:\t{}DIR {}{}{}",
color::Green,
color::Reset,
@ -197,14 +231,14 @@ where
}
/// 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
&'a W: Write,
W: Write,
{
let path = req.file_path();
let mut f = fs::File::open(&path)?;
io::copy(&mut f, &mut w)?;
println!(
io::copy(&mut f, w)?;
info!(
"{}│{} Server reply:\t{}FILE {}{}{}",
color::Green,
color::Reset,
@ -217,9 +251,9 @@ where
}
/// 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
&'a W: Write,
W: Write,
{
let path = req.file_path();
@ -231,9 +265,9 @@ where
};
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!(
info!(
"{}│{} Server reply:\t{}MAP {}{}{}",
color::Green,
color::Reset,
@ -285,10 +319,8 @@ fn gph_line_to_gopher(line: &str, req: &Request) -> String {
} else {
match line.matches('\t').count() {
0 => {
// Insert `i` prefix to any prefix-less lines without tabs.
if line.chars().nth(0) != Some('i') {
line.insert(0, 'i');
}
// Always insert `i` prefix to any lines without tabs.
line.insert(0, 'i');
line.push_str(&format!("\t(null)\t{}\t{}", req.host, req.port))
}
// Auto-add host and port to lines with just a selector.
@ -301,26 +333,26 @@ fn gph_line_to_gopher(line: &str, req: &Request) -> String {
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
&'a W: Write,
W: Write,
{
let line = format!("3Not Found: {}\t/\tnone\t70\r\n", req.selector);
println!(
info!(
"{}│ Not found: {}{}{}",
color::Red,
color::Cyan,
req.relative_file_path(),
color::Reset,
);
w.write_all(line.as_bytes())?;
write!(w, "{}", line)?;
Ok(())
}
/// Determine the gopher type for a DirEntry on disk.
fn file_type(dir: &fs::DirEntry) -> ItemType {
fn file_type(dir: &fs::DirEntry) -> gopher::Type {
let metadata = match dir.metadata() {
Err(_) => return ItemType::Error,
Err(_) => return gopher::Type::Error,
Ok(md) => md,
};
@ -329,17 +361,17 @@ fn file_type(dir: &fs::DirEntry) -> ItemType {
let mut buffer: Vec<u8> = vec![];
let _ = file.take(MAX_PEEK_SIZE as u64).read_to_end(&mut buffer);
if content_inspector::inspect(&buffer).is_binary() {
ItemType::Binary
gopher::Type::Binary
} else {
ItemType::File
gopher::Type::Text
}
} else {
ItemType::Error
gopher::Type::Error
}
} else if metadata.is_dir() {
ItemType::Directory
gopher::Type::Menu
} else {
ItemType::Error
gopher::Type::Error
}
}
@ -378,7 +410,10 @@ fn sort_paths(dir_path: &str, reverse: bool) -> Result<Vec<DirEntry>> {
let a_is_dir = is_dir(a);
let b_is_dir = is_dir(b);
if a_is_dir && b_is_dir || !a_is_dir && !b_is_dir {
let ord = alphanumeric_sort::compare_os_str(a.path().as_ref(), b.path().as_ref());
let ord = alphanumeric_sort::compare_os_str::<&Path, &Path>(
a.path().as_ref(),
b.path().as_ref(),
);
if reverse {
ord.reverse()
} else {

Loading…
Cancel
Save