Min 8 years ago
commit
0735b5f79d
6 changed files with 404 additions and 0 deletions
  1. 19 0
      LICENSE
  2. 6 0
      config.json
  3. 42 0
      index.html
  4. 256 0
      main.go
  5. 32 0
      readme.md
  6. 49 0
      template.go

+ 19 - 0
LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2017 Infinite Coffee (infcof.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 6 - 0
config.json

@@ -0,0 +1,6 @@
+{
+  "address": "127.0.0.1:3001",
+  "data": {
+    "Temp logs": {"path": "/tmp/logs", "ext": "log", "compress": true}
+  }
+}

+ 42 - 0
index.html

@@ -0,0 +1,42 @@
+{{define "index"}}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>{{.Title}}</title>
+
+    <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
+    <link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
+    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
+</head>
+<body>
+    <div class="container">
+        <h2>{{.Title}}</h2>
+        {{if .Error}}
+                <p>Error {{.Error}}</p>
+        {{else}}
+        <hr>
+        <table>
+        <thead>
+            <tr>
+                <th>Name</th>
+                <th>Size</th>
+                <th>Date</th>
+            </tr>
+        </thead>
+        <tbody>
+        {{range .Items}}{{ if .Valid}}
+        <tr>
+            <td><a href="{{.URL}}" class="pure-menu-link">{{.Name}}</a></td>
+            <td>{{.FSize}}</td>
+            <td>{{.FDate}}</td>
+        </tr>
+        {{end}}{{end}}
+        </tbody>
+        </table>
+        {{end}}
+        <hr>
+    </div>
+</body>
+</html>
+{{end}}

+ 256 - 0
main.go

@@ -0,0 +1,256 @@
+package main
+
+
+import (
+	"os"
+	"log"
+	"path"
+	"net/url"
+	"net/http"
+	"io/ioutil"
+	"strings"
+	"encoding/json"
+	"path/filepath"
+
+	"compress/gzip"
+	"bytes"
+	"archive/tar"
+	"io"
+	"time"
+	"math"
+	"fmt"
+	"html/template"
+)
+
+type Item struct {
+	Name string
+	Size int64
+	URL string
+	Date time.Time
+	Valid bool
+}
+
+func (i Item) FDate() string {
+	if i.Date.IsZero() {
+		return "-"
+	}
+	return i.Date.Format(time.RFC822)
+}
+
+func (i Item) FSize() string {
+	if i.Size == 0 {
+		return "-"
+	}
+	if i.Size < 1024 {
+		return fmt.Sprintf("%d B", i.Size)
+	}
+	exp := int(math.Log(float64(i.Size)) / math.Log(1024))
+	pre := string("KMGTPE"[(exp-1)])
+	fsize := float64(i.Size) / math.Pow(1024, float64(exp))
+	return fmt.Sprintf("%.1f %siB", fsize, pre)
+}
+
+type Page struct {
+	Title string
+	Items []Item
+	Error int
+}
+
+type Config struct {
+	Directories map[string]Directory 	`json:"data"`
+	Address 	string					`json:"address"`
+}
+
+type Directory struct {
+	Path 		string	`json:"path"`
+	Extension 	string	`json:"ext"`
+	Compress 	bool	`json:"compress"`
+}
+var config = Config{}
+
+func main() {
+	defer func() {
+		if r := recover(); r != nil {
+			log.Fatalln("Exiting due to error:", r)
+			os.Exit(1)
+		}
+	}()
+	var err error
+
+	// Processing template
+	tmpl, err = template.New("index").Parse(index)
+	if err != nil {
+		panic(err)
+	}
+
+	// Reading configuration
+	logFile, err := ioutil.ReadFile("config.json")
+	if err != nil {
+		panic(err)
+	}
+	err = json.Unmarshal(logFile, &config)
+	if err !=nil {
+		panic(err)
+	}
+
+	// Serving HTTP
+	http.HandleFunc("/", router)
+
+	err = http.ListenAndServe(config.Address, nil)
+	if err != nil {
+		panic(err)
+	}
+	log.Println("Listening on", config.Address)
+}
+
+func router(w http.ResponseWriter, r *http.Request) {
+	defer func() {
+		if rec := recover(); rec != nil {
+			handleError(w, r, rec, "Internal Server Error", http.StatusInternalServerError)
+		}
+	}()
+	if r.URL.Path == "/" {
+		serveIndex(w, r)
+		return
+	}
+	location, _ := path.Split(r.URL.Path)
+	if location == "/" {
+		serveDirectory(w, r)
+	} else {
+		serveFile(w, r)
+	}
+
+
+}
+
+func handleError(w http.ResponseWriter, r *http.Request, err interface{}, message string, status int) {
+	log.Println("Request error [", r.URL.Path, "]:", message + ":", err)
+	w.WriteHeader(status)
+	tmpl.ExecuteTemplate(w, "index", &Page{Title: message, Error: status})
+}
+
+func serveDirectory(w http.ResponseWriter, r *http.Request) {
+	var page Page
+
+	urlPath, err := url.QueryUnescape(r.URL.Path)
+	if err !=nil {
+		handleError(w,r, nil, "Invalid URL", http.StatusBadRequest)
+		return
+	}
+	urlPath = strings.TrimLeft(urlPath, "/")
+
+	configDir, exists := config.Directories[urlPath]
+	if !exists {
+		handleError(w,r, nil, "Directory do not exist", http.StatusNotFound)
+		return
+	}
+
+	files, err := ioutil.ReadDir(configDir.Path)
+	if err != nil {
+		handleError(w,r, err, "Unable to read server directory", http.StatusForbidden)
+		return
+	}
+	page = Page{Title: urlPath, Items: make([]Item, len(files))}
+	for i, file := range files {
+		if file.IsDir() {
+			continue
+		}
+		ext := strings.TrimLeft(filepath.Ext(file.Name()), ".")
+		if strings.ToLower(ext) != strings.ToLower(configDir.Extension) {
+			continue
+		}
+		page.Items[i] = Item{
+			Name: file.Name(),
+			URL: url.QueryEscape(urlPath + "/" + file.Name()),
+			Size: file.Size(),
+			Date: file.ModTime(),
+			Valid: true,
+			}
+	}
+	tmpl.ExecuteTemplate(w, "index", &page)
+}
+
+func serveFile(w http.ResponseWriter, r *http.Request) {
+	urlPath, err := url.QueryUnescape(r.URL.Path)
+	if err !=nil {
+		handleError(w,r, nil, "Invalid URL", http.StatusBadRequest)
+		return
+	}
+	urlPath = strings.TrimLeft(urlPath, "/")
+
+	location, urlFile := path.Split(urlPath)
+	configDir, exists := config.Directories[strings.TrimRight(location, "/")]
+	if !exists {
+		handleError(w,r, nil, "Directory do not exist", http.StatusNotFound)
+		return
+	}
+
+	servedFile := path.Join(configDir.Path, urlFile)
+	if _, err := os.Stat(servedFile); os.IsNotExist(err) {
+		handleError(w,r, err, "File does not exist", http.StatusNotFound)
+		return
+	}
+	if configDir.Compress {
+		buf := new(bytes.Buffer)
+		err = compress(servedFile, buf)
+		if err != nil {
+			handleError(w, r, err, "Unable to compress file", http.StatusInternalServerError)
+			return
+		}
+		w.Header().Set("Content-Disposition", "attachment; filename=\""+urlFile+".tar.gz\"")
+		w.Write(buf.Bytes())
+	} else {
+		file, err := ioutil.ReadFile(servedFile)
+		if err != nil {
+			handleError(w, r, err, "Unable to read file", http.StatusInternalServerError)
+			return
+		}
+		w.Header().Set("Content-Disposition", "attachment; filename=\""+urlFile+"\"")
+		w.Write(file)
+	}
+}
+
+func compress(filePath string, buf *bytes.Buffer) error {
+	gw := gzip.NewWriter(buf)
+	defer gw.Close()
+	tw := tar.NewWriter(gw)
+	defer tw.Close()
+
+	file, err := os.Open(filePath)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	if stat, err := file.Stat(); err == nil {
+		header := new(tar.Header)
+		header.Name = path.Base(filePath)
+		header.Size = stat.Size()
+		header.Mode = int64(stat.Mode())
+		header.ModTime = stat.ModTime()
+		if err := tw.WriteHeader(header); err != nil {
+			return err
+		}
+		if _, err := io.Copy(tw, file); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func serveIndex(w http.ResponseWriter, r *http.Request) {
+	page := Page{Title: "Index", Items: make([]Item, len(config.Directories))}
+	i := 0
+	for name, dir := range config.Directories {
+		stat, err := os.Stat(dir.Path)
+		if err == nil {
+			page.Items[i].Date = stat.ModTime()
+		}
+
+		page.Items[i].Name = name
+		page.Items[i].URL = url.QueryEscape(name)
+		page.Items[i].Valid = true
+		i++
+	}
+	tmpl.ExecuteTemplate(w, "index", &page)
+
+}

+ 32 - 0
readme.md

@@ -0,0 +1,32 @@
+# LogServer
+Program that serves compressed files over http.
+
+## Configuration
+
+Configuration file (config.json) must be located in the same place as executable
+TODO: allow change conifg location via command line arguments
+
+**address** - IP serving address
+**data** - A dictionary of string and _item_.
+String is a name that will be used as directory name in html page.
+**item** - Consists of 3 parts:
+  * **path** - directory location in server
+  * **ext** - file extension. All other files in directory mentioned above will be excluded
+  * **compress** - if true files will be compressed in memory as tarball when served over web.
+
+## Build
+
+Simple as
+``` sh
+go build
+```
+
+For smallest executable
+``` sh
+GOOS=linux go build -ldflags="-s -w"
+upx --brute logserver
+```
+
+## License
+
+[MIT](https://gogs.infcof.com/infcof/Logserver/LICENSE)

+ 49 - 0
template.go

@@ -0,0 +1,49 @@
+package main
+
+import "html/template"
+
+const index = `
+{{define "index"}}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>{{.Title}}</title>
+
+    <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
+    <link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
+    <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
+</head>
+<body>
+    <div class="container">
+        <h2>{{.Title}}</h2>
+        {{if .Error}}
+                <p>Error {{.Error}}</p>
+        {{else}}
+        <hr>
+        <table>
+        <thead>
+            <tr>
+                <th>Name</th>
+                <th>Size</th>
+                <th>Date</th>
+            </tr>
+        </thead>
+        <tbody>
+        {{range .Items}}{{ if .Valid}}
+        <tr>
+            <td><a href="{{.URL}}" class="pure-menu-link">{{.Name}}</a></td>
+            <td>{{.FSize}}</td>
+            <td>{{.FDate}}</td>
+        </tr>
+        {{end}}{{end}}
+        </tbody>
+        </table>
+        {{end}}
+        <hr>
+    </div>
+</body>
+</html>
+{{end}}
+`
+var tmpl *template.Template