From 45d48b78ea2809efd6f55c671e146d039fb389af Mon Sep 17 00:00:00 2001 From: Mirror Date: Thu, 16 Jun 2022 12:07:46 +0300 Subject: [PATCH] init Signed-off-by: Mirror --- .gitignore | 4 + admin.go | 206 +++++++++++++++++++++++++++++++++++++++++++ config.go | 42 +++++++++ go.mod | 5 ++ go.sum | 3 + logger.go | 18 ++++ main.go | 195 ++++++++++++++++++++++++++++++++++++++++ templates/admin.html | 41 +++++++++ templates/index.html | 100 +++++++++++++++++++++ video.go | 39 ++++++++ 10 files changed, 653 insertions(+) create mode 100644 .gitignore create mode 100644 admin.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logger.go create mode 100644 main.go create mode 100644 templates/admin.html create mode 100644 templates/index.html create mode 100644 video.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eca95b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +videopage +web +config.yml + diff --git a/admin.go b/admin.go new file mode 100644 index 0000000..71a4dba --- /dev/null +++ b/admin.go @@ -0,0 +1,206 @@ +package main + +import ( + "crypto/md5" + "crypto/subtle" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "strings" + "time" +) + +type AdminApi struct { + mux *http.ServeMux +} + +func NewAdminApi() (a *AdminApi) { + a = &AdminApi{} + a.mux = http.NewServeMux() + a.mux.HandleFunc("/", a.adminHandler) + a.mux.HandleFunc("/delete", a.deleteVideo) + a.mux.HandleFunc("/upload", a.uploadVideo) + a.mux.HandleFunc("/rename", a.renameVideo) + return a +} +func (a *AdminApi) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if a.checkAuth(w, r) { + a.mux.ServeHTTP(w, r) + } +} +func (a *AdminApi) checkAuth(w http.ResponseWriter, r *http.Request) (success bool) { + success = false + user, password, ok := r.BasicAuth() + if subtle.ConstantTimeCompare([]byte(user), []byte(config.AdminUser)) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte(config.AdminPassword)) == 1 { + success = true + } + if !success { + if !ok { + time.Sleep(3 * time.Second) + } + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", config.ApplicationName)) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + return success +} + +func (a *AdminApi) adminHandler(w http.ResponseWriter, r *http.Request) { + var v struct { + ApplicationName string + Videos []Video + } + v.ApplicationName = config.ApplicationName + + dataDir := path.Join(config.DataDirectory, "video") + dir, err := os.ReadDir(dataDir) + if err != nil { + log.Printf("admin: can't read data directory: %s", err.Error()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + for _, entry := range dir { + if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".json") && entry.Name() != "index.json" { + video, err := readVideoInfo(path.Join(dataDir, entry.Name())) + if err != nil { + log.Printf("admin: can't read video info: %s", err.Error()) + continue + } + v.Videos = append(v.Videos, video) + } + } + + err = executeTemplate(w, "admin.html", &v) + if err != nil { + log.Printf("Can't execute template \"admin.html\": %s", err.Error()) + return + } +} + +func (a *AdminApi) deleteVideo(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + err := r.ParseForm() + referer := r.Header.Get("Referer") + if err != nil { + log.Printf("can't parse form: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + ID := r.Form.Get("fileid") + ID = path.Clean(path.Base(ID)) + rawInfo, err := ioutil.ReadFile(path.Join(config.DataDirectory, "video", ID+".json")) + if err != nil { + log.Printf("can't read info: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + var info Video + err = json.Unmarshal(rawInfo, &info) + if err != nil { + log.Printf("can't read info: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + err = os.Remove(path.Join(config.DataDirectory, "video", info.Name)) + if err != nil { + log.Printf("can't remove video: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + err = os.Remove(path.Join(config.DataDirectory, "video", ID+".json")) + if err != nil { + log.Printf("can't remove info: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + http.Redirect(w, r, referer, http.StatusMovedPermanently) +} +func (a *AdminApi) uploadVideo(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + err := r.ParseForm() + if err != nil { + log.Printf("can't parse form: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + referer := r.Header.Get("Referer") + reader, err := r.MultipartReader() + if err != nil { + log.Printf("can't read multipart: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + for { + header, err := reader.NextPart() + if err == io.EOF { + break + } + defer header.Close() + if header.FormName() != "file" { + break + } + filename := header.FileName() + if len(filename) == 0 { + http.Error(w, "Filename are empty", http.StatusBadRequest) + return + } + tempFile, err := ioutil.TempFile(path.Join(config.DataDirectory, "tmp"), "upload-") + if err != nil { + log.Printf("can't create temp file: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + defer tempFile.Close() + hashbucket := md5.New() + fileWriter := io.MultiWriter(tempFile, hashbucket) + written, err := io.Copy(fileWriter, header) + if err != nil { + log.Printf("can't write temp file: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + tmpName := path.Base(tempFile.Name()) + err = tempFile.Close() + if err != nil { + log.Printf("can't close file: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + var video Video + video.DisplayName = path.Base(path.Clean(filename)) + video.ID = fmt.Sprintf("%x", hashbucket.Sum(nil)) + video.Name = fmt.Sprintf("%s%s", video.ID, path.Ext(filename)) + video.Size = written + err = os.Rename(path.Join(config.DataDirectory, "tmp", tmpName), path.Join(config.DataDirectory, "video", video.Name)) + if err != nil { + log.Printf("can't rename file: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + infoRaw, err := json.Marshal(&video) + if err != nil { + log.Printf("can't marshal info: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + err = ioutil.WriteFile(path.Join(config.DataDirectory, "video", video.ID+".json"), infoRaw, os.ModePerm) + if err != nil { + log.Printf("can't write info: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } + http.Redirect(w, r, referer, http.StatusMovedPermanently) +} +func (a *AdminApi) renameVideo(w http.ResponseWriter, r *http.Request) {} diff --git a/config.go b/config.go new file mode 100644 index 0000000..a1104a4 --- /dev/null +++ b/config.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +type Config struct { + ApplicationName string `yaml:"application_name"` + ApplicationPrefix string `yaml:"application_prefix"` + DataDirectory string `yaml:"data_directory"` + ListenAddress string `yaml:"listen_address"` + AdminUser string `yaml:"admin_user"` + AdminPassword string `yaml:"admin_password"` + Generation int `yaml:"generation"` +} + +func (c *Config) ParseFile(path string) (err error) { + configRaw, err := ioutil.ReadFile(path) + if err != nil { + err = fmt.Errorf("can't load configuration: %s", err.Error()) + return + } + err = yaml.UnmarshalStrict(configRaw, c) + if err != nil { + err = fmt.Errorf("can't parse configuration: %s", err.Error()) + return + } + return +} + +func NewConfig() *Config { + return &Config{} +} + +func NewConfigParseFile(path string) (c *Config, err error) { + c = NewConfig() + err = c.ParseFile(path) + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ad63a8b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module videopage + +go 1.18 + +require gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7534661 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..d341a16 --- /dev/null +++ b/logger.go @@ -0,0 +1,18 @@ +package main + +import ( + "log" + "net/http" +) + +type Logger struct { + h http.Handler +} + +func NewLogger(h http.Handler) (l *Logger) { + return &Logger{h: h} +} +func (l *Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s%s", r.RemoteAddr, r.Method, r.Host, r.URL) + l.h.ServeHTTP(w, r) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9d9b807 --- /dev/null +++ b/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "bufio" + "bytes" + "crypto/md5" + "embed" + "encoding/json" + "flag" + "fmt" + "html/template" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "strings" +) + +var ( + templates *template.Template + //go:embed templates + fs embed.FS + config *Config +) + +func main() { + var configPath string + var rescan bool + flag.StringVar(&configPath, "config", "config.yaml", "Path to the configuration file") + flag.BoolVar(&rescan, "rescan", false, "Rescan the data direcory") + flag.Parse() + var err error + config, err = NewConfigParseFile(configPath) + if err != nil { + log.Fatal(err.Error()) + } + + if rescan { + rescanFiles() + return + } + + { + entry, err := os.Stat(path.Join(config.DataDirectory, "tmp")) + if os.IsNotExist(err) { + err = os.Mkdir(path.Join(config.DataDirectory, "tmp"), os.ModeDir) + if err != nil { + log.Printf("FATAL: can't create tmp directory: %s", err.Error()) + os.Exit(-1) + } + } + if err != nil { + log.Printf("FATAL: can't stat tmp directory: %s", err.Error()) + os.Exit(-2) + } + if !entry.IsDir() { + log.Printf("FATAL: tmp path isn't directory") + os.Exit(-3) + } + } + + templates = template.Must(template.ParseFS(fs, "templates/*")) + log.Printf("Loaded templates: %s", templates.DefinedTemplates()) + videoFS := http.FileServer(http.Dir(config.DataDirectory)) + adminMux := NewAdminApi() + mux := http.NewServeMux() + mux.HandleFunc("/", indexHandler) + mux.HandleFunc("/video/index.json", videoIndexHandler) + mux.Handle("/video/", videoFS) + mux.Handle("/admin/", http.StripPrefix("/admin", adminMux)) + if len(config.ApplicationPrefix) > 0 { + http.ListenAndServe(config.ListenAddress, http.StripPrefix(config.ApplicationPrefix, NewLogger(mux))) + } else { + http.ListenAndServe(config.ListenAddress, NewLogger(mux)) + } +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + var v struct { + ApplicationName string + } + v.ApplicationName = config.ApplicationName + err := executeTemplate(w, "index.html", &v) + if err != nil { + log.Printf("Error executing temlate \"index.html\": %s", err.Error()) + } +} + +func videoIndexHandler(w http.ResponseWriter, r *http.Request) { + var index VideoIndex + index.Success = true + index.Generation = config.Generation + dir, err := os.ReadDir(path.Join(config.DataDirectory, "video")) + if err != nil { + log.Printf("can't red video directory: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + for _, entry := range dir { + if entry.Type().IsRegular() && path.Ext(entry.Name()) == ".json" && entry.Name() != "index.json" { + var v Video + rawInfo, err := ioutil.ReadFile(path.Join(config.DataDirectory, "video", entry.Name())) + if err != nil { + log.Printf("can't read info for %s: %s", entry.Name(), err.Error()) + continue + } + err = json.Unmarshal(rawInfo, &v) + if err != nil { + log.Printf("can't read info for %s: %s", entry.Name(), err.Error()) + continue + } + index.Videos = append(index.Videos, v) + index.Count++ + } + } + indexRaw, err := json.Marshal(index) + if err != nil { + log.Printf("can't marshal index: %s", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + w.Write(indexRaw) +} + +func executeTemplate(w http.ResponseWriter, name string, data interface{}) (err error) { + var buf bytes.Buffer + bufferRW := bufio.NewReadWriter(bufio.NewReader(&buf), bufio.NewWriter(&buf)) + err = templates.ExecuteTemplate(bufferRW, name, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + bufferRW.Flush() + w.WriteHeader(http.StatusOK) + io.Copy(w, bufferRW) + return +} + +func rescanFiles() { + var index VideoIndex + index.Generation = config.Generation + index.Success = true + dirPath := path.Join(config.DataDirectory, "video") + dir, err := os.ReadDir(dirPath) + if err != nil { + log.Printf("can't read data directory: %s", err.Error()) + } + for _, entry := range dir { + if entry.Type().IsRegular() { + if !strings.HasSuffix(entry.Name(), ".json") { + var v Video + file, err := os.Open(path.Join(dirPath, entry.Name())) + if err != nil { + log.Printf("can't read info file: %s", err.Error()) + } + v.Name = entry.Name() + hashsum := md5.New() + _, err = io.Copy(hashsum, file) + if err != nil { + log.Printf("can't read info file: %s", err.Error()) + } + hashstring := fmt.Sprintf("%x", hashsum.Sum(nil)) + v.ID = hashstring + ext := path.Ext(entry.Name()) + err = os.Rename(path.Join(dirPath, entry.Name()), path.Join(dirPath, string(hashstring)+ext)) + if err != nil { + log.Printf("can't move file: %s", err.Error()) + } + if info, _ := os.Stat(path.Join(dirPath, v.ID+".json")); info == nil { + rawInfo, err := json.Marshal(&v) + if err != nil { + log.Printf("can't write info: %s", err.Error()) + } + err = ioutil.WriteFile(path.Join(dirPath, v.ID+".json"), rawInfo, os.ModePerm) + if err != nil { + log.Printf("can't write info: %s", err.Error()) + } + } + index.Videos = append(index.Videos, v) + index.Count++ + } + } + } + rawIndex, err := json.Marshal(&index) + if err != nil { + log.Fatalf("can't create index: %s", err) + } + err = ioutil.WriteFile(path.Join(dirPath, "index.json"), rawIndex, os.ModePerm) + if err != nil { + log.Fatalf("can't create index: %s", err) + } +} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..dda8cb9 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,41 @@ +{{define "admin.html"}} + + + + {{.ApplicationName}} | Admin page + + + +
+

Current playlist

+ + + + + + + + + + + {{range .Videos}} + + + + + + {{end}} + + +

Add new:

+ +
namesizepreview
{{html .DisplayName}}{{html .Size}}
+
+ + +{{end}} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..cc862b7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,100 @@ +{{define "index.html"}} + + + + {{.ApplicationName}} + + + + + +
+ +
+ + +{{end}} \ No newline at end of file diff --git a/video.go b/video.go new file mode 100644 index 0000000..79c34bb --- /dev/null +++ b/video.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" +) + +type Video struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + ID string `json:"id"` + Size int64 `json:"size"` +} + +type VideoIndex struct { + Success bool `json:"success"` + Generation int `json:"generation"` + Count int `json:"count"` + Videos []Video `json:"videos"` +} + +func readVideoInfo(filepath string) (video Video, err error) { + if !strings.HasSuffix(filepath, ".json") { + filepath = filepath + ".json" + } + infoRaw, err := ioutil.ReadFile(filepath) + if err != nil { + err = fmt.Errorf("can't read infofile \"%s\": %s", filepath, err.Error()) + return + } + err = json.Unmarshal(infoRaw, &video) + if err != nil { + err = fmt.Errorf("can't read infofile \"%s\": %s", filepath, err.Error()) + return + } + return +}