summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--cn-fileserver.service24
-rw-r--r--fileserver.go231
-rw-r--r--magic.go32
4 files changed, 288 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..226f507
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/cn-fileserver
diff --git a/cn-fileserver.service b/cn-fileserver.service
new file mode 100644
index 0000000..007585a
--- /dev/null
+++ b/cn-fileserver.service
@@ -0,0 +1,24 @@
+[Unit]
+Description=ContNet file server
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/cn-fileserver 0.0.0.0 /var/contnet/cnroot
+PrivateTmp=yes
+ProtectSystem=strict
+ProtectHome=yes
+InaccessibleDirectories=/home
+ReadOnlyDirectories=/
+CapabilityBoundingSet=
+LimitFSIZE=0
+DeviceAllow=/dev/null rw
+MemoryDenyWriteExecute=yes
+User=nobody
+Group=nogroup
+WorkingDirectory=/var/contnet/cnroot
+Restart=always
+RestartSec=10s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/fileserver.go b/fileserver.go
new file mode 100644
index 0000000..e1754a1
--- /dev/null
+++ b/fileserver.go
@@ -0,0 +1,231 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime"
+ "os"
+ "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "contnet.org/lib/cnm-go"
+ "contnet.org/lib/cnm-go/cnmfmt"
+ "contnet.org/lib/cnp-go"
+)
+
+func init() {
+ mime.AddExtensionType(".cnm", "text/cnm")
+}
+
+func listdir(w cnp.ResponseWriter, r *cnp.Request, root string, f *os.File, stat os.FileInfo) {
+ if p := r.Path(); !strings.HasSuffix(p, "/") {
+ w.Response().SetIntent(cnp.IntentRedirect)
+ w.Response().SetLocation("", p+"/")
+ return
+ }
+
+ files, err := f.Readdir(-1)
+ if err != nil {
+ panic(cnp.ErrorServerError{Reason: err.Error()})
+ }
+
+ sort.Slice(files, func(i, j int) bool {
+ d1, d2 := files[i].IsDir(), files[j].IsDir()
+ if !d1 && d2 {
+ return false
+ }
+ if d1 && !d2 {
+ return true
+ }
+ return files[i].Name() < files[j].Name()
+ })
+
+ relpath, err := filepath.Rel(root, f.Name())
+ if err != nil {
+ panic(cnp.ErrorServerError{Reason: err.Error()})
+ }
+ dirpath := filepath.ToSlash(relpath)
+ site := cnm.Site{Path: dirpath}
+ dirname := path.Clean("/" + dirpath)
+ if !strings.HasSuffix(dirname, "/") {
+ dirname += "/"
+ }
+
+ var par []cnmfmt.Text
+ var links []cnm.Link
+
+ if dirname != "/" {
+ links = []cnm.Link{cnm.Link{
+ URL: "../",
+ Name: "..",
+ Description: "Go to the parent directory",
+ }}
+ par = append(par, cnmfmt.Text{Spans: []cnmfmt.Span{cnmfmt.Span{
+ //Format: cnmfmt.Format{Link: path.Dir(dirname)},
+ Format: cnmfmt.Format{Link: "../"},
+ Text: "..",
+ }}})
+ }
+
+ for _, file := range files {
+ name := file.Name()
+ if name[0] == '.' {
+ continue
+ }
+ if file.IsDir() {
+ name += "/"
+ }
+ //site.Children = append(site.Children, cnm.Site{Path: name, Name: name})
+ par = append(par, cnmfmt.Text{Spans: []cnmfmt.Span{cnmfmt.Span{
+ Format: cnmfmt.Format{Link: dirname + name},
+ Text: name,
+ }}})
+ }
+
+ content := cnm.NewContentBlock("content")
+ content.AppendChild(cnmfmt.NewTextFmtBlock(par))
+
+ if site.Path != "." {
+ site = cnm.Site{Children: []cnm.Site{site}}
+ }
+
+ doc := &cnm.Document{
+ Title: dirname,
+ Content: content,
+ Site: site,
+ Links: links,
+ }
+
+ var buf bytes.Buffer
+ if err = doc.Write(&buf); err != nil {
+ panic(cnp.ErrorServerError{Reason: err.Error()})
+ }
+
+ w.Response().SetType("text/cnm")
+ w.Response().SetLength(int64(buf.Len()))
+
+ if _, err = io.Copy(w, &buf); err != nil {
+ panic(cnp.ErrorServerError{Reason: err.Error()})
+ }
+}
+
+func guessType(fname string) string {
+ typ := mime.TypeByExtension(path.Ext(fname))
+ if typ == "" {
+ typ, _ = GetMimeType(fname)
+ }
+ t, _, _ := mime.ParseMediaType(typ)
+ return t
+}
+
+func open(dir, fpath string) (*os.File, error) {
+ if dir == "" {
+ dir = "."
+ }
+ if filepath.Separator != '/' && strings.ContainsRune(fpath, filepath.Separator) {
+ // prevent exiting root dir on e.g. Windows
+ // TODO: maybe filepath.Clean instead?
+ return nil, cnp.ErrorRejected{Reason: "invalid filepath"}
+ }
+ return os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+fpath))))
+}
+
+func getfile(dir, pth string, strict bool) (*os.File, os.FileInfo) {
+ var f *os.File
+ var err error
+ fnames := []string{pth}
+ if !strings.HasSuffix(pth, ".cnm") {
+ fnames = []string{pth, pth + ".cnm"}
+ }
+ var firstErr error
+ for _, fname := range fnames {
+ f, err = open(dir, fname)
+ if firstErr == nil {
+ firstErr = err
+ }
+ if os.IsNotExist(err) {
+ //panic(cnp.ErrorNotFound{Reason: err.Error()})
+ continue
+ } else if _, ok := err.(cnp.Error); ok {
+ panic(err)
+ } else if err != nil {
+ panic(cnp.ErrorServerError{Reason: err.Error()})
+ }
+ break
+ }
+ var stat os.FileInfo
+ if err != nil {
+ if strict {
+ return nil, stat
+ }
+ panic(cnp.ErrorNotFound{Reason: firstErr.Error()})
+ }
+ stat, err = f.Stat()
+ if err != nil {
+ panic(cnp.ErrorServerError{Reason: err.Error()})
+ }
+ if stat.IsDir() {
+ if !strings.HasSuffix(pth, "/") {
+ return nil, stat
+ }
+ f2, st2 := getfile(dir, pth+"index.cnm", true)
+ if f2 != nil {
+ return f2, st2
+ }
+ }
+ return f, stat
+}
+
+func server(addr, dir string) {
+ fmt.Printf("listening on %q, serving dir %q\n", addr, dir)
+ panic(cnp.ListenAndServe(addr, cnp.HandlerFunc(func(w cnp.ResponseWriter, r *cnp.Request) {
+ f, stat := getfile(dir, r.Path(), false)
+ defer f.Close()
+
+ mtime := stat.ModTime().Truncate(time.Second)
+
+ w.Response().SetModified(mtime)
+ w.Response().SetTime(time.Now())
+ if stat.IsDir() {
+ listdir(w, r, dir, f, stat)
+ return
+ }
+
+ if ifmod := r.IfModified(); !ifmod.IsZero() && (ifmod.Equal(mtime) || ifmod.After(mtime)) {
+ w.Response().SetIntent(cnp.IntentNotModified)
+ if err := w.WriteHeader(); err != nil {
+ panic(err)
+ }
+ return
+ }
+
+ w.Response().SetLength(stat.Size())
+ w.Response().SetName(stat.Name())
+ w.Response().SetType(guessType(f.Name()))
+
+ if _, err := io.Copy(w, f); err != nil {
+ panic(err)
+ }
+ })))
+}
+
+func main() {
+ if len(os.Args) > 3 {
+ prog := path.Base(os.Args[0])
+ fmt.Fprintf(os.Stderr, "%s: usage: %s [HOST[:PORT]] [DIR]", prog, prog)
+ os.Exit(2)
+ }
+ addr := "localhost"
+ dir := "."
+ if len(os.Args) >= 2 {
+ addr = os.Args[1]
+ }
+ if len(os.Args) >= 3 {
+ dir = os.Args[2]
+ }
+ server(addr, dir)
+}
diff --git a/magic.go b/magic.go
new file mode 100644
index 0000000..a6fb48d
--- /dev/null
+++ b/magic.go
@@ -0,0 +1,32 @@
+package main
+
+// #cgo LDFLAGS: -lmagic
+// #include <stdlib.h>
+// #include <magic.h>
+import "C"
+import "errors"
+import "unsafe"
+
+var magic C.magic_t
+
+func init() {
+ magic = C.magic_open(C.MAGIC_MIME_TYPE | C.MAGIC_SYMLINK | C.MAGIC_ERROR)
+ if magic == nil {
+ panic("unable to initialize libmagic")
+ }
+ if C.magic_load(magic, nil) != 0 {
+ C.magic_close(magic)
+ panic("unable to load libmagic database: " + C.GoString(C.magic_error(magic)))
+ }
+}
+
+// GetMimeType uses libmagic to find the mime type of a file by filename.
+func GetMimeType(fname string) (string, error) {
+ cfname := C.CString(fname)
+ defer C.free(unsafe.Pointer(cfname))
+ mime := C.magic_file(magic, cfname)
+ if mime == nil {
+ return "", errors.New(C.GoString(C.magic_error(magic)))
+ }
+ return C.GoString(mime), nil
+}