mirror of
https://github.com/xvxx/phd
synced 2024-11-16 12:13:06 +00:00
Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5b02ccc1db | ||
|
1e31a05f9d | ||
|
006a3f58a5 | ||
|
6761e1d738 | ||
|
ae866b4609 | ||
|
555504689f | ||
|
a0ab2fa9c5 | ||
|
fb304bce9e | ||
|
be85890d84 | ||
|
2e78294135 | ||
|
0db0c7e149 | ||
|
10ba1ca69d | ||
|
fe7e2e43e6 | ||
|
4c0d707a7a | ||
|
06c0a2d5ea | ||
|
954855fd80 | ||
|
1cb71b3450 | ||
|
4de39201ba | ||
|
4ee7008070 | ||
|
de8607bee8 | ||
|
db0de166e8 | ||
|
19e7579a4f | ||
|
f3ff3fb3eb | ||
|
924c5b1ae9 | ||
|
68979f2d88 | ||
|
a07c921fee | ||
|
f3bfbbd9f5 | ||
|
3d19ac030c | ||
|
e7e0ff9294 | ||
|
dc1f48e917 | ||
|
3b0d9de144 | ||
|
d63f0e866b | ||
|
85865968e3 | ||
|
c0e0aa4548 | ||
|
36cdefd32c | ||
|
03b557312c | ||
|
822e1e0f26 | ||
|
b527304e67 | ||
|
ee2d324a98 | ||
|
73a0016893 | ||
|
63b12145ad | ||
|
4d712e9a05 | ||
|
497185c2e3 | ||
|
4717780bfb | ||
|
ab890ec450 | ||
|
bc30c3a209 | ||
|
2714841316 | ||
|
49ae7abb63 | ||
|
67665c3327 | ||
|
9964d2c530 | ||
|
c94b6c986f | ||
|
a1eb708980 | ||
|
1a102fa9a8 | ||
|
aeaf8d36c4 | ||
|
08e3056f55 | ||
|
1dd632e433 | ||
|
5dc22bf677 | ||
|
ac41b7f191 | ||
|
1c2b065ee7 | ||
|
0241a698c8 | ||
|
3f6c5c43de | ||
|
3f5fed5631 | ||
|
82bbb4edd3 | ||
|
b90325579b | ||
|
5ae4663bf0 | ||
|
fc22f88363 | ||
|
5cc05f1349 | ||
|
87bc9a7a58 | ||
|
eeb342ebfd | ||
|
a1bb9c24d3 | ||
|
525d06383a | ||
|
d295650413 | ||
|
01c327192d | ||
|
f911b4a355 | ||
|
8b825dbd5e | ||
|
bdd127f665 | ||
|
4eebc84c1b | ||
|
99178d51d9 | ||
|
49dab5b83d | ||
|
616a164baf | ||
|
43e596d01b | ||
|
9ee7416403 | ||
|
840c1dd5cc |
2
.cargo/config
Normal file
2
.cargo/config
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
rustflags = ["-C", "link-arg=-s"]
|
73
.github/workflows/build.yml
vendored
Normal file
73
.github/workflows/build.yml
vendored
Normal file
@ -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
|
162
.github/workflows/release.yml
vendored
Normal file
162
.github/workflows/release.yml
vendored
Normal file
@ -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 }}
|
62
CHANGELOG.md
Normal file
62
CHANGELOG.md
Normal file
@ -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.
|
53
Cargo.lock
generated
53
Cargo.lock
generated
@ -1,67 +1,74 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alphanumeric-sort"
|
||||||
|
version = "1.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "content_inspector"
|
name = "content_inspector"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.66"
|
version = "0.2.66"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"hermit-abi",
|
||||||
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phd"
|
name = "phd"
|
||||||
version = "0.1.5"
|
version = "0.1.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"content_inspector 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"alphanumeric-sort",
|
||||||
"gophermap 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"content_inspector",
|
||||||
"threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"shell-escape",
|
||||||
|
"threadpool",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-escape"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "170a13e64f2a51b77a45702ba77287f5c6829375b04a69cf2222acd17d0cfab9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "threadpool"
|
name = "threadpool"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
[metadata]
|
|
||||||
"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 threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
|
|
||||||
|
24
Cargo.toml
24
Cargo.toml
@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "phd"
|
name = "phd"
|
||||||
version = "0.1.5"
|
version = "0.1.15"
|
||||||
authors = ["dvkt <c@dvkt.io>"]
|
authors = ["chris west <c@xvxx.io>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "an esoteric gopher server"
|
description = "an esoteric gopher server"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/dvkt/phd"
|
repository = "https://github.com/xvxx/phd"
|
||||||
keywords = ["gopher", "server", "daemon"]
|
keywords = ["gopher", "server", "daemon"]
|
||||||
exclude = [
|
exclude = [
|
||||||
"img/*"
|
"img/*"
|
||||||
@ -18,15 +18,15 @@ codegen-units = 1
|
|||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
opt-level = 'z' # Optimize for size.
|
opt-level = 'z' # Optimize for size.
|
||||||
|
|
||||||
|
[package.metadata.release]
|
||||||
|
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}}"},
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
content_inspector = "0.2.4"
|
content_inspector = "0.2.4"
|
||||||
threadpool = "1.7.1"
|
threadpool = "1.7.1"
|
||||||
gophermap = "0.1.2"
|
alphanumeric-sort = "1.4"
|
||||||
|
shell-escape = "0.1.4"
|
||||||
[package.metadata.release]
|
|
||||||
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}}/"},
|
|
||||||
]
|
|
||||||
dev-version-ext = "dev"
|
|
||||||
|
|
||||||
|
15
Makefile
15
Makefile
@ -27,11 +27,22 @@ uninstall: $(RELEASE)
|
|||||||
clean:
|
clean:
|
||||||
-rm -rf target
|
-rm -rf target
|
||||||
|
|
||||||
# Build and strip the release version
|
# Build the release version
|
||||||
$(RELEASE): $(SOURCES)
|
$(RELEASE): $(SOURCES)
|
||||||
cargo build --release
|
cargo build --release
|
||||||
strip $@
|
|
||||||
|
|
||||||
# Build the debug version
|
# Build the debug version
|
||||||
$(DEBUG): $(SOURCES)
|
$(DEBUG): $(SOURCES)
|
||||||
cargo build
|
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)
|
||||||
|
175
README.md
175
README.md
@ -4,50 +4,82 @@
|
|||||||
| )| )| )
|
| )| )| )
|
||||||
|__/ | / |__/
|
|__/ | / |__/
|
||||||
|
|
|
|
||||||
--> <p align="center"><img src="./img/logo.png"></p>
|
-->
|
||||||
|
<p align="center">
|
||||||
|
<img src="./img/logo.png"> <br>
|
||||||
|
|
||||||
`phd` is an esoteric gopher server for small gopherholes.
|
<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>
|
||||||
|
|
||||||
point it at a directory and it'll serve up all its 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
|
|
||||||
run as a script with their output served to the client, like cgi!
|
|
||||||
|
|
||||||
special files:
|
`phd` is a small, easy-to-use gopher server.
|
||||||
|
|
||||||
- **header.gph**: if it exists in a directory, its content will be
|
Point it at a directory and it'll serve up all the text files,
|
||||||
shown above the directory's content. put ascii art in it.
|
sub-directories, and binary files over Gopher. Any `.gph` files will
|
||||||
- **footer.gph**: same, but will be shown below a directory's content.
|
be served up as [gophermaps][map] and executable `.gph` files will be
|
||||||
- **index.gph**: completely replaces a directory's content with what's
|
run as a program with their output served to the client, like the
|
||||||
|
glorious cgi-bin days of yore!
|
||||||
|
|
||||||
|
### ~ special files ~
|
||||||
|
|
||||||
|
- **`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.
|
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.
|
`dog.gph` from disk. Visiting `/1/dog.gph` will render the raw
|
||||||
- **.reverse**: if this exists, the directory contents will be listed
|
content of the .gph file.
|
||||||
in reverse alphanumeric order. useful for phloggin'.
|
- **`.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
|
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.
|
|
||||||
|
|
||||||
any `.gph` file that is marked **executable** with be run as if it
|
For your convenience, phd supports **[geomyidae][gmi]** syntax for
|
||||||
were a shell script and its output will be sent to the client. it will
|
creating links:
|
||||||
be passed three arguments: the query string (if any, the host, and the
|
|
||||||
port. do with them what you will.
|
|
||||||
|
|
||||||
for example:
|
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
|
$ cat echo.gph
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
echo "Hi, world! You said:" $1
|
echo "Hi, world! You said:" $1
|
||||||
echo "1Visit Gopherpedia / gopherpedia.com 70"
|
echo "1Visit Gopherpedia / gopherpedia.com 70"
|
||||||
|
|
||||||
then:
|
Then:
|
||||||
|
|
||||||
$ gopher-client gopher://localhost/1/echo?something
|
$ gopher-client gopher://localhost/1/echo?something
|
||||||
[INFO] Hi, world! You said: something
|
[INFO] Hi, world! You said: something
|
||||||
[LINK] Visit Gopherpedia
|
[LINK] Visit Gopherpedia
|
||||||
|
|
||||||
or more seriously:
|
Or more seriously:
|
||||||
|
|
||||||
$ cat figlet.gph
|
$ cat figlet.gph
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
@ -61,10 +93,44 @@ then:
|
|||||||
[INFO] | '_ \| | / _` |/ _ \| '_ \| '_ \ / _ \ '__|
|
[INFO] | '_ \| | / _` |/ _ \| '_ \| '_ \ / _ \ '__|
|
||||||
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
|
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
|
||||||
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
|
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
|
||||||
[INFO] |___/ |_|
|
[INFO] |___/ |_|
|
||||||
|
|
||||||
|
### ~ ruby on rails ~
|
||||||
|
|
||||||
## usage
|
`sh` is fun, but for serious work you need a serious scripting
|
||||||
|
language like Ruby or PHP or Node.JS:
|
||||||
|
|
||||||
|
$ cat sizes.gph
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
def filesize(file)
|
||||||
|
(size=File.size file) > (k=1024) ? "#{size/k}K" : "#{size}B"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "~ file sizes ~"
|
||||||
|
spaces = 20
|
||||||
|
Dir[__dir__ + "/*"].each do |entry|
|
||||||
|
name = File.basename entry
|
||||||
|
puts "#{name}#{' ' * (spaces - name.length)}#{filesize entry}"
|
||||||
|
end
|
||||||
|
|
||||||
|
Now you can finally share the file sizes of a directory with the world
|
||||||
|
of Gopher!
|
||||||
|
|
||||||
|
$ phetch -r 0.0.0.0:7070/1/sizes
|
||||||
|
i~ file sizes ~ (null) 127.0.0.1 7070
|
||||||
|
iCargo.toml 731B (null) 127.0.0.1 7070
|
||||||
|
iLICENSE 1K (null) 127.0.0.1 7070
|
||||||
|
iMakefile 724B (null) 127.0.0.1 7070
|
||||||
|
itarget 288B (null) 127.0.0.1 7070
|
||||||
|
iphd 248K (null) 127.0.0.1 7070
|
||||||
|
iCargo.lock 2K (null) 127.0.0.1 7070
|
||||||
|
iREADME.md 4K (null) 127.0.0.1 7070
|
||||||
|
img 96B (null) 127.0.0.1 7070
|
||||||
|
isizes.gph 276B (null) 127.0.0.1 7070
|
||||||
|
isrc 224B (null) 127.0.0.1 7070
|
||||||
|
|
||||||
|
## ~ usage ~
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
@ -72,8 +138,11 @@ then:
|
|||||||
|
|
||||||
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.
|
-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:
|
Other flags:
|
||||||
|
|
||||||
@ -85,38 +154,52 @@ then:
|
|||||||
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 ~
|
||||||
|
|
||||||
binaries for linux, mac, and raspberry pi are available
|
On macOS you can install with [Homebrew](https://brew.sh/):
|
||||||
at https://github.com/dvkt/phd/releases:
|
|
||||||
|
|
||||||
- [phd-v0.1.5-linux-x86_64.tar.gz][0]
|
brew install xvxx/code/phd
|
||||||
- [phd-v0.1.5-linux-armv7.tar.gz (RPi)][1]
|
|
||||||
- [phd-v0.1.5-macos.zip][2]
|
|
||||||
|
|
||||||
just unzip/untar the `phd` program into your $PATH and get going!
|
Binaries for Linux, Mac, and Raspberry Pi are available at
|
||||||
|
gopher://phkt.io/1/releases/phd and https://github.com/xvxx/phd/releases:
|
||||||
|
|
||||||
## development
|
- [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:
|
||||||
|
|
||||||
|
cargo install phd --locked
|
||||||
|
|
||||||
|
## ~ development ~
|
||||||
|
|
||||||
cargo run -- ./path/to/gopher/site
|
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://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
|
- [ ] user input sanitization tests
|
||||||
- [ ] ipv6
|
|
||||||
|
|
||||||
[0]: https://github.com/dvkt/phd/releases/download/v0.1.5/phd-v0.1.5-linux-x86_64.tar.gz
|
## ~ status ~
|
||||||
[1]: https://github.com/dvkt/phd/releases/download/v0.1.5/phd-v0.1.5-linux-armv7.tar.gz
|
|
||||||
[2]: https://github.com/dvkt/phd/releases/download/v0.1.5/phd-v0.1.5-macos.zip
|
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
|
[map]: https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu
|
||||||
|
[gmi]: http://r-36.net/scm/geomyidae/
|
||||||
|
[rustup]: https://rustup.rs
|
||||||
|
206
doc/phd.1
Normal file
206
doc/phd.1
Normal file
@ -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
|
154
doc/phd.1.md
Normal file
154
doc/phd.1.md
Normal file
@ -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_
|
10
header.gph
Normal file
10
header.gph
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
/ |
|
||||||
|
___ (___ ___|
|
||||||
|
| )| )| )
|
||||||
|
|__/ | / |__/
|
||||||
|
|
|
||||||
|
|
||||||
|
~ browse source ~
|
||||||
|
(updated nightly)
|
||||||
|
|
33
src/color.rs
33
src/color.rs
@ -1,11 +1,40 @@
|
|||||||
use std::fmt;
|
//! Cheesy way to easily wrap text in console colors.
|
||||||
|
//! Example:
|
||||||
|
//! ```
|
||||||
|
//! use phd::color;
|
||||||
|
//! println!("{}Error: {}{}", color::Red, "Something broke.", color::Reset);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
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 {
|
macro_rules! color {
|
||||||
($t:ident, $code:expr) => {
|
($t:ident, $code:expr) => {
|
||||||
|
#[allow(missing_docs)]
|
||||||
pub struct $t;
|
pub struct $t;
|
||||||
impl fmt::Display for $t {
|
impl fmt::Display for $t {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
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, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
127
src/gopher.rs
Normal file
127
src/gopher.rs
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
}
|
14
src/lib.rs
14
src/lib.rs
@ -1,7 +1,21 @@
|
|||||||
|
//! phd is a small, easy-to-use Gopher server that tries to make
|
||||||
|
//! serving up a Gopher site quick and painless. Best used for local
|
||||||
|
//! development or low traffic Gopher sites.
|
||||||
|
|
||||||
|
#![allow(unused_must_use)]
|
||||||
|
#![warn(absolute_paths_not_starting_with_crate)]
|
||||||
|
#![warn(explicit_outlives_requirements)]
|
||||||
|
#![warn(unreachable_pub)]
|
||||||
|
#![warn(deprecated_in_future)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![allow(clippy::while_let_on_iterator)]
|
||||||
|
|
||||||
pub mod color;
|
pub mod color;
|
||||||
|
pub mod gopher;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
pub use crate::request::Request;
|
pub use crate::request::Request;
|
||||||
|
|
||||||
|
/// Alias for a generic Result type.
|
||||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
75
src/main.rs
75
src/main.rs
@ -1,21 +1,38 @@
|
|||||||
use phd;
|
use phd;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
|
const DEFAULT_BIND: &str = "[::]:7070";
|
||||||
const DEFAULT_HOST: &str = "127.0.0.1";
|
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 addr = DEFAULT_BIND;
|
||||||
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(),
|
||||||
|
"--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" => {
|
"--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 +43,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,13 +59,32 @@ fn main() {
|
|||||||
eprintln!("unknown flag: {}", arg);
|
eprintln!("unknown flag: {}", arg);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
} else {
|
} 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);
|
eprintln!("{}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,15 +97,28 @@ 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}]
|
-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:
|
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,
|
||||||
|
bind = DEFAULT_BIND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
|
//! A Request represents a Gopher request made by a client. phd can
|
||||||
|
//! serve directory listings as Gopher Menus, plain text files as
|
||||||
|
//! Text, binary files as downloads, Gophermap files as menus, or
|
||||||
|
//! executable files as dynamic content.
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
/// This struct represents a single gopher request.
|
/// This struct represents a single gopher request.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
|
/// Gopher selector requested
|
||||||
pub selector: String,
|
pub selector: String,
|
||||||
|
/// Search query string, if any.
|
||||||
pub query: String,
|
pub query: String,
|
||||||
|
/// Root directory of the server. Can't serve outside of this.
|
||||||
pub root: String,
|
pub root: String,
|
||||||
|
/// Host of the currently running server.
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
/// Port of the currently running server.
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,7 +26,7 @@ impl Request {
|
|||||||
pub fn from(host: &str, port: u16, root: &str) -> Result<Request> {
|
pub fn from(host: &str, port: u16, root: &str) -> Result<Request> {
|
||||||
Ok(Request {
|
Ok(Request {
|
||||||
host: host.into(),
|
host: host.into(),
|
||||||
port: port,
|
port,
|
||||||
root: fs::canonicalize(root)?.to_string_lossy().into(),
|
root: fs::canonicalize(root)?.to_string_lossy().into(),
|
||||||
selector: String::new(),
|
selector: String::new(),
|
||||||
query: String::new(),
|
query: String::new(),
|
||||||
@ -25,12 +35,11 @@ impl Request {
|
|||||||
|
|
||||||
/// Path to the target file on disk requested by this request.
|
/// Path to the target file on disk requested by this request.
|
||||||
pub fn file_path(&self) -> String {
|
pub fn file_path(&self) -> String {
|
||||||
let mut path = self.root.to_string();
|
format!(
|
||||||
if !path.ends_with('/') {
|
"{}/{}",
|
||||||
path.push('/');
|
self.root.to_string().trim_end_matches('/'),
|
||||||
}
|
self.selector.replace("..", ".").trim_start_matches('/')
|
||||||
path.push_str(self.selector.replace("..", ".").trim_start_matches('/'));
|
)
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the target file relative to the server root.
|
/// Path to the target file relative to the server root.
|
||||||
@ -42,8 +51,12 @@ impl Request {
|
|||||||
pub fn parse_request(&mut self, line: &str) {
|
pub fn parse_request(&mut self, line: &str) {
|
||||||
self.query.clear();
|
self.query.clear();
|
||||||
self.selector.clear();
|
self.selector.clear();
|
||||||
if let Some(i) = line.find('\t') {
|
if let Some((i, _)) = line
|
||||||
if line.len() >= i + 1 {
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.find(|&(_, c)| c == '\t' || c == '?')
|
||||||
|
{
|
||||||
|
if line.len() > i {
|
||||||
self.query.push_str(&line[i + 1..]);
|
self.query.push_str(&line[i + 1..]);
|
||||||
self.selector.push_str(&line[..i]);
|
self.selector.push_str(&line[..i]);
|
||||||
return;
|
return;
|
||||||
|
349
src/server.rs
349
src/server.rs
@ -1,13 +1,16 @@
|
|||||||
use crate::{color, Request, Result};
|
//! A simple multi-threaded Gopher server.
|
||||||
use gophermap::{GopherMenu, ItemType};
|
|
||||||
|
use crate::{color, gopher, Request, Result};
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
cmp::Ordering,
|
||||||
|
fs::{self, DirEntry},
|
||||||
io::{self, prelude::*, BufReader, Read, Write},
|
io::{self, prelude::*, BufReader, Read, Write},
|
||||||
net::{TcpListener, TcpStream},
|
net::{SocketAddr, TcpListener, TcpStream},
|
||||||
os::unix::fs::PermissionsExt,
|
os::unix::fs::PermissionsExt,
|
||||||
path::Path,
|
path::Path,
|
||||||
process::Command,
|
process::Command,
|
||||||
str,
|
str,
|
||||||
|
sync::atomic::{AtomicBool, Ordering as AtomicOrdering},
|
||||||
};
|
};
|
||||||
use threadpool::ThreadPool;
|
use threadpool::ThreadPool;
|
||||||
|
|
||||||
@ -21,19 +24,42 @@ const MAX_PEEK_SIZE: usize = 1024;
|
|||||||
/// Files not displayed in directory listings.
|
/// Files not displayed in directory listings.
|
||||||
const IGNORED_FILES: [&str; 3] = ["header.gph", "footer.gph", ".reverse"];
|
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.
|
/// Starts a Gopher server at the specified host, port, and root directory.
|
||||||
pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
|
pub fn start(bind: SocketAddr, host: &str, port: u16, root: &str) -> Result<()> {
|
||||||
let addr = format!("{}:{}", "0.0.0.0", port);
|
let listener = TcpListener::bind(&bind)?;
|
||||||
let listener = TcpListener::bind(&addr)?;
|
|
||||||
let full_root_path = fs::canonicalize(&root)?.to_string_lossy().to_string();
|
let full_root_path = fs::canonicalize(&root)?.to_string_lossy().to_string();
|
||||||
let pool = ThreadPool::new(MAX_WORKERS);
|
let pool = ThreadPool::new(MAX_WORKERS);
|
||||||
|
|
||||||
println!(
|
info!(
|
||||||
"{}┬ Listening {}on {}{}{} at {}{}{}",
|
"{}» Listening {}on {}{}{} at {}{}{}",
|
||||||
color::Yellow,
|
color::Yellow,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
color::Yellow,
|
color::Yellow,
|
||||||
addr,
|
bind,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
color::Blue,
|
color::Blue,
|
||||||
full_root_path,
|
full_root_path,
|
||||||
@ -41,7 +67,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
|
|||||||
);
|
);
|
||||||
for stream in listener.incoming() {
|
for stream in listener.incoming() {
|
||||||
let stream = stream?;
|
let stream = stream?;
|
||||||
println!(
|
info!(
|
||||||
"{}┌ Connection{} from {}{}",
|
"{}┌ Connection{} from {}{}",
|
||||||
color::Green,
|
color::Green,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
@ -51,7 +77,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
|
|||||||
let req = Request::from(host, port, root)?;
|
let req = Request::from(host, port, root)?;
|
||||||
pool.execute(move || {
|
pool.execute(move || {
|
||||||
if let Err(e) = accept(stream, req) {
|
if let Err(e) = accept(stream, req) {
|
||||||
eprintln!("{}└ {}{}", color::Red, e, color::Reset);
|
info!("{}└ {}{}", color::Red, e, color::Reset);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -59,11 +85,11 @@ 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() {
|
||||||
println!(
|
info!(
|
||||||
"{}│{} Client sent:\t{}{:?}{}",
|
"{}│{} Client sent:\t{}{:?}{}",
|
||||||
color::Green,
|
color::Green,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
@ -72,15 +98,25 @@ 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> {
|
||||||
|
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.
|
/// 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();
|
||||||
|
|
||||||
@ -118,9 +154,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) {
|
||||||
@ -141,34 +177,26 @@ where
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut menu = GopherMenu::with_write(w);
|
|
||||||
let rel_path = req.relative_file_path();
|
let rel_path = req.relative_file_path();
|
||||||
|
|
||||||
// sort directory entries
|
// show directory entries
|
||||||
let mut paths: Vec<_> = fs::read_dir(&path)?.filter_map(|r| r.ok()).collect();
|
let reverse = format!("{}/.reverse", path);
|
||||||
let mut reverse = path.clone();
|
let paths = sort_paths(&path, fs_exists(&reverse))?;
|
||||||
reverse.push_str("/.reverse");
|
|
||||||
let is_dir = |entry: &fs::DirEntry| match entry.file_type() {
|
|
||||||
Ok(t) => t.is_dir(),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if fs_exists(&reverse) {
|
|
||||||
paths.sort_by_key(|entry| (!is_dir(&entry), std::cmp::Reverse(entry.path())));
|
|
||||||
} else {
|
|
||||||
paths.sort_by_key(|entry| (!is_dir(&entry), entry.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in paths {
|
for entry in paths {
|
||||||
let file_name = entry.file_name();
|
let file_name = entry.file_name();
|
||||||
let f = file_name.to_string_lossy().to_string();
|
let f = file_name.to_string_lossy().to_string();
|
||||||
if f.chars().nth(0) == Some('.') || IGNORED_FILES.contains(&f.as_ref()) {
|
if f.chars().nth(0) == Some('.') || IGNORED_FILES.contains(&f.as_ref()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut path = rel_path.clone();
|
let path = format!(
|
||||||
path.push('/');
|
"{}/{}",
|
||||||
path.push_str(&file_name.to_string_lossy());
|
rel_path.trim_end_matches('/'),
|
||||||
menu.write_entry(
|
file_name.to_string_lossy()
|
||||||
file_type(&entry),
|
);
|
||||||
|
write!(
|
||||||
|
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,
|
||||||
@ -176,11 +204,9 @@ where
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut footer = path.clone();
|
let footer = format!("{}/footer.gph", path.trim_end_matches('/'));
|
||||||
footer.push_str("/footer.gph");
|
|
||||||
if fs_exists(&footer) {
|
if fs_exists(&footer) {
|
||||||
let mut sel = req.selector.clone();
|
let sel = format!("{}/footer.gph", req.selector);
|
||||||
sel.push_str("/footer.gph");
|
|
||||||
write_gophermap(
|
write_gophermap(
|
||||||
w,
|
w,
|
||||||
Request {
|
Request {
|
||||||
@ -190,8 +216,9 @@ where
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.end()?;
|
write!(w, ".\r\n");
|
||||||
println!(
|
|
||||||
|
info!(
|
||||||
"{}│{} Server reply:\t{}DIR {}{}{}",
|
"{}│{} Server reply:\t{}DIR {}{}{}",
|
||||||
color::Green,
|
color::Green,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
@ -204,14 +231,14 @@ 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!(
|
info!(
|
||||||
"{}│{} Server reply:\t{}FILE {}{}{}",
|
"{}│{} Server reply:\t{}FILE {}{}{}",
|
||||||
color::Green,
|
color::Green,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
@ -224,9 +251,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();
|
||||||
|
|
||||||
@ -238,24 +265,9 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
let mut line = line.trim_end_matches("\r").to_string();
|
write!(w, "{}", gph_line_to_gopher(line, &req))?;
|
||||||
match line.chars().filter(|&c| c == '\t').count() {
|
|
||||||
0 => {
|
|
||||||
// Insert `i` prefix to any prefix-less lines without tabs.
|
|
||||||
if line.chars().nth(0) != Some('i') {
|
|
||||||
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.
|
|
||||||
1 => line.push_str(&format!("\t{}\t{}", req.host, req.port)),
|
|
||||||
2 => line.push_str(&format!("\t{}", req.port)),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
line.push_str("\r\n");
|
|
||||||
w.write_all(line.as_bytes())?;
|
|
||||||
}
|
}
|
||||||
println!(
|
info!(
|
||||||
"{}│{} Server reply:\t{}MAP {}{}{}",
|
"{}│{} Server reply:\t{}MAP {}{}{}",
|
||||||
color::Green,
|
color::Green,
|
||||||
color::Reset,
|
color::Reset,
|
||||||
@ -267,26 +279,80 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_not_found<'a, W>(mut w: &'a W, req: Request) -> Result<()>
|
/// Given a single line from a .gph file, convert it into a
|
||||||
|
/// Gopher-format line. Supports a basic format where lines without \t
|
||||||
|
/// get an `i` prefixed, and the geomyidae format.
|
||||||
|
fn gph_line_to_gopher(line: &str, req: &Request) -> String {
|
||||||
|
if line.starts_with('#') {
|
||||||
|
return "".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line = line.trim_end_matches('\r').to_string();
|
||||||
|
if line.starts_with('[') && line.ends_with(']') && line.contains('|') {
|
||||||
|
// [1|name|sel|server|port]
|
||||||
|
let port = req.port.to_string();
|
||||||
|
line = line
|
||||||
|
.replacen('|', "", 1)
|
||||||
|
.trim_start_matches('[')
|
||||||
|
.trim_end_matches(']')
|
||||||
|
.replace("\\|", "__P_ESC_PIPE") // cheap hack
|
||||||
|
.replace('|', "\t")
|
||||||
|
.replace("__P_ESC_PIPE", "\\|")
|
||||||
|
.replace("\tserver\t", format!("\t{}\t", req.host).as_ref())
|
||||||
|
.replace("\tport", format!("\t{}", port).as_ref());
|
||||||
|
let tabs = line.matches('\t').count();
|
||||||
|
if tabs < 1 {
|
||||||
|
line.push('\t');
|
||||||
|
line.push_str("(null)");
|
||||||
|
}
|
||||||
|
// if a link is missing host + port, assume it's this server.
|
||||||
|
// if it's just missing the port, assume port 70
|
||||||
|
if tabs < 2 {
|
||||||
|
line.push('\t');
|
||||||
|
line.push_str(&req.host);
|
||||||
|
line.push('\t');
|
||||||
|
line.push_str(&port);
|
||||||
|
} else if tabs < 3 {
|
||||||
|
line.push('\t');
|
||||||
|
line.push_str("70");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match line.matches('\t').count() {
|
||||||
|
0 => {
|
||||||
|
// 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.
|
||||||
|
1 => line.push_str(&format!("\t{}\t{}", req.host, req.port)),
|
||||||
|
2 => line.push_str(&format!("\t{}", req.port)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.push_str("\r\n");
|
||||||
|
line
|
||||||
|
}
|
||||||
|
|
||||||
|
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!(
|
info!(
|
||||||
"{}│ Not found: {}{}{}",
|
"{}│ Not found: {}{}{}",
|
||||||
color::Red,
|
color::Red,
|
||||||
color::Cyan,
|
color::Cyan,
|
||||||
req.relative_file_path(),
|
req.relative_file_path(),
|
||||||
color::Reset,
|
color::Reset,
|
||||||
);
|
);
|
||||||
w.write_all(line.as_bytes())?;
|
write!(w, "{}", line)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine the gopher type for a DirEntry on disk.
|
/// 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() {
|
let metadata = match dir.metadata() {
|
||||||
Err(_) => return ItemType::Error,
|
Err(_) => return gopher::Type::Error,
|
||||||
Ok(md) => md,
|
Ok(md) => md,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -295,17 +361,17 @@ fn file_type(dir: &fs::DirEntry) -> ItemType {
|
|||||||
let mut buffer: Vec<u8> = vec![];
|
let mut buffer: Vec<u8> = vec![];
|
||||||
let _ = file.take(MAX_PEEK_SIZE as u64).read_to_end(&mut buffer);
|
let _ = file.take(MAX_PEEK_SIZE as u64).read_to_end(&mut buffer);
|
||||||
if content_inspector::inspect(&buffer).is_binary() {
|
if content_inspector::inspect(&buffer).is_binary() {
|
||||||
ItemType::Binary
|
gopher::Type::Binary
|
||||||
} else {
|
} else {
|
||||||
ItemType::File
|
gopher::Type::Text
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ItemType::Error
|
gopher::Type::Error
|
||||||
}
|
}
|
||||||
} else if metadata.is_dir() {
|
} else if metadata.is_dir() {
|
||||||
ItemType::Directory
|
gopher::Type::Menu
|
||||||
} else {
|
} else {
|
||||||
ItemType::Error
|
gopher::Type::Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,3 +398,128 @@ fn shell(path: &str, args: &[&str]) -> Result<String> {
|
|||||||
Ok(str::from_utf8(&output.stderr)?.to_string())
|
Ok(str::from_utf8(&output.stderr)?.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort directory paths: dirs first, files 2nd, version #s respected.
|
||||||
|
fn sort_paths(dir_path: &str, reverse: bool) -> Result<Vec<DirEntry>> {
|
||||||
|
let mut paths: Vec<_> = fs::read_dir(dir_path)?.filter_map(|r| r.ok()).collect();
|
||||||
|
let is_dir = |entry: &fs::DirEntry| match entry.file_type() {
|
||||||
|
Ok(t) => t.is_dir(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
paths.sort_by(|a, b| {
|
||||||
|
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::<&Path, &Path>(
|
||||||
|
a.path().as_ref(),
|
||||||
|
b.path().as_ref(),
|
||||||
|
);
|
||||||
|
if reverse {
|
||||||
|
ord.reverse()
|
||||||
|
} else {
|
||||||
|
ord
|
||||||
|
}
|
||||||
|
} else if is_dir(a) {
|
||||||
|
Ordering::Less
|
||||||
|
} else if is_dir(b) {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
Ordering::Equal // what
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! str_path {
|
||||||
|
($e:expr) => {
|
||||||
|
$e.path()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.trim_start_matches("tests/sort/")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_directory() {
|
||||||
|
let paths = sort_paths("tests/sort", false).unwrap();
|
||||||
|
assert_eq!(str_path!(paths[0]), "zzz");
|
||||||
|
assert_eq!(str_path!(paths[1]), "phetch-v0.1.7-linux-armv7.tar.gz");
|
||||||
|
assert_eq!(
|
||||||
|
str_path!(paths[paths.len() - 1]),
|
||||||
|
"phetch-v0.1.11-macos.zip"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rsort_directory() {
|
||||||
|
let paths = sort_paths("tests/sort", true).unwrap();
|
||||||
|
assert_eq!(str_path!(paths[0]), "zzz");
|
||||||
|
assert_eq!(str_path!(paths[1]), "phetch-v0.1.11-macos.zip");
|
||||||
|
assert_eq!(
|
||||||
|
str_path!(paths[paths.len() - 1]),
|
||||||
|
"phetch-v0.1.7-linux-armv7.tar.gz"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gph_line_to_gopher() {
|
||||||
|
let req = Request::from("localhost", 70, ".").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("regular line test", &req),
|
||||||
|
"iregular line test (null) localhost 70\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("1link test /test localhost 70", &req),
|
||||||
|
"1link test /test localhost 70\r\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let line = "0short link test /test";
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher(line, &req),
|
||||||
|
"0short link test /test localhost 70\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gph_geomyidae() {
|
||||||
|
let req = Request::from("localhost", 7070, ".").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("[1|phkt.io|/|phkt.io]", &req),
|
||||||
|
"1phkt.io / phkt.io 70\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(gph_line_to_gopher("#[1|phkt.io|/|phkt.io]", &req), "");
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("[1|sdf6000|/not-real|sdf.org|6000]", &req),
|
||||||
|
"1sdf6000 /not-real sdf.org 6000\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("[1|R-36|/]", &req),
|
||||||
|
"1R-36 / localhost 7070\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("[1|R-36|/|server|port]", &req),
|
||||||
|
"1R-36 / localhost 7070\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("[0|file - comment|/file.dat|server|port]", &req),
|
||||||
|
"0file - comment /file.dat localhost 7070\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher(
|
||||||
|
"[0|some \\| escape and [ special characters ] test|error|server|port]",
|
||||||
|
&req
|
||||||
|
),
|
||||||
|
"0some \\| escape and [ special characters ] test error localhost 7070\r\n"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
gph_line_to_gopher("[|empty type||server|port]", &req),
|
||||||
|
"empty type\t\tlocalhost\t7070\r\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
tests/sort/phetch-v0.1.10-linux-armv7.tgz
Normal file
0
tests/sort/phetch-v0.1.10-linux-armv7.tgz
Normal file
0
tests/sort/phetch-v0.1.10-linux-x86_64.tgz
Normal file
0
tests/sort/phetch-v0.1.10-linux-x86_64.tgz
Normal file
0
tests/sort/phetch-v0.1.10-macos.zip
Normal file
0
tests/sort/phetch-v0.1.10-macos.zip
Normal file
0
tests/sort/phetch-v0.1.11-linux-armv7.tgz
Normal file
0
tests/sort/phetch-v0.1.11-linux-armv7.tgz
Normal file
0
tests/sort/phetch-v0.1.11-linux-x86_64.tgz
Normal file
0
tests/sort/phetch-v0.1.11-linux-x86_64.tgz
Normal file
0
tests/sort/phetch-v0.1.11-macos.zip
Normal file
0
tests/sort/phetch-v0.1.11-macos.zip
Normal file
0
tests/sort/phetch-v0.1.7-linux-armv7.tar.gz
Normal file
0
tests/sort/phetch-v0.1.7-linux-armv7.tar.gz
Normal file
0
tests/sort/phetch-v0.1.7-linux-x86_64.tar.gz
Normal file
0
tests/sort/phetch-v0.1.7-linux-x86_64.tar.gz
Normal file
0
tests/sort/phetch-v0.1.7-macos.zip
Normal file
0
tests/sort/phetch-v0.1.7-macos.zip
Normal file
0
tests/sort/phetch-v0.1.8-linux-armv7.tar.gz
Normal file
0
tests/sort/phetch-v0.1.8-linux-armv7.tar.gz
Normal file
0
tests/sort/phetch-v0.1.8-linux-x86_64.tar.gz
Normal file
0
tests/sort/phetch-v0.1.8-linux-x86_64.tar.gz
Normal file
0
tests/sort/phetch-v0.1.8-macos.zip
Normal file
0
tests/sort/phetch-v0.1.8-macos.zip
Normal file
0
tests/sort/phetch-v0.1.9-linux-armv7.tar.gz
Normal file
0
tests/sort/phetch-v0.1.9-linux-armv7.tar.gz
Normal file
0
tests/sort/phetch-v0.1.9-linux-x86_64.tar.gz
Normal file
0
tests/sort/phetch-v0.1.9-linux-x86_64.tar.gz
Normal file
0
tests/sort/phetch-v0.1.9-macos.zip
Normal file
0
tests/sort/phetch-v0.1.9-macos.zip
Normal file
0
tests/sort/zzz/.gitignore
vendored
Normal file
0
tests/sort/zzz/.gitignore
vendored
Normal file
Loading…
Reference in New Issue
Block a user