diff --git a/config/config.go b/config/config.go index cf03d74..0fc3784 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" daulog "github.com/tardisx/discord-auto-upload/log" @@ -21,62 +22,115 @@ type ConfigV1 struct { Exclude string } -type ConfigV2Watcher struct { +type Watcher struct { WebHookURL string Path string Username string NoWatermark bool - Exclude string + Exclude []string } type ConfigV2 struct { WatchInterval int Version int - Watchers []ConfigV2Watcher + Port int + Watchers []Watcher } -var Config ConfigV2 -var configPath string +type ConfigService struct { + Config ConfigV2 + ConfigFilename string +} -func Init() { - configPath = defaultConfigPath() +func DefaultConfigService() *ConfigService { + c := ConfigService{ + ConfigFilename: defaultConfigPath(), + } + return &c } // LoadOrInit loads the current configuration from the config file, or creates // a new config file if none exists. -func LoadOrInit() error { - daulog.SendLog(fmt.Sprintf("Trying to load config from %s", configPath), daulog.LogTypeDebug) - _, err := os.Stat(configPath) +func (c *ConfigService) LoadOrInit() error { + daulog.SendLog(fmt.Sprintf("Trying to load config from %s\n", c.ConfigFilename), daulog.LogTypeDebug) + _, err := os.Stat(c.ConfigFilename) if os.IsNotExist(err) { daulog.SendLog("NOTE: No config file, writing out sample configuration", daulog.LogTypeInfo) daulog.SendLog("You need to set the configuration via the web interface", daulog.LogTypeInfo) - Config.Version = 2 - Config.WatchInterval = 10 - return SaveConfig() + c.Config = *DefaultConfig() + return c.Save() } else { - return LoadConfig() + return c.Load() } } -// LoadConfig will load the configuration from a known-to-exist config file. -func LoadConfig() error { - data, err := ioutil.ReadFile(configPath) - if err != nil { - return fmt.Errorf("cannot read config file %s: %s", configPath, err.Error()) +func DefaultConfig() *ConfigV2 { + c := ConfigV2{} + c.Version = 2 + c.WatchInterval = 10 + c.Port = 9090 + w := Watcher{ + WebHookURL: "abcedf", + Path: "/Users/justin/tmp", + Username: "", + NoWatermark: false, + Exclude: []string{}, } - err = json.Unmarshal([]byte(data), &Config) + c.Watchers = []Watcher{w} + return &c +} + +// Load will load the configuration from a known-to-exist config file. +func (c *ConfigService) Load() error { + fmt.Printf("Loading from %s\n\n", c.ConfigFilename) + + data, err := ioutil.ReadFile(c.ConfigFilename) if err != nil { - return fmt.Errorf("cannot decode config file %s: %s", configPath, err.Error()) + return fmt.Errorf("cannot read config file %s: %s", c.ConfigFilename, err.Error()) } + err = json.Unmarshal([]byte(data), &c.Config) + if err != nil { + return fmt.Errorf("cannot decode config file %s: %s", c.ConfigFilename, err.Error()) + } + + fmt.Printf("Got config: %#v", c.Config) + + // Version 0 predates config migrations + if c.Config.Version == 0 { + // need to migrate this + daulog.SendLog("Migrating config to V2", daulog.LogTypeInfo) + + configV1 := ConfigV1{} + err = json.Unmarshal([]byte(data), &configV1) + if err != nil { + return fmt.Errorf("cannot decode legacy config file as v1 %s: %s", c.ConfigFilename, err.Error()) + } + + // copy stuff across + c.Config.Version = 2 + c.Config.WatchInterval = configV1.Watch + c.Config.Port = 9090 // this never used to be configurable + + onlyWatcher := Watcher{ + WebHookURL: configV1.WebHookURL, + Path: configV1.Path, + Username: configV1.Username, + NoWatermark: configV1.NoWatermark, + Exclude: strings.Split(configV1.Exclude, " "), + } + + c.Config.Watchers = []Watcher{onlyWatcher} + } + return nil } -func SaveConfig() error { +func (c *ConfigService) Save() error { daulog.SendLog("saving configuration", daulog.LogTypeInfo) - jsonString, _ := json.Marshal(Config) - err := ioutil.WriteFile(configPath, jsonString, os.ModePerm) + jsonString, _ := json.Marshal(c.Config) + err := ioutil.WriteFile(c.ConfigFilename, jsonString, os.ModePerm) if err != nil { - return fmt.Errorf("cannot save config %s: %s", configPath, err.Error()) + return fmt.Errorf("cannot save config %s: %s", c.ConfigFilename, err.Error()) } return nil } diff --git a/config/config_test.go b/config/config_test.go index 2c2ca62..86eb38c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -7,39 +7,76 @@ import ( ) func TestNoConfig(t *testing.T) { - if Config.Version != 0 { - t.Error("not 0 empty config") - } + c := ConfigService{} - configPath = emptyTempFile() - os.Remove(configPath) + c.ConfigFilename = emptyTempFile() + os.Remove(c.ConfigFilename) + defer os.Remove(c.ConfigFilename) // because we are about to create it - err := LoadOrInit() + err := c.LoadOrInit() if err != nil { t.Errorf("unexpected failure from load: %s", err) } - if Config.Version != 2 { + if c.Config.Version != 2 { t.Error("not version 2 starting config") } - if fileSize(configPath) < 40 { - t.Errorf("File is too small %d bytes", fileSize(configPath)) + if fileSize(c.ConfigFilename) < 40 { + t.Errorf("File is too small %d bytes", fileSize(c.ConfigFilename)) } - os.Remove(configPath) } func TestEmptyFileConfig(t *testing.T) { + c := ConfigService{} - configPath = emptyTempFile() + c.ConfigFilename = emptyTempFile() + defer os.Remove(c.ConfigFilename) - err := LoadOrInit() + err := c.LoadOrInit() if err == nil { t.Error("unexpected success from LoadOrInit()") } - os.Remove(configPath) +} + +func TestMigrateFromV1toV2(t *testing.T) { + c := ConfigService{} + + c.ConfigFilename = v1Config() + err := c.LoadOrInit() + if err != nil { + t.Error("unexpected error from LoadOrInit()") + } + if c.Config.Version != 2 { + t.Errorf("Version %d not 2", c.Config.Version) + } + + if len(c.Config.Watchers) != 1 { + t.Error("wrong amount of watchers") + } + + if c.Config.Watchers[0].Path != "/private/tmp" { + t.Error("Wrong path") + } + if c.Config.WatchInterval != 69 { + t.Error("Wrong watch interval") + } + if c.Config.Port != 9090 { + t.Error("Wrong port") + } +} + +func v1Config() string { + f, err := ioutil.TempFile("", "dautest-*") + if err != nil { + panic(err) + } + config := `{"WebHookURL":"https://discord.com/api/webhooks/abc123","Path":"/private/tmp","Watch":69,"Username":"abcdedf","NoWatermark":true,"Exclude":"ab cd ef"}` + f.Write([]byte(config)) + defer f.Close() + return f.Name() } func emptyTempFile() string { diff --git a/dau.go b/dau.go index a8bb803..c366008 100644 --- a/dau.go +++ b/dau.go @@ -2,7 +2,9 @@ package main import ( "encoding/json" + "flag" "fmt" + "io/fs" "io/ioutil" "log" "net/http" @@ -15,59 +17,132 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/pborman/getopt" - // "github.com/skratchdot/open-golang/open" "github.com/tardisx/discord-auto-upload/config" daulog "github.com/tardisx/discord-auto-upload/log" - "github.com/tardisx/discord-auto-upload/uploads" + "github.com/tardisx/discord-auto-upload/upload" + + // "github.com/tardisx/discord-auto-upload/upload" "github.com/tardisx/discord-auto-upload/version" "github.com/tardisx/discord-auto-upload/web" ) -var lastCheck = time.Now() -var newLastCheck = time.Now() +type watch struct { + lastCheck time.Time + newLastCheck time.Time + config config.Watcher + uploader upload.Uploader +} func main() { parseOptions() + // grab the config + config := config.DefaultConfigService() + config.LoadOrInit() + + // create the uploader + up := upload.Uploader{} + // log.Print("Opening web browser") // open.Start("http://localhost:9090") + web := web.WebService{Config: *config} web.StartWebServer() - checkUpdates() + go func() { checkUpdates() }() - daulog.SendLog(fmt.Sprintf("Waiting for images to appear in %s", config.Config.Path), daulog.LogTypeInfo) - // wander the path, forever + // create the watchers + + log.Printf("Conf: %#v", config.Config) + + for _, c := range config.Config.Watchers { + log.Printf("Creating watcher for %v", c) + watcher := watch{uploader: up, lastCheck: time.Now(), newLastCheck: time.Now(), config: c} + go watcher.Watch(config.Config.WatchInterval) + } + + select {} +} + +func (w *watch) Watch(interval int) { for { - if checkPath(config.Config.Path) { - err := filepath.Walk(config.Config.Path, - func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err) }) - if err != nil { - log.Fatal("could not watch path", err) - } - lastCheck = newLastCheck + newFiles := w.ProcessNewFiles() + for _, f := range newFiles { + w.uploader.AddFile(f, w.config) } - daulog.SendLog(fmt.Sprintf("sleeping for %ds before next check of %s", config.Config.Watch, config.Config.Path), daulog.LogTypeDebug) - time.Sleep(time.Duration(config.Config.Watch) * time.Second) + // upload them + w.uploader.Upload() + daulog.SendLog(fmt.Sprintf("sleeping for %ds before next check of %s", interval, w.config.Path), daulog.LogTypeDebug) + time.Sleep(time.Duration(interval) * time.Second) } } -func checkPath(path string) bool { - src, err := os.Stat(path) +// ProcessNewFiles returns an array of new files that have appeared since +// the last time ProcessNewFiles was run. +func (w *watch) ProcessNewFiles() []string { + var newFiles []string + // check the path each time around, in case it goes away or something + if w.checkPath() { + // walk the path + err := filepath.WalkDir(w.config.Path, + func(path string, d fs.DirEntry, err error) error { + return w.checkFile(path, &newFiles) + }) + + if err != nil { + log.Fatal("could not watch path", err) + } + w.lastCheck = w.newLastCheck + } + return newFiles +} + +// checkPath makes sure the path exists, and is a directory. +// It logs errors if there are problems, and returns false +func (w *watch) checkPath() bool { + src, err := os.Stat(w.config.Path) if err != nil { - log.Printf("Problem with path '%s': %s", path, err) + log.Printf("Problem with path '%s': %s", w.config.Path, err) return false } if !src.IsDir() { - log.Printf("Problem with path '%s': is not a directory", path) + log.Printf("Problem with path '%s': is not a directory", w.config.Path) return false } return true } +// checkFile checks if a file is eligible, first looking at extension (to +// avoid statting files uselessly) then modification times. +// If the file is eligble, and new enough to care we add it to the passed in +// array of files +func (w *watch) checkFile(path string, found *[]string) error { + log.Printf("Considering %s", path) + + extension := strings.ToLower(filepath.Ext(path)) + + if !(extension == ".png" || extension == ".jpg" || extension == ".gif") { + return nil + } + + fi, err := os.Stat(path) + if err != nil { + return err + } + + if fi.ModTime().After(w.lastCheck) && fi.Mode().IsRegular() { + *found = append(*found, path) + } + + if w.newLastCheck.Before(fi.ModTime()) { + w.newLastCheck = fi.ModTime() + } + + return nil +} + func checkUpdates() { type GithubRelease struct { @@ -115,61 +190,14 @@ func checkUpdates() { } func parseOptions() { + var versionFlag bool + flag.BoolVar(&versionFlag, "version", false, "show version") + flag.Parse() - // Declare the flags to be used - helpFlag := getopt.BoolLong("help", 'h', "help") - versionFlag := getopt.BoolLong("version", 'v', "show version") - getopt.SetParameters("") - - getopt.Parse() - - if *helpFlag { - getopt.PrintUsage(os.Stderr) - os.Exit(0) - } - - if *versionFlag { + if versionFlag { fmt.Println("dau - https://github.com/tardisx/discord-auto-upload") fmt.Printf("Version: %s\n", version.CurrentVersion) os.Exit(0) } - // grab the config - config.LoadOrInit() -} - -func checkFile(path string, f os.FileInfo, err error) error { - if f.ModTime().After(lastCheck) && f.Mode().IsRegular() { - - if fileEligible(path) { - // process file - processFile(path) - } - - if newLastCheck.Before(f.ModTime()) { - newLastCheck = f.ModTime() - } - } - - return nil -} - -func fileEligible(file string) bool { - - if config.Config.Exclude != "" && strings.Contains(file, config.Config.Exclude) { - return false - } - - extension := strings.ToLower(filepath.Ext(file)) - if extension == ".png" || extension == ".jpg" || extension == ".gif" { - return true - } - - return false -} - -func processFile(file string) { - - daulog.SendLog("Sending to uploader", daulog.LogTypeInfo) - uploads.AddFile(file) } diff --git a/log/log.go b/log/log.go index bb20740..b08e5ad 100644 --- a/log/log.go +++ b/log/log.go @@ -1,6 +1,7 @@ package log import ( + "log" "time" ) @@ -42,4 +43,5 @@ func SendLog(entry string, entryType LogEntryType) { Entry: entry, Type: entryType, } + log.Printf(entry) } diff --git a/uploads/uploads.go b/upload/upload.go similarity index 72% rename from uploads/uploads.go rename to upload/upload.go index 6be229d..9457ae9 100644 --- a/uploads/uploads.go +++ b/upload/upload.go @@ -1,6 +1,6 @@ -// The uploads pacakge encapsulates dealing with file uploads to -// discord -package uploads +// Package upload encapsulates prepping an image for sending to discord, +// and actually uploading it there. +package upload import ( "bytes" @@ -22,48 +22,70 @@ import ( "golang.org/x/image/font/inconsolata" ) -type Upload struct { - Uploaded bool `json:"uploaded"` // has this file been uploaded to discord - UploadedAt time.Time `json:"uploaded_at"` - originalFilename string // path on the local disk - mungedFilename string // post-watermark - Url string `json:"url"` // url on the discord CDN - Width int `json:"width"` - Height int `json:"height"` +type Uploader struct { + Uploads []*Upload } -var Uploads []*Upload +type Upload struct { + Uploaded bool `json:"uploaded"` // has this file been uploaded to discord + UploadedAt time.Time `json:"uploaded_at"` -func AddFile(file string) { + originalFilename string // path on the local disk + filenameToUpload string // post-watermark, or just original if unwatermarked + + webhookURL string + + watermark bool // should watermark + + usernameOverride string + + Url string `json:"url"` // url on the discord CDN + + Width int `json:"width"` + Height int `json:"height"` +} + +func (u *Uploader) AddFile(file string, conf config.Watcher) { thisUpload := Upload{ Uploaded: false, originalFilename: file, + watermark: !conf.NoWatermark, + webhookURL: conf.WebHookURL, + usernameOverride: conf.Username, } - Uploads = append(Uploads, &thisUpload) - - ProcessUpload(&thisUpload) + u.Uploads = append(u.Uploads, &thisUpload) } -func ProcessUpload(up *Upload) { - - file := up.originalFilename - - if !config.Config.NoWatermark { - daulog.SendLog("Copying to temp location and watermarking ", daulog.LogTypeInfo) - file = mungeFile(file) - up.mungedFilename = file +// Upload uploads any files that have not yet been uploaded +func (u *Uploader) Upload() { + for _, upload := range u.Uploads { + if !upload.Uploaded { + upload.processUpload() + } } +} - if config.Config.WebHookURL == "" { +func (u *Upload) processUpload() { + + // file := u.originalFilename + + if u.webhookURL == "" { daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError) return } + if u.watermark { + daulog.SendLog("Watermarking", daulog.LogTypeInfo) + u.applyWatermark() + } else { + u.filenameToUpload = u.originalFilename + } + extraParams := map[string]string{} - if config.Config.Username != "" { - daulog.SendLog("Overriding username with "+config.Config.Username, daulog.LogTypeInfo) - extraParams["username"] = config.Config.Username + if u.usernameOverride != "" { + daulog.SendLog("Overriding username with "+u.usernameOverride, daulog.LogTypeInfo) + extraParams["username"] = u.usernameOverride } type DiscordAPIResponseAttachment struct { @@ -83,7 +105,7 @@ func ProcessUpload(up *Upload) { var retriesRemaining = 5 for retriesRemaining > 0 { - request, err := newfileUploadRequest(config.Config.WebHookURL, extraParams, "file", file) + request, err := newfileUploadRequest(u.webhookURL, extraParams, "file", u.filenameToUpload) if err != nil { log.Fatal(err) } @@ -145,19 +167,19 @@ func ProcessUpload(up *Upload) { daulog.SendLog(fmt.Sprintf("Uploaded to %s %dx%d", a.URL, a.Width, a.Height), daulog.LogTypeInfo) daulog.SendLog(fmt.Sprintf("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate), daulog.LogTypeInfo) - up.Url = a.URL - up.Uploaded = true - up.Width = a.Width - up.Height = a.Height - up.UploadedAt = time.Now() + u.Url = a.URL + u.Uploaded = true + u.Width = a.Width + u.Height = a.Height + u.UploadedAt = time.Now() break } } - if !config.Config.NoWatermark { - daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", file), daulog.LogTypeDebug) - os.Remove(file) + if u.watermark { + daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", u.filenameToUpload), daulog.LogTypeDebug) + os.Remove(u.filenameToUpload) } if retriesRemaining == 0 { @@ -196,9 +218,9 @@ func newfileUploadRequest(uri string, params map[string]string, paramName, path return req, err } -func mungeFile(path string) string { +func (u *Upload) applyWatermark() { - reader, err := os.Open(path) + reader, err := os.Open(u.originalFilename) if err != nil { log.Fatal(err) } @@ -206,7 +228,10 @@ func mungeFile(path string) string { im, _, err := image.Decode(reader) if err != nil { - log.Fatal(err) + log.Printf("Cannot decode image: %v - skipping watermarking", err) + u.watermark = false + u.filenameToUpload = u.originalFilename + return } bounds := im.Bounds() // var S float64 = float64(bounds.Max.X) @@ -236,7 +261,7 @@ func mungeFile(path string) string { actualName := tempfile.Name() + ".png" dc.SavePNG(actualName) - return actualName + u.filenameToUpload = actualName } func sleepForRetries(retry int) { diff --git a/web/data/config.html b/web/data/config.html index a687ef8..3059dc7 100644 --- a/web/data/config.html +++ b/web/data/config.html @@ -1,159 +1,153 @@ {{ define "content" }} -
-

Config

-

Discord-auto-upload configuration

-

How to find your discord webhook

+
+

Config

+ + + +
+ +

Configuration changes are not made until the Save button is pressed + at the bottom of this page. +

+ +

global configuration

+ +

The server port dictates which TCP port the web server listens on. + If you change this number you will need to restart. +

+ +

The Watch Interval is how often new files will be discovered by your + watchers (configured below).

+ +
+
+ Server port +
+
+ + +
+
+ +
+
+ Watch interval +
+
+ + +
+
- -
-
- Discord WebHook URL -
-
- - -
-
- -
-
-
+

watcher configuration

-
-
-
- Bot username (optional) -
-
- - -
-
- -
-
-
+

You may configure one or more watchers. Each watcher watches a + single directory (and all subdirectories) and when a new image file + is found it uploads it to the specified channel via the webhook URL. +

-
-
-
- Directory to watch -
-
- - -
-
- -
-
-
+

+ Click here for information on how to find your discord webhook URL.

-
-
-
- Period between filesystem checks (seconds) -
-
- - -
-
- -
-
-
+

You may also specify a username for the bot to masquerade as. This is a cosmetic + change only, and does not hide the uploaders actual identity. +

-
-
-
- Do not watermark images -
-
-
- - - 😭 -
-
-
- -
-
-
+ + +
+ +
+ +
+ + +
+ + + + + +
{{ end }} {{ define "js" }} + -{{ end }} +{{ end }} \ No newline at end of file diff --git a/web/server.go b/web/server.go index 97040d3..c87c729 100644 --- a/web/server.go +++ b/web/server.go @@ -5,44 +5,32 @@ import ( "encoding/json" "fmt" "html/template" + "io/ioutil" "log" "mime" "net/http" - "os" "path/filepath" - "strconv" "strings" "github.com/tardisx/discord-auto-upload/config" daulog "github.com/tardisx/discord-auto-upload/log" - "github.com/tardisx/discord-auto-upload/uploads" "github.com/tardisx/discord-auto-upload/version" ) +type WebService struct { + Config config.ConfigService +} + //go:embed data var webFS embed.FS // DAUWebServer - stuff for the web server type DAUWebServer struct { - ConfigChange chan int + // ConfigChange chan int + } -type valueStringResponse struct { - Success bool `json:"success"` - Value string `json:"value"` -} - -type valueBooleanResponse struct { - Success bool `json:"success"` - Value bool `json:"value"` -} - -type errorResponse struct { - Success bool `json:"success"` - Error string `json:"error"` -} - -func getStatic(w http.ResponseWriter, r *http.Request) { +func (ws *WebService) getStatic(w http.ResponseWriter, r *http.Request) { path := r.URL.Path path = strings.TrimLeft(path, "/") @@ -52,8 +40,7 @@ func getStatic(w http.ResponseWriter, r *http.Request) { extension := filepath.Ext(string(path)) - if extension == ".html" { - + if extension == ".html" { // html file t, err := template.ParseFS(webFS, "data/wrapper.tmpl", "data/"+path) if err != nil { log.Printf("when fetching: %s got: %s", path, err) @@ -78,7 +65,7 @@ func getStatic(w http.ResponseWriter, r *http.Request) { panic(err) } return - } else { + } else { // anything else otherStatic, err := webFS.ReadFile("data/" + path) if err != nil { @@ -96,200 +83,7 @@ func getStatic(w http.ResponseWriter, r *http.Request) { } -// TODO there should be locks around all these config accesses -func getSetWebhook(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method == "GET" { - getResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL} - - // I can't see any way this will fail - js, _ := json.Marshal(getResponse) - w.Write(js) - } else if r.Method == "POST" { - err := r.ParseForm() - if err != nil { - log.Fatal(err) - } - config.Config.WebHookURL = r.PostForm.Get("value") - config.SaveConfig() - postResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL} - - js, _ := json.Marshal(postResponse) - w.Write(js) - } -} - -// TODO there should be locks around all these config accesses -func getSetUsername(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method == "GET" { - getResponse := valueStringResponse{Success: true, Value: config.Config.Username} - - // I can't see any way this will fail - js, _ := json.Marshal(getResponse) - w.Write(js) - } else if r.Method == "POST" { - err := r.ParseForm() - if err != nil { - log.Fatal(err) - } - config.Config.Username = r.PostForm.Get("value") - config.SaveConfig() - - postResponse := valueStringResponse{Success: true, Value: config.Config.Username} - - js, _ := json.Marshal(postResponse) - w.Write(js) - } -} - -func getSetWatch(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method == "GET" { - getResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)} - - // I can't see any way this will fail - js, _ := json.Marshal(getResponse) - w.Write(js) - } else if r.Method == "POST" { - err := r.ParseForm() - if err != nil { - log.Fatal(err) - } - - i, err := strconv.Atoi(r.PostForm.Get("value")) - - if err != nil { - response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for watch: %v", err)} - js, _ := json.Marshal(response) - w.Write(js) - return - } - - if i < 1 { - response := errorResponse{Success: false, Error: "must be > 0"} - js, _ := json.Marshal(response) - w.Write(js) - return - } - - config.Config.Watch = i - config.SaveConfig() - - postResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)} - - js, _ := json.Marshal(postResponse) - w.Write(js) - } -} - -func getSetNoWatermark(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method == "GET" { - getResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark} - - // I can't see any way this will fail - js, _ := json.Marshal(getResponse) - w.Write(js) - } else if r.Method == "POST" { - err := r.ParseForm() - if err != nil { - log.Fatal(err) - } - - v := r.PostForm.Get("value") - - if v != "0" && v != "1" { - response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for nowatermark: %v", err)} - js, _ := json.Marshal(response) - w.Write(js) - return - } - - if v == "0" { - config.Config.NoWatermark = false - } else { - config.Config.NoWatermark = true - } - config.SaveConfig() - - postResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark} - - js, _ := json.Marshal(postResponse) - w.Write(js) - } -} - -func getSetDirectory(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method == "GET" { - getResponse := valueStringResponse{Success: true, Value: config.Config.Path} - - // I can't see any way this will fail - js, _ := json.Marshal(getResponse) - w.Write(js) - } else if r.Method == "POST" { - err := r.ParseForm() - if err != nil { - log.Fatal(err) - } - newPath := r.PostForm.Get("value") - - // sanity check this path - stat, err := os.Stat(newPath) - if os.IsNotExist(err) { - // not exist - response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - does not exist", newPath)} - js, _ := json.Marshal(response) - w.Write(js) - return - } else if !stat.IsDir() { - // not a directory - response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - is not a directory", newPath)} - js, _ := json.Marshal(response) - w.Write(js) - return - } - - config.Config.Path = newPath - config.SaveConfig() - - postResponse := valueStringResponse{Success: true, Value: config.Config.Path} - - js, _ := json.Marshal(postResponse) - w.Write(js) - } -} - -func getSetExclude(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method == "GET" { - getResponse := valueStringResponse{Success: true, Value: config.Config.Exclude} - // I can't see any way this will fail - js, _ := json.Marshal(getResponse) - w.Write(js) - } else if r.Method == "POST" { - err := r.ParseForm() - if err != nil { - log.Fatal(err) - } - config.Config.Exclude = r.PostForm.Get("value") - config.SaveConfig() - - postResponse := valueStringResponse{Success: true, Value: config.Config.Exclude} - - js, _ := json.Marshal(postResponse) - w.Write(js) - } -} - -func getLogs(w http.ResponseWriter, r *http.Request) { +func (ws *WebService) getLogs(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") showDebug := false @@ -312,29 +106,50 @@ func getLogs(w http.ResponseWriter, r *http.Request) { w.Write([]byte(text)) } -func getUploads(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - ups := uploads.Uploads - text, _ := json.Marshal(ups) - w.Write([]byte(text)) +func (ws *WebService) handleConfig(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + newConfig := config.ConfigV2{} + + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + if err != nil { + w.Write([]byte("bad body")) + return + } + err = json.Unmarshal(b, &newConfig) + if err != nil { + w.Write([]byte("bad data")) + log.Printf("%s", err) + return + } + ws.Config.Config = newConfig + ws.Config.Save() + + } + + b, _ := json.Marshal(ws.Config.Config) + w.Write(b) } -func StartWebServer() { +// func getUploads(w http.ResponseWriter, r *http.Request) { +// w.Header().Set("Content-Type", "application/json") +// ups := uploads.Uploads +// text, _ := json.Marshal(ups) +// w.Write([]byte(text)) +// } - http.HandleFunc("/", getStatic) - http.HandleFunc("/rest/config/webhook", getSetWebhook) - http.HandleFunc("/rest/config/username", getSetUsername) - http.HandleFunc("/rest/config/watch", getSetWatch) - http.HandleFunc("/rest/config/nowatermark", getSetNoWatermark) - http.HandleFunc("/rest/config/directory", getSetDirectory) - http.HandleFunc("/rest/config/exclude", getSetExclude) +func (ws *WebService) StartWebServer() { - http.HandleFunc("/rest/logs", getLogs) - http.HandleFunc("/rest/uploads", getUploads) + http.HandleFunc("/", ws.getStatic) + + http.HandleFunc("/rest/logs", ws.getLogs) + // http.HandleFunc("/rest/uploads", getUploads) + http.HandleFunc("/rest/config", ws.handleConfig) go func() { - log.Print("Starting web server on http://localhost:9090") - err := http.ListenAndServe(":9090", nil) // set listen port + listen := fmt.Sprintf(":%d", ws.Config.Config.Port) + log.Print("Starting web server on http://localhost%s", listen) + err := http.ListenAndServe(listen, nil) // set listen port if err != nil { log.Fatal("ListenAndServe: ", err) } diff --git a/web/server_test.go b/web/server_test.go index 5c61b31..381aa6f 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -6,15 +6,19 @@ import ( "net/http/httptest" "strings" "testing" + + "github.com/tardisx/discord-auto-upload/config" ) func TestHome(t *testing.T) { + s := WebService{} req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() - getStatic(w, req) + s.getStatic(w, req) res := w.Result() - defer res.Body.Close() + data, err := ioutil.ReadAll(res.Body) + if err != nil { t.Errorf("expected error to be nil got %v", err) } @@ -28,6 +32,7 @@ func TestHome(t *testing.T) { } func TestNotFound(t *testing.T) { + s := WebService{} notFounds := []string{ "/abc.html", "/foo.html", "/foo.html", "/../foo.html", @@ -38,8 +43,9 @@ func TestNotFound(t *testing.T) { req := httptest.NewRequest(http.MethodGet, nf, nil) w := httptest.NewRecorder() - getStatic(w, req) + s.getStatic(w, req) res := w.Result() + defer res.Body.Close() b, err := ioutil.ReadAll(res.Body) if err != nil { @@ -54,3 +60,25 @@ func TestNotFound(t *testing.T) { } } } + +func TestGetConfig(t *testing.T) { + conf := config.DefaultConfigService() + conf.Config = *config.DefaultConfig() + s := WebService{Config: *conf} + + req := httptest.NewRequest(http.MethodGet, "/rest/config", nil) + w := httptest.NewRecorder() + s.handleConfig(w, req) + res := w.Result() + defer res.Body.Close() + + b, err := ioutil.ReadAll(res.Body) + + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + + if string(b) != `{"WatchInterval":10,"Version":2,"Watchers":[{"WebHookURL":"abcedf","Path":"/Users/justin/tmp","Username":"","NoWatermark":false,"Exclude":[]}]}` { + t.Errorf("Got unexpected response %v", string(b)) + } +}