diff options
| author | James Mills <prologic@shortcircuit.net.au> | 2020-03-21 09:55:06 +1000 |
|---|---|---|
| committer | James Mills <prologic@shortcircuit.net.au> | 2020-03-21 09:55:06 +1000 |
| commit | 4dae45a68ff1ff32a251a5835b2a2723ae125d1c (patch) | |
| tree | b3abe641f39f50ac444aee2bb617d7b4ab44187d /pkg/app | |
| parent | 76fa613b7eaf1c8626d2a51901c623af7bf1a665 (diff) | |
Restructured source code layout, Added rice support for templates and static assets
Diffstat (limited to 'pkg/app')
| -rw-r--r-- | pkg/app/app.go | 222 | ||||
| -rw-r--r-- | pkg/app/config.go | 89 | ||||
| -rw-r--r-- | pkg/app/feed.go | 77 | ||||
| -rw-r--r-- | pkg/app/listener.go | 37 | ||||
| -rw-r--r-- | pkg/app/tor.go | 45 | ||||
| -rw-r--r-- | pkg/app/watcher.go | 63 |
6 files changed, 0 insertions, 533 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go deleted file mode 100644 index 0efd22a..0000000 --- a/pkg/app/app.go +++ /dev/null @@ -1,222 +0,0 @@ -// Package app manages main application server. -package app - -import ( - "errors" - "fmt" - "html/template" - "log" - "net" - "net/http" - "path" - - "github.com/fsnotify/fsnotify" - "github.com/gorilla/mux" - "github.com/wybiral/tube/pkg/media" - "github.com/wybiral/tube/pkg/onionkey" -) - -// App represents main application. -type App struct { - Config *Config - Library *media.Library - Watcher *fsnotify.Watcher - Templates *template.Template - Feed []byte - Tor *tor - Listener net.Listener - Router *mux.Router -} - -// NewApp returns a new instance of App from Config. -func NewApp(cfg *Config) (*App, error) { - if cfg == nil { - cfg = DefaultConfig() - } - a := &App{ - Config: cfg, - } - // Setup Library - a.Library = media.NewLibrary() - // Setup Watcher - w, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - a.Watcher = w - // Setup Listener - ln, err := newListener(cfg.Server) - if err != nil { - return nil, err - } - a.Listener = ln - // Setup Templates - a.Templates = template.Must(template.ParseGlob("templates/*")) - // Setup Tor - if cfg.Tor.Enable { - t, err := newTor(cfg.Tor) - if err != nil { - return nil, err - } - a.Tor = t - } - // Setup Router - r := mux.NewRouter().StrictSlash(true) - r.HandleFunc("/", a.indexHandler).Methods("GET") - r.HandleFunc("/v/{id}.mp4", a.videoHandler).Methods("GET") - r.HandleFunc("/v/{prefix}/{id}.mp4", a.videoHandler).Methods("GET") - r.HandleFunc("/t/{id}", a.thumbHandler).Methods("GET") - r.HandleFunc("/t/{prefix}/{id}", a.thumbHandler).Methods("GET") - r.HandleFunc("/v/{id}", a.pageHandler).Methods("GET") - r.HandleFunc("/v/{prefix}/{id}", a.pageHandler).Methods("GET") - r.HandleFunc("/feed.xml", a.rssHandler).Methods("GET") - // Static file handler - fsHandler := http.StripPrefix( - "/static/", - http.FileServer(http.Dir("./static/")), - ) - r.PathPrefix("/static/").Handler(fsHandler).Methods("GET") - a.Router = r - return a, nil -} - -// Run imports the library and starts server. -func (a *App) Run() error { - if a.Tor != nil { - var err error - cs := a.Config.Server - key := a.Tor.OnionKey - if key == nil { - key, err = onionkey.GenerateKey() - if err != nil { - return err - } - a.Tor.OnionKey = key - } - onion, err := key.Onion() - if err != nil { - return err - } - onion.Ports[80] = fmt.Sprintf("%s:%d", cs.Host, cs.Port) - err = a.Tor.Controller.AddOnion(onion) - if err != nil { - return errors.New("unable to start Tor onion service") - } - log.Printf("Onion service: http://%s.onion", onion.ServiceID) - } - for _, pc := range a.Config.Library { - p := &media.Path{ - Path: pc.Path, - Prefix: pc.Prefix, - } - err := a.Library.AddPath(p) - if err != nil { - return err - } - err = a.Library.Import(p) - if err != nil { - return err - } - a.Watcher.Add(p.Path) - } - buildFeed(a) - go startWatcher(a) - return http.Serve(a.Listener, a.Router) -} - -// HTTP handler for / -func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) { - log.Printf("/") - pl := a.Library.Playlist() - if len(pl) > 0 { - http.Redirect(w, r, "/v/"+pl[0].ID, 302) - } else { - a.Templates.ExecuteTemplate(w, "index.html", &struct { - Playing *media.Video - Playlist media.Playlist - }{ - Playing: &media.Video{ID: ""}, - Playlist: a.Library.Playlist(), - }) - } -} - -// HTTP handler for /v/id -func (a *App) pageHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - prefix, ok := vars["prefix"] - if ok { - id = path.Join(prefix, id) - } - log.Printf("/v/%s", id) - playing, ok := a.Library.Videos[id] - if !ok { - a.Templates.ExecuteTemplate(w, "index.html", &struct { - Playing *media.Video - Playlist media.Playlist - }{ - Playing: &media.Video{ID: ""}, - Playlist: a.Library.Playlist(), - }) - return - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - a.Templates.ExecuteTemplate(w, "index.html", &struct { - Playing *media.Video - Playlist media.Playlist - }{ - Playing: playing, - Playlist: a.Library.Playlist(), - }) -} - -// HTTP handler for /v/id.mp4 -func (a *App) videoHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - prefix, ok := vars["prefix"] - if ok { - id = path.Join(prefix, id) - } - log.Printf("/v/%s", id) - m, ok := a.Library.Videos[id] - if !ok { - return - } - title := m.Title - disposition := "attachment; filename=\"" + title + ".mp4\"" - w.Header().Set("Content-Disposition", disposition) - w.Header().Set("Content-Type", "video/mp4") - http.ServeFile(w, r, m.Path) -} - -// HTTP handler for /t/id -func (a *App) thumbHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - prefix, ok := vars["prefix"] - if ok { - id = path.Join(prefix, id) - } - log.Printf("/t/%s", id) - m, ok := a.Library.Videos[id] - if !ok { - return - } - w.Header().Set("Cache-Control", "public, max-age=7776000") - if m.ThumbType == "" { - w.Header().Set("Content-Type", "image/jpeg") - http.ServeFile(w, r, "static/defaulticon.jpg") - } else { - w.Header().Set("Content-Type", m.ThumbType) - w.Write(m.Thumb) - } -} - -// HTTP handler for /feed.xml -func (a *App) rssHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "public, max-age=7776000") - w.Header().Set("Content-Type", "text/xml") - w.Write(a.Feed) -} diff --git a/pkg/app/config.go b/pkg/app/config.go deleted file mode 100644 index ec27435..0000000 --- a/pkg/app/config.go +++ /dev/null @@ -1,89 +0,0 @@ -package app - -import ( - "encoding/json" - "os" -) - -// Config settings for main App. -type Config struct { - Library []*PathConfig `json:"library"` - Server *ServerConfig `json:"server"` - Feed *FeedConfig `json:"feed"` - Tor *TorConfig `json:"tor,omitempty"` -} - -// PathConfig settings for media library path. -type PathConfig struct { - Path string `json:"path"` - Prefix string `json:"prefix"` -} - -// ServerConfig settings for App Server. -type ServerConfig struct { - Host string `json:"host"` - Port int `json:"port"` -} - -// FeedConfig settings for App Feed. -type FeedConfig struct { - ExternalURL string `json:"external_url"` - Title string `json:"title"` - Link string `json:"link"` - Description string `json:"description"` - Author struct { - Name string `json:"name"` - Email string `json:"email"` - } `json:"author"` - Copyright string `json:"copyright"` -} - -// TorConfig stores tor configuration. -type TorConfig struct { - Enable bool `json:"enable"` - Controller *TorControllerConfig `json:"controller"` -} - -// TorControllerConfig stores tor controller configuration. -type TorControllerConfig struct { - Host string `json:"host"` - Port int `json:"port"` - Password string `json:"password,omitempty"` -} - -// DefaultConfig returns Config initialized with default values. -func DefaultConfig() *Config { - return &Config{ - Library: []*PathConfig{ - &PathConfig{ - Path: "videos", - Prefix: "", - }, - }, - Server: &ServerConfig{ - Host: "127.0.0.1", - Port: 0, - }, - Feed: &FeedConfig{ - ExternalURL: "http://localhost", - }, - Tor: &TorConfig{ - Enable: false, - Controller: &TorControllerConfig{ - Host: "127.0.0.1", - Port: 9051, - }, - }, - } -} - -// ReadFile reads a JSON file into Config. -func (c *Config) ReadFile(path string) error { - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - d := json.NewDecoder(f) - return d.Decode(c) -} diff --git a/pkg/app/feed.go b/pkg/app/feed.go deleted file mode 100644 index e4c1ce2..0000000 --- a/pkg/app/feed.go +++ /dev/null @@ -1,77 +0,0 @@ -package app - -import ( - "fmt" - "net/url" - "os" - "path" - "strconv" - "time" - - "github.com/wybiral/feeds" -) - -// buildFeed creates RSS feed attribute for App based on Library contents. -func buildFeed(a *App) { - cfg := a.Config.Feed - now := time.Now() - f := &feeds.Feed{ - Title: cfg.Title, - Link: &feeds.Link{Href: cfg.Link}, - Description: cfg.Description, - Author: &feeds.Author{ - Name: cfg.Author.Name, - Email: cfg.Author.Email, - }, - Created: now, - Copyright: cfg.Copyright, - } - var externalURL string - if len(cfg.ExternalURL) > 0 { - externalURL = cfg.ExternalURL - } else if a.Tor != nil { - onion, err := a.Tor.OnionKey.Onion() - if err != nil { - return - } - externalURL = fmt.Sprintf("http://%s.onion", onion.ServiceID) - } else { - hostname, err := os.Hostname() - if err != nil { - host := a.Config.Server.Host - port := a.Config.Server.Port - externalURL = fmt.Sprintf("http://%s:%d", host, port) - } else { - externalURL = fmt.Sprintf("http://%s", hostname) - } - } - for _, v := range a.Library.Playlist() { - u, err := url.Parse(externalURL) - if err != nil { - return - } - u.Path = path.Join(u.Path, "v", v.ID) - id := u.String() - f.Items = append(f.Items, &feeds.Item{ - Id: id, - Title: v.Title, - Link: &feeds.Link{Href: id}, - Description: v.Description, - Enclosure: &feeds.Enclosure{ - Url: id + ".mp4", - Length: strconv.FormatInt(v.Size, 10), - Type: "video/mp4", - }, - Author: &feeds.Author{ - Name: cfg.Author.Name, - Email: cfg.Author.Email, - }, - Created: v.Timestamp, - }) - } - feed, err := f.ToRss() - if err != nil { - return - } - a.Feed = []byte(feed) -} diff --git a/pkg/app/listener.go b/pkg/app/listener.go deleted file mode 100644 index a4236d8..0000000 --- a/pkg/app/listener.go +++ /dev/null @@ -1,37 +0,0 @@ -// Instead of using the default new.Listener this file will construct a custom -// one. The main purpose for this is to have more control over the settings -// (like keep-alive) and to retrieve the assigned port when using port 0. - -package app - -import ( - "fmt" - "net" - "time" -) - -func newListener(cfg *ServerConfig) (net.Listener, error) { - addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - ln, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - // set actual port on config object (in case original port was 0) - cfg.Port = ln.Addr().(*net.TCPAddr).Port - return tcpListener{ln.(*net.TCPListener)}, nil -} - -// custom TCP listener with keep-alive timeout -type tcpListener struct { - *net.TCPListener -} - -func (ln tcpListener) Accept() (net.Conn, error) { - tc, err := ln.AcceptTCP() - if err != nil { - return nil, err - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} diff --git a/pkg/app/tor.go b/pkg/app/tor.go deleted file mode 100644 index d06751f..0000000 --- a/pkg/app/tor.go +++ /dev/null @@ -1,45 +0,0 @@ -package app - -import ( - "errors" - "fmt" - "os" - - "github.com/wybiral/torgo" - "github.com/wybiral/tube/pkg/onionkey" -) - -type tor struct { - OnionKey onionkey.Key - Controller *torgo.Controller -} - -func newTor(ct *TorConfig) (*tor, error) { - addr := fmt.Sprintf("%s:%d", ct.Controller.Host, ct.Controller.Port) - ctrl, err := torgo.NewController(addr) - if err != nil { - return nil, errors.New("unable to connect to Tor controller") - } - if len(ct.Controller.Password) > 0 { - err = ctrl.AuthenticatePassword(ct.Controller.Password) - } else { - err = ctrl.AuthenticateCookie() - if err != nil { - err = ctrl.AuthenticateNone() - } - } - if err != nil { - return nil, errors.New("unable to authenticate to Tor controller") - } - key, err := onionkey.ReadFile("onion.key") - if os.IsNotExist(err) { - key = nil - } else if err != nil { - return nil, err - } - t := &tor{ - Controller: ctrl, - OnionKey: key, - } - return t, nil -} diff --git a/pkg/app/watcher.go b/pkg/app/watcher.go deleted file mode 100644 index 74db715..0000000 --- a/pkg/app/watcher.go +++ /dev/null @@ -1,63 +0,0 @@ -package app - -import ( - "time" - - fs "github.com/fsnotify/fsnotify" -) - -// This is the amount of time to wait after changes before reacting to them. -// Debounce is done because moving files into the watched directories causes -// many rapid "Write" events to fire which would cause excessive Remove/Add -// method calls on the Library. To avoid this we accumulate the changes and -// only perform them once the events have stopped for this amount of time. -const debounceTimeout = time.Second * 5 - -// create, write, and chmod all require an add event -const addFlags = fs.Create | fs.Write | fs.Chmod - -// remove, rename, write, and chmod all require a remove event -const removeFlags = fs.Remove | fs.Rename | fs.Write | fs.Chmod - -// watch library paths and update Library with changes. -func startWatcher(a *App) { - timer := time.NewTimer(debounceTimeout) - addEvents := make(map[string]struct{}) - removeEvents := make(map[string]struct{}) - for { - select { - case e := <-a.Watcher.Events: - if e.Op&removeFlags != 0 { - removeEvents[e.Name] = struct{}{} - } - if e.Op&addFlags != 0 { - addEvents[e.Name] = struct{}{} - } - // reset timer - timer.Reset(debounceTimeout) - case <-timer.C: - eventCount := len(removeEvents) + len(addEvents) - // handle remove events first - if len(removeEvents) > 0 { - for p := range removeEvents { - a.Library.Remove(p) - } - // clear map - removeEvents = make(map[string]struct{}) - } - // then handle add events - if len(addEvents) > 0 { - for p := range addEvents { - a.Library.Add(p) - } - // clear map - addEvents = make(map[string]struct{}) - } - if eventCount > 0 { - buildFeed(a) - } - // reset timer - timer.Reset(debounceTimeout) - } - } -} |
