diff --git a/.gitignore b/.gitignore index 0072e7f..20a6192 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *~ # Go binaries +web/fileviewer/fileviewer web/proxy/proxy web/server/server diff --git a/WORKSPACE b/WORKSPACE index 2f4f0bd..981fd0b 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -56,6 +56,13 @@ go_repository( version = "v2.4.0", ) +go_repository( + name = "com_github_gomarkdown_markdown", + importpath = "github.com/gomarkdown/markdown", + sum = "h1:7dT6mSWxX6R/7sB6FDSade73Q6BVL834Y1wJR/db+5o=", + version = "v0.0.0-20230714230225-84ecad09a30a", +) + go_rules_dependencies() go_register_toolchains(version = "host") diff --git a/go.mod b/go.mod index 8f96ee5..1c8284e 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.12 require ( github.com/ghodss/yaml v1.0.0 + github.com/gomarkdown/markdown v0.0.0-20230714230225-84ecad09a30a gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0f65cfd..e107581 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gomarkdown/markdown v0.0.0-20230714230225-84ecad09a30a h1:7dT6mSWxX6R/7sB6FDSade73Q6BVL834Y1wJR/db+5o= +github.com/gomarkdown/markdown v0.0.0-20230714230225-84ecad09a30a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/web/fileviewer/BUILD.bazel b/web/fileviewer/BUILD.bazel new file mode 100644 index 0000000..18d8cef --- /dev/null +++ b/web/fileviewer/BUILD.bazel @@ -0,0 +1,29 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "fileviewer_lib", + srcs = ["fileviewer.go"], + importpath = "github.com/mbrukman/notebook/web/fileviewer", + visibility = ["//visibility:private"], + deps = ["@com_github_gomarkdown_markdown//:go_default_library"], +) + +go_binary( + name = "fileviewer", + embed = [":fileviewer_lib"], + visibility = ["//visibility:public"], +) diff --git a/web/fileviewer/README.md b/web/fileviewer/README.md new file mode 100644 index 0000000..972abfd --- /dev/null +++ b/web/fileviewer/README.md @@ -0,0 +1,64 @@ +# File viewer + +This is a simple web server which renders text files (including HTML, JS, CSS) +and Markdown files (dynamically rendered to HTML). Some use cases include +quickly browsing local directories via a web browser, including Markdown files +with relative links. + +This is an early prototype, but you're welcome to play around with it and +provide feedback, potential new use cases, etc. + +## Building + +Build in the current directory: + +```sh +$ go build . +``` + +Or you can build it from the top of the tree in a local checkout: + +```sh +$ go build ./web/filevewer +``` + +Or you can build via Bazel: + +```sh +$ bazel build //web/fileviewer +``` + +Or you can build it without having this repo locally: + +```sh +$ go install github.com/mbrukman/notebook/web/fileviewer@latest +``` + +# Running + +Run locally with a custom web root (only accessible from `localhost` by +default): + +```sh +$ ./fileviewer -web-root ~/notebook +``` + +Expose it to everyone who can access this computer via the network: + +```sh +$ ./fileviewer -web-root ~/notebook -host 0.0.0.0 +``` + +Get a list of available flags: + +```sh +$ ./fileviewer -help +``` + +Running via Bazel (this path is printed when you run the `bazel build ...` +command above): + +```sh +$ bazel-bin/web/fileviewer/fileviewer_/fileviewer [...flags] +``` + diff --git a/web/fileviewer/fileviewer.go b/web/fileviewer/fileviewer.go new file mode 100644 index 0000000..4c3cb3f --- /dev/null +++ b/web/fileviewer/fileviewer.go @@ -0,0 +1,166 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "strings" + + "github.com/gomarkdown/markdown" +) + +var ( + cwd, _ = os.Getwd() + webRoot = flag.String("web-root", cwd, "Root of the web file tree.") + host = flag.String("host", "127.0.0.1", "By default, the server is only accessible via localhost. "+ + "Set to 0.0.0.0 or empty string to open to all.") + port = flag.String("port", getEnvWithDefault("PORT", "8080"), "Port to listen on; $PORT env var overrides default value.") +) + +func getEnvWithDefault(varName, defaultValue string) string { + if value := os.Getenv(varName); value != "" { + return value + } + return defaultValue +} + +func stringHasOneOfSuffixes(str string, suffixes []string) bool { + for _, suffix := range suffixes { + if strings.HasSuffix(str, suffix) { + return true + } + } + return false +} + +type DocHandler struct { + webRoot string +} + +func serveFile(rw http.ResponseWriter, mimeType string, fileContents []byte) { + rw.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", fmt.Sprintf("%s; charset=UTF-8", mimeType)) + rw.Write([]byte(fileContents)) +} + +func (handler *DocHandler) DispatchHandler(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: %s", req.URL.Path) + var urlPath string = req.URL.Path + // Remove any occurrences of `..` in the path to avoid escaping the web root. + urlPath = strings.ReplaceAll(urlPath, "..", "") + // Replace all multi-sequences of `/` with a single slash. + urlPath = strings.ReplaceAll(urlPath, "//", "/") + var fsPath string = path.Join(handler.webRoot, "/./", urlPath) + + log.Printf("Local path: %s", fsPath) + + fileInfo, err := os.Stat(fsPath) + if err != nil && os.IsNotExist(err) { + // File does not exist. + rw.WriteHeader(http.StatusNotFound) + rw.Header().Set("Content-Type", "text/html; charset=UTF-8") + rw.Write([]byte("\n")) + rw.Write([]byte("")) + rw.Write([]byte("
")) + rw.Write([]byte(fmt.Sprintf("%s
not found", urlPath)))
+ rw.Write([]byte(""))
+ rw.Write([]byte(""))
+
+ log.Printf("Path not found: %s", fsPath)
+ return
+ }
+
+ if fileInfo.IsDir() {
+ files, err := ioutil.ReadDir(fsPath)
+ if err != nil {
+ log.Printf("Error listing directory: %s", fsPath)
+ return
+ }
+
+ rw.WriteHeader(http.StatusOK)
+ rw.Header().Set("Content-Type", "text/html; charset=UTF-8")
+ rw.Write([]byte("\n"))
+ rw.Write([]byte(""))
+ rw.Write([]byte(""))
+ rw.Write([]byte(fmt.Sprintf("