Skip to content

Commit

Permalink
Add fileviewer for browsing files via web server
Browse files Browse the repository at this point in the history
  • Loading branch information
mbrukman committed Apr 19, 2024
1 parent 78d8d7c commit 6d23276
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*~

# Go binaries
web/fileviewer/fileviewer
web/proxy/proxy
web/server/server

Expand Down
7 changes: 7 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
29 changes: 29 additions & 0 deletions web/fileviewer/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
)
64 changes: 64 additions & 0 deletions web/fileviewer/README.md
Original file line number Diff line number Diff line change
@@ -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]
```

166 changes: 166 additions & 0 deletions web/fileviewer/fileviewer.go
Original file line number Diff line number Diff line change
@@ -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("<!DOCTYPE html>\n"))
rw.Write([]byte("<html>"))
rw.Write([]byte("<body>"))
rw.Write([]byte(fmt.Sprintf("<h1>Error 404: <code>%s</code> not found", urlPath)))
rw.Write([]byte("</body>"))
rw.Write([]byte("</html>"))

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("<!DOCTYPE html>\n"))
rw.Write([]byte("<html>"))
rw.Write([]byte("<body>"))
rw.Write([]byte(fmt.Sprintf("<h1>Directory listing: %s</h1>", urlPath)))
rw.Write([]byte("<ul>"))
for _, file := range files {
// Skip internal files, e.g., `.git` directory, `.gitignore`, other dotfiles, etc.
if strings.HasPrefix(file.Name(), ".") {
continue
}
var listItem string
if urlPath == "/" {
listItem = fmt.Sprintf("<li><a href='%s'>%s</li>\n", file.Name(), file.Name())
} else {
listItem = fmt.Sprintf("<li><a href='%s/%s'>%s</li>\n", urlPath, file.Name(), file.Name())
}
rw.Write([]byte(listItem))
}
rw.Write([]byte("</ul>"))
rw.Write([]byte("</body>"))
rw.Write([]byte("</html>"))
return
}

fileContents, err := ioutil.ReadFile(fsPath)
if err != nil {
log.Printf("Error reading file (%s): %s", fsPath, err)
return
}

if strings.HasSuffix(fsPath, ".html") {
serveFile(rw, "text/html", fileContents)
} else if strings.HasSuffix(fsPath, ".js") {
serveFile(rw, "text/javascript", fileContents)
} else if strings.HasSuffix(fsPath, ".ts") {
serveFile(rw, "text/typescript", fileContents)
} else if strings.HasSuffix(fsPath, ".css") {
serveFile(rw, "text/css", fileContents)
} else if strings.HasSuffix(fsPath, ".md") {
rw.WriteHeader(http.StatusOK)
rw.Header().Set("Content-Type", "text/html; charset=UTF-8")
rw.Write([]byte(`<!doctype html>
<html>
<head>
<style>
code {
background-color: #efefef;
margin: 3px;
padding: 3px;
}
</style>
</head>
<body>`))
rw.Write(markdown.ToHTML(fileContents, nil, nil))
rw.Write([]byte(`</body>
</html>`))
} else if stringHasOneOfSuffixes(fsPath, []string{".txt", ".text", ".json", ".sh"}) {
rw.Header().Set("Content-Type", "text/plain; charset=UTF-8")
rw.Write(fileContents)
} else {
rw.Header().Set("Content-Type", "text/plain; charset=UTF-8")
rw.Write([]byte("Unrecognized file content type or suffix."))
log.Printf("Unrecognized file content type or suffix: %s", fsPath)
}
}

func NewDocHandler(webRoot string) *DocHandler {
return &DocHandler{webRoot: webRoot}
}

func main() {
flag.Parse()

handler := NewDocHandler(*webRoot)
http.HandleFunc("/", handler.DispatchHandler)

hostPort := fmt.Sprintf("%s:%s", *host, *port)
log.Printf("Listening on http://%s", hostPort)
log.Printf("Serving from %s", *webRoot)
log.Fatal(http.ListenAndServe(hostPort, nil))
}

0 comments on commit 6d23276

Please sign in to comment.