diff options
author | Christian Pointner <equinox@helsinki.at> | 2016-04-02 16:53:16 (GMT) |
---|---|---|
committer | Christian Pointner <equinox@helsinki.at> | 2016-04-02 16:53:16 (GMT) |
commit | 374cabeb4d2d59b588e9d4a30e3c09e2dbc0abdd (patch) | |
tree | 06bd6587085c6a572bee5077c440b445a4479b6a | |
parent | 27eaf84de6c5900c739f73725bd07270d43d49b7 (diff) | |
parent | 761d6d0824c0cc92fc746b77499ac563f4e6e579 (diff) |
Merge branch 'upload-feature'
-rw-r--r-- | src/rhimportd/ctrlWatchDir.go | 5 | ||||
-rw-r--r-- | src/rhimportd/ctrlWeb.go | 9 | ||||
-rw-r--r-- | src/rhimportd/main.go | 35 | ||||
-rw-r--r-- | src/rhimportd/uploadWeb.go | 211 | ||||
-rw-r--r-- | web-static/upload-form.html | 25 |
5 files changed, 279 insertions, 6 deletions
diff --git a/src/rhimportd/ctrlWatchDir.go b/src/rhimportd/ctrlWatchDir.go index 1b08e62..14d1c1f 100644 --- a/src/rhimportd/ctrlWatchDir.go +++ b/src/rhimportd/ctrlWatchDir.go @@ -165,6 +165,8 @@ func watchDirHandler(conf *rhimport.Config, db *rddb.DBChan, ctx *rhimport.Conte func watchDirRun(dir *os.File, conf *rhimport.Config, db *rddb.DBChan) { rhl.Printf("watch-dir-ctrl: watching for files in %s", dir.Name()) + t := time.NewTicker(1 * time.Second) + defer t.Stop() for { var err error if _, err = dir.Seek(0, 0); err != nil { @@ -199,8 +201,7 @@ func watchDirRun(dir *os.File, conf *rhimport.Config, db *rddb.DBChan) { } } } - - time.Sleep(1 * time.Second) + <-t.C } } diff --git a/src/rhimportd/ctrlWeb.go b/src/rhimportd/ctrlWeb.go index 40075d0..2b52ef0 100644 --- a/src/rhimportd/ctrlWeb.go +++ b/src/rhimportd/ctrlWeb.go @@ -44,11 +44,14 @@ func (self webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { self.H(self.Config, self.DBChan, self.SessionStoreChan, self.trusted, w, r) } -func StartControlWeb(addr string, conf *rhimport.Config, db *rddb.DBChan, sessions *rhimport.SessionStoreChan) { - http.Handle("/public/simple", webHandler{conf, db, sessions, false, webSimpleHandler}) - // http.Handle("/trusted/simple", webHandler{conf, db, sessions, true, webSimpleHandler}) +func StartControlWeb(addr, staticDir string, uploadMaxAge time.Duration, conf *rhimport.Config, db *rddb.DBChan, sessions *rhimport.SessionStoreChan) { + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) + // http.Handle("/trusted/simple", webHandler{conf, db, sessions, true, webSimpleHandler}) + http.Handle("/public/simple", webHandler{conf, db, sessions, false, webSimpleHandler}) http.Handle("/public/socket", webHandler{conf, db, sessions, false, webSocketHandler}) + http.Handle("/public/upload", webHandler{conf, db, sessions, false, webUploadHandler}) + go webUploadCleaner(conf, uploadMaxAge) rhl.Println("web-ctrl: listening on", addr) server := &http.Server{Addr: addr, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second} diff --git a/src/rhimportd/main.go b/src/rhimportd/main.go index 4b0514b..97a12ec 100644 --- a/src/rhimportd/main.go +++ b/src/rhimportd/main.go @@ -34,6 +34,7 @@ import ( "os" "os/signal" "sync" + "time" ) var ( @@ -66,9 +67,33 @@ func (s *envStringValue) Get() interface{} { return string(*s) } func (s *envStringValue) String() string { return fmt.Sprintf("%s", *s) } +type envDurationValue time.Duration + +func newEnvDurationValue(key string, dflt time.Duration) (*envDurationValue, error) { + if envval, exists := os.LookupEnv(key); exists { + var val envDurationValue + err := (&val).Set(envval) + return &val, err + } else { + return (*envDurationValue)(&dflt), nil + } +} + +func (d *envDurationValue) Set(val string) error { + dval, err := time.ParseDuration(val) + *d = envDurationValue(dval) + return err +} + +func (d *envDurationValue) Get() interface{} { return time.Duration(*d) } + +func (d *envDurationValue) String() string { return fmt.Sprintf("%v", *d) } + func main() { webAddr := newEnvStringValue("RHIMPORTD_WEB_ADDR", "localhost:4080") flag.Var(webAddr, "web-addr", "addr:port to listen on (environment: RHIMPORTD_WEB_ADDR)") + webStaticDir := newEnvStringValue("RHIMPORTD_WEB_STATIC_DIR", "/usr/share/rhimportd/web-static/") + flag.Var(webStaticDir, "web-static-dir", "path to static html files (environment: RHIMPORTD_WEB_STATIC_DIR)") telnetAddr := newEnvStringValue("RHIMPORTD_TELNET_ADDR", "localhost:4023") flag.Var(telnetAddr, "telnet-addr", "addr:port to listen on (environment: RHIMPORTD_TELNET_ADDR)") watchDir := newEnvStringValue("RHIMPORTD_WATCH_DIR", "") @@ -81,6 +106,14 @@ func main() { flag.Var(tempDir, "tmp-dir", "path to temporary files (environment: RHIMPORTD_TEMP_DIR)") localFetchDir := newEnvStringValue("RHIMPORTD_LOCAL_FETCH_DIR", os.TempDir()) flag.Var(localFetchDir, "local-fetch-dir", "base path for local:// urls (environment: RHIMPORTD_LOCAL_FETCH_DIR)") + + uploadMaxAge, err := newEnvDurationValue("RHIMPORTD_UPLOAD_MAX_AGE", 30*time.Minute) + if err != nil { + rhl.Println("Error parsing RHIMPORTD_UPLOAD_MAX_AGE from environment:", err) + return + } + flag.Var(uploadMaxAge, "upload-max-age", "maximum age of uploaded files before the get deleted (environment: RHIMPORTD_UPLOAD_MAX_AGE)") + help := flag.Bool("help", false, "show usage") flag.Parse() @@ -112,7 +145,7 @@ func main() { go func() { defer wg.Done() rhl.Println("starting web-ctrl") - StartControlWeb(webAddr.Get().(string), conf, db.GetInterface(), sessions.GetInterface()) + StartControlWeb(webAddr.Get().(string), webStaticDir.Get().(string), uploadMaxAge.Get().(time.Duration), conf, db.GetInterface(), sessions.GetInterface()) rhl.Println("web-ctrl finished") }() } diff --git a/src/rhimportd/uploadWeb.go b/src/rhimportd/uploadWeb.go new file mode 100644 index 0000000..4b56a43 --- /dev/null +++ b/src/rhimportd/uploadWeb.go @@ -0,0 +1,211 @@ +// +// rhimportd +// +// The Radio Helsinki Rivendell Import Daemon +// +// +// Copyright (C) 2015-2016 Christian Pointner <equinox@helsinki.at> +// +// This file is part of rhimportd. +// +// rhimportd is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// rhimportd is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with rhimportd. If not, see <http://www.gnu.org/licenses/>. +// + +package main + +import ( + "code.helsinki.at/rhrd-go/rddb" + "code.helsinki.at/rhrd-go/rhimport" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + "time" +) + +type webUploadResponseData struct { + ResponseCode int `json:"RESPONSE_CODE"` + ErrorString string `json:"ERROR_STRING"` + SourceFile string `json:"SOURCE_FILE"` +} + +func webUploadErrorResponse(w http.ResponseWriter, code int, errStr string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + encoder := json.NewEncoder(w) + respdata := webUploadResponseData{code, errStr, ""} + encoder.Encode(respdata) +} + +func webUploadResponse(w http.ResponseWriter, file string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + encoder := json.NewEncoder(w) + respdata := webUploadResponseData{http.StatusOK, "success", file} + encoder.Encode(respdata) +} + +const ( + webUploadMaxRequestSize = 2 << 30 // 2GB +) + +func webUploadHandler(conf *rhimport.Config, db *rddb.DBChan, sessions *rhimport.SessionStoreChan, trusted bool, w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + http.Redirect(w, r, "/static/upload-form.html", http.StatusTemporaryRedirect) + return + } + + if r.Method != "POST" { + rhl.Printf("WebUploadHandler: got invalid request method") + webUploadErrorResponse(w, http.StatusMethodNotAllowed, "only POST and GET method are allowed") + return + } + + // This is from: stackoverflow.com/questions/26392196 + if r.ContentLength > webUploadMaxRequestSize { + rhl.Printf("WebUploadHandler: request ist to large: %d > %d", r.ContentLength, webUploadMaxRequestSize) + webUploadErrorResponse(w, http.StatusExpectationFailed, "request too large") + return + } + r.Body = http.MaxBytesReader(w, r.Body, webUploadMaxRequestSize) + if err := r.ParseMultipartForm(32 << 10); err != nil { + rhl.Printf("WebUploadHandler: error while parsing multi-part-form: %v", err) + webUploadErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + username := r.FormValue("LOGIN_NAME") + password := r.FormValue("PASSWORD") + if username == "" { + webUploadErrorResponse(w, http.StatusBadRequest, "missing field LOGIN_NAME") + return + } + if password == "" { + webUploadErrorResponse(w, http.StatusBadRequest, "missing field LOGIN_NAME") + return + } + + if authenticated, err := db.CheckPassword(username, password); err != nil { + rhl.Printf("WebUploadHandler: error checking username/password: %v", err) + webUploadErrorResponse(w, http.StatusUnauthorized, err.Error()) + return + } else if !authenticated { + rhl.Printf("WebUploadHandler: invalid username/password") + webUploadErrorResponse(w, http.StatusUnauthorized, "invalid username/password") + return + } + + src, hdr, err := r.FormFile("FILENAME") + if err != nil { + rhdl.Printf("WebUploadHandler: error looking for upload file in form: %s", err) + webUploadErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + defer src.Close() + + rhdl.Printf("WebUploadHandler: got request from user '%s', filename='%s'", username, hdr.Filename) + + dstpath, err := ioutil.TempDir(conf.TempDir, "webupload-"+username+"-") + if err != nil { + rhl.Printf("WebUploadHandler: error creating temporary directory: %v", err) + webUploadErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + dstfile := dstpath + "/" + hdr.Filename + dst, err := os.OpenFile(dstfile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + rhl.Printf("WebUploadHandler: Unable to create file '%s': %v", dstfile, err) + webUploadErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + defer dst.Close() + + size, err := io.Copy(dst, src) + if err != nil { + rhl.Printf("WebUploadHandler: error storing uploaded file '%s': %v", dstfile, err) + webUploadErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + rhl.Printf("WebUploadHandler: stored file '%s' (size: %d Bytes)", dstfile, size) + webUploadResponse(w, "tmp://"+strings.TrimPrefix(dstfile, conf.TempDir)) +} + +func webUploadCleanerRun(dir *os.File, conf *rhimport.Config, maxAge time.Duration) { + t := time.NewTicker(1 * time.Minute) + defer t.Stop() + for now := range t.C { + var err error + if _, err = dir.Seek(0, 0); err != nil { + rhl.Printf("webUploadCleaner: reading directory contents failed: %s", err) + return + } + + var entries []os.FileInfo + if entries, err = dir.Readdir(0); err != nil { + rhl.Printf("webUploadCleaner: reading directory contents failed: %s", err) + return + } + + for _, entry := range entries { + if !strings.HasPrefix(entry.Name(), "webupload-") { + continue + } + + age := now.Sub(entry.ModTime()) + if age <= maxAge { + continue + } + + if err := os.RemoveAll(conf.TempDir + entry.Name()); err != nil { + rhl.Printf("webUploadCleaner: deleting dir '%s' failed: %v", entry.Name(), err) + continue + } + rhl.Printf("webUploadCleaner: successfully deleted dir '%s', Age: %v", entry.Name(), age) + } + } +} + +func webUploadCleaner(conf *rhimport.Config, maxAge time.Duration) { + first := true + for { + if !first { + time.Sleep(5 * time.Second) + } + first = false + + dir, err := os.Open(conf.TempDir) + if err != nil { + rhl.Printf("webUploadCleaner: %s", err) + continue + } + if i, err := dir.Stat(); err != nil { + rhl.Printf("webUploadCleaner: %s", err) + dir.Close() + continue + } else { + if !i.IsDir() { + rhl.Printf("webUploadCleaner: %s is not a directory", conf.TempDir) + dir.Close() + continue + } + } + rhdl.Printf("webUploadCleaner: running with max age: %v", maxAge) + webUploadCleanerRun(dir, conf, maxAge) + rhdl.Printf("webUploadCleanerRun: returned - restring in 5 sec...") + dir.Close() + } +} diff --git a/web-static/upload-form.html b/web-static/upload-form.html new file mode 100644 index 0000000..bbb397c --- /dev/null +++ b/web-static/upload-form.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>rhimportd File Upload</title> + </head> + <body> + <div class="container"> + <h1>rhimportd File Upload</h1> + <form class="form-upload" method="post" action="/public/upload" enctype="multipart/form-data"> + <fieldset> + <label for="LOGIN_NAME">Username:</label> + <input type="text" name="LOGIN_NAME"> + </br> + <label for="LOGIN_NAME">Password:</label> + <input type="password" name="PASSWORD"> + </br> + <label for="FILENAME">File:</label> + <input type="file" name="FILENAME"> + </br> + <input type="submit" name="submit" value="Submit"> + </fieldset> + </form> + </div> + </body> +</html> |