Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 896946c751 | |||
| bc2e88786c | |||
| 7c12b04700 | |||
| 2af34fddc8 | |||
| 093327088f | |||
| 369abfbbd3 | |||
| 9765c6909b | |||
| 536657e0e8 | |||
| e049160cfc | |||
| 111a33bc8a | |||
| 2f5bb2ff36 | |||
| 563de29fcf | |||
| bebc161256 | |||
| 4118866f7b | |||
| 35e5a00888 | |||
| f64240e135 | |||
| a099d738fc | |||
| e110fc307f | |||
| 0a72d6e2dd | |||
| ced209a7db | |||
| 1228920004 | |||
| 2042c7520d | |||
| 42fb7a2003 | |||
| e1f18b104f | |||
| 5adf81fcf6 | |||
| 726ae9a5aa | |||
| 6fa6c34ccb | |||
| 71c70ce965 | |||
| 02b26e60a9 | |||
| a9df878024 | |||
| 460fcf5523 | |||
| 672fd83f27 | |||
| c3f1813f6e |
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"daulog",
|
||||
"inconsolata"
|
||||
]
|
||||
}
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.12.0] - 2020-04-03
|
||||
|
||||
- Break upload page into pending/current/complete sections
|
||||
- Add preview thumbnails for each upload
|
||||
- Add feature to hold an image for upload, so the user can
|
||||
choose to upload it or not
|
||||
- Add simple image editor to add text captions
|
||||
- Discord server created: https://discord.gg/eErG9sntbZ
|
||||
|
||||
## [v0.11.2] - 2021-10-19
|
||||
|
||||
- Really fix the bug where too large attachments keep retrying
|
||||
- Fix tests on Windows
|
||||
|
||||
## [v0.11.1] - 2021-10-11
|
||||
|
||||
- Improve logging and error handling
|
||||
@@ -66,4 +80,4 @@ Add --exclude option to avoid uploading files in thumbnail directories
|
||||
|
||||
## [0.1.0] - 2017-02-16
|
||||
|
||||
Initial release
|
||||
Initial release
|
||||
|
||||
54
README.md
54
README.md
@@ -8,6 +8,8 @@ This program automatically uploads new screenshots that appear in a folder on yo
|
||||
|
||||
Point it at your Steam screenshot folder, or similar, and shortly after you hit your screenshot hotkey the screenshot will appear in your discord chat.
|
||||
|
||||
Need help? Join our discord: https://discord.gg/eErG9sntbZ
|
||||
|
||||
## What you'll need
|
||||
|
||||
* A folder where screenshots are stored
|
||||
@@ -41,39 +43,51 @@ Thus, you do not have to worry about pointing `dau` at a directory full of image
|
||||
|
||||
## Configuration options
|
||||
|
||||
See the web interface at http://localhost:9090 to configure `dau`.
|
||||
See the web interface at http://localhost:9090 to configure `dau`. The configuration is a single page of options,
|
||||
no changes will take effect until the "Save All Configuration" button has been pressed.
|
||||
|
||||
### 'Discord WebHook URL'
|
||||
### Global options
|
||||
|
||||
The webhook URL from Discord. See https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks
|
||||
for more information on setting one up.
|
||||
* Server port - the port number the web server listens on. Requires restart
|
||||
* Watch interval - how often each watcher will check the directory for new files
|
||||
|
||||
### 'Bot Username'
|
||||
### Watcher configuration
|
||||
|
||||
This is completely optional and can be any arbitrary string. It makes the upload
|
||||
There can be one or more watchers configured. Each watcher looks in a particular directory,
|
||||
and uploads new files to a different discord channel.
|
||||
|
||||
Each watcher has the following configuration options:
|
||||
|
||||
* Directory to watch - This is the path that `dau` will periodically inspect, looking for new images.
|
||||
Note that subdirectories are also scanned. You need to enter the full filesystem path here.
|
||||
* Discord WebHook URL - The webhook URL from Discord. See https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks for more information on setting one up.
|
||||
* Username - This is completely optional and can be any arbitrary string. It makes the upload
|
||||
appear to come from a different user (though this is visual only, and does not
|
||||
actually hide the bot identity in any way). You might like to set it to your own
|
||||
discord name.
|
||||
* Watermark - Disabling the watermark will prevent `dau` from putting a link to the projects
|
||||
on the bottom left hand corner of your uploaded images. I really appreciate it when you leave this enabled :-)
|
||||
* Hold Uploads - See "Holding uploads" below
|
||||
* Exclusions - You can set one or more arbitrary strings to exclude files from being matched by this watcher.
|
||||
This is most commonly used to prevent thumbnail images from being uploads.
|
||||
|
||||
### 'Directory to watch'
|
||||
## Holding uploads
|
||||
|
||||
This is the path that `dau` will periodically inspect, looking for new images.
|
||||
Note that subdirectories are also scanned. You need to enter the full filesystem
|
||||
path here.
|
||||
If the "Hold Uploads" option is selected, newly found files will not immediately be uploaded. They will be available
|
||||
in the "uploads" tab of the web interface. This has two purposes:
|
||||
|
||||
### 'Period between filesystem checks'
|
||||
* It gives you a chance to vet your screenshot selection before uploading
|
||||
* It allows you to edit the images before uploading.
|
||||
|
||||
This is the number of seconds between which `dau` will look for new images.
|
||||
In the list of uploads there are three actions you can take on each file:
|
||||
|
||||
### 'Do not watermark images'
|
||||
* Press "upload" to upload the image
|
||||
* Press "reject" to reject the image
|
||||
* Click on the image thumbnail to edit the image
|
||||
|
||||
This will disable the watermarking of images. I like it when you don't set this :-)
|
||||
|
||||
### 'Files to exclude'
|
||||
|
||||
This is a string to match against the filename to check for exclusions. The common
|
||||
use case is to use 'thumbnail' or similar if your image directory contains additional
|
||||
thumbnail files.
|
||||
If you click on the image thumbnail, an image editor will open, and allow you to add text captions to your image.
|
||||
More functionality is coming soon. When you are finished editing, choose "Apply" and you will return to the uploads
|
||||
list. Click "upload" to upload your edited image.
|
||||
|
||||
## Limitations/bugs
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type Watcher struct {
|
||||
Path string
|
||||
Username string
|
||||
NoWatermark bool
|
||||
HoldUploads bool
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
@@ -94,8 +95,6 @@ func (c *ConfigService) Load() error {
|
||||
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
|
||||
|
||||
@@ -10,10 +10,14 @@ func TestNoConfig(t *testing.T) {
|
||||
c := ConfigService{}
|
||||
|
||||
c.ConfigFilename = emptyTempFile()
|
||||
os.Remove(c.ConfigFilename)
|
||||
err := os.Remove(c.ConfigFilename)
|
||||
if err != nil {
|
||||
t.Fatalf("could not remove file: %v", err)
|
||||
}
|
||||
|
||||
defer os.Remove(c.ConfigFilename) // because we are about to create it
|
||||
|
||||
err := c.LoadOrInit()
|
||||
err = c.LoadOrInit()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected failure from load: %s", err)
|
||||
}
|
||||
@@ -84,6 +88,7 @@ func emptyTempFile() string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.Close()
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
|
||||
16
dau_test.go
16
dau_test.go
@@ -80,7 +80,11 @@ func TestCheckPath(t *testing.T) {
|
||||
if !w.checkPath() {
|
||||
t.Error("checkPath failed?")
|
||||
}
|
||||
os.RemoveAll(dir)
|
||||
|
||||
err := os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("could not remove test dir: %v", err)
|
||||
}
|
||||
if w.checkPath() {
|
||||
t.Error("checkPath succeeded when shouldn't?")
|
||||
}
|
||||
@@ -91,9 +95,11 @@ func createFileTree() string {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "a.gif"))
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "a.jpg"))
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "a.png"))
|
||||
|
||||
f1, _ := os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "a.gif"))
|
||||
f2, _ := os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "a.jpg"))
|
||||
f3, _ := os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "a.png"))
|
||||
f1.Close()
|
||||
f2.Close()
|
||||
f3.Close()
|
||||
return dir
|
||||
}
|
||||
|
||||
1
go.mod
1
go.mod
@@ -5,6 +5,7 @@ go 1.16
|
||||
require (
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e
|
||||
golang.org/x/mod v0.4.2
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
||||
4
imageprocess/imageprocess.go
Normal file
4
imageprocess/imageprocess.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package imageprocess
|
||||
|
||||
type Processor struct {
|
||||
}
|
||||
57
imageprocess/thumb.go
Normal file
57
imageprocess/thumb.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package imageprocess
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/tardisx/discord-auto-upload/upload"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
const (
|
||||
thumbnailMaxX = 128
|
||||
thumbnailMaxY = 128
|
||||
)
|
||||
|
||||
func (ip *Processor) ThumbPNG(ul *upload.Upload, which string, w io.Writer) error {
|
||||
|
||||
var filename string
|
||||
if which == "orig" {
|
||||
filename = ul.OriginalFilename
|
||||
} else if which == "markedup" {
|
||||
filename = ul.MarkedUpFilename
|
||||
} else {
|
||||
log.Fatal("was passed incorrect 'which' arg")
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open file: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
im, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not decode file: %s", err)
|
||||
}
|
||||
|
||||
newXY := image.Point{}
|
||||
if im.Bounds().Max.X/thumbnailMaxX > im.Bounds().Max.Y/thumbnailMaxY {
|
||||
newXY.X = thumbnailMaxX
|
||||
newXY.Y = im.Bounds().Max.Y / (im.Bounds().Max.X / thumbnailMaxX)
|
||||
} else {
|
||||
newXY.Y = thumbnailMaxY
|
||||
newXY.X = im.Bounds().Max.X / (im.Bounds().Max.Y / thumbnailMaxY)
|
||||
}
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, newXY.X, newXY.Y))
|
||||
draw.BiLinear.Scale(dst, dst.Rect, im, im.Bounds(), draw.Over, nil)
|
||||
|
||||
png.Encode(w, dst)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
151
upload/upload.go
151
upload/upload.go
@@ -15,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
@@ -23,6 +24,19 @@ import (
|
||||
"golang.org/x/image/font/inconsolata"
|
||||
)
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
StatePending State = "Pending" // waiting for decision to upload (could be edited)
|
||||
StateQueued State = "Queued" // ready for upload
|
||||
StateUploading State = "Uploading" // uploading
|
||||
StateComplete State = "Complete" // finished successfully
|
||||
StateFailed State = "Failed" // failed
|
||||
StateSkipped State = "Skipped" // user did not want to upload
|
||||
)
|
||||
|
||||
var currentId int32
|
||||
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
@@ -32,13 +46,11 @@ type Uploader struct {
|
||||
}
|
||||
|
||||
type Upload struct {
|
||||
Uploaded bool `json:"uploaded"` // has this file been uploaded to discord
|
||||
Id int32 `json:"id"`
|
||||
UploadedAt time.Time `json:"uploaded_at"`
|
||||
|
||||
Failed bool `json:"failed"`
|
||||
|
||||
originalFilename string // path on the local disk
|
||||
filenameToUpload string // post-watermark, or just original if unwatermarked
|
||||
OriginalFilename string `json:"original_file"` // path on the local disk
|
||||
MarkedUpFilename string `json:"markedup_file"` // a temporary file, if the user did some markup
|
||||
|
||||
webhookURL string
|
||||
|
||||
@@ -51,6 +63,8 @@ type Upload struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
|
||||
State State `json:"state"`
|
||||
|
||||
Client HTTPClient `json:"-"`
|
||||
}
|
||||
|
||||
@@ -62,12 +76,19 @@ func NewUploader() *Uploader {
|
||||
}
|
||||
|
||||
func (u *Uploader) AddFile(file string, conf config.Watcher) {
|
||||
atomic.AddInt32(¤tId, 1)
|
||||
thisUpload := Upload{
|
||||
Uploaded: false,
|
||||
originalFilename: file,
|
||||
Id: currentId,
|
||||
OriginalFilename: file,
|
||||
watermark: !conf.NoWatermark,
|
||||
webhookURL: conf.WebHookURL,
|
||||
usernameOverride: conf.Username,
|
||||
State: StateQueued,
|
||||
}
|
||||
// if the user wants uploads to be held for editing etc,
|
||||
// set it to Pending instead
|
||||
if conf.HoldUploads {
|
||||
thisUpload.State = StatePending
|
||||
}
|
||||
u.Uploads = append(u.Uploads, &thisUpload)
|
||||
}
|
||||
@@ -76,27 +97,37 @@ func (u *Uploader) AddFile(file string, conf config.Watcher) {
|
||||
func (u *Uploader) Upload() {
|
||||
|
||||
for _, upload := range u.Uploads {
|
||||
if !upload.Uploaded && !upload.Failed {
|
||||
if upload.State == StateQueued {
|
||||
upload.processUpload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Uploader) UploadById(id int32) *Upload {
|
||||
for _, anUpload := range u.Uploads {
|
||||
if anUpload.Id == int32(id) {
|
||||
return anUpload
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Upload) RemoveMarkupTempFile() {
|
||||
if len(u.MarkedUpFilename) > 0 {
|
||||
os.Remove(u.MarkedUpFilename)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Upload) processUpload() error {
|
||||
daulog.SendLog(fmt.Sprintf("Uploading: %s", u.originalFilename), daulog.LogTypeInfo)
|
||||
daulog.SendLog(fmt.Sprintf("Uploading: %s", u.OriginalFilename), daulog.LogTypeInfo)
|
||||
|
||||
baseFilename := filepath.Base(u.OriginalFilename)
|
||||
|
||||
if u.webhookURL == "" {
|
||||
daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError)
|
||||
return errors.New("webhook url not configured")
|
||||
}
|
||||
|
||||
if u.watermark {
|
||||
daulog.SendLog("Watermarking image", daulog.LogTypeInfo)
|
||||
u.applyWatermark()
|
||||
} else {
|
||||
u.filenameToUpload = u.originalFilename
|
||||
}
|
||||
|
||||
extraParams := map[string]string{}
|
||||
|
||||
if u.usernameOverride != "" {
|
||||
@@ -121,7 +152,42 @@ func (u *Upload) processUpload() error {
|
||||
var retriesRemaining = 5
|
||||
for retriesRemaining > 0 {
|
||||
|
||||
request, err := newfileUploadRequest(u.webhookURL, extraParams, "file", u.filenameToUpload)
|
||||
// open an io.ReadCloser for the file we intend to upload
|
||||
var filedata *os.File
|
||||
var err error
|
||||
if len(u.MarkedUpFilename) > 0 {
|
||||
filedata, err = os.Open(u.MarkedUpFilename)
|
||||
if err != nil {
|
||||
log.Print("Error opening marked up file:", err)
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
filedata, err = os.Open(u.OriginalFilename)
|
||||
if err != nil {
|
||||
log.Print("Error opening original file:", err)
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var imageData io.Reader
|
||||
if u.watermark {
|
||||
daulog.SendLog("Watermarking image", daulog.LogTypeInfo)
|
||||
imageData, err = u.applyWatermark(filedata)
|
||||
if err != nil {
|
||||
log.Print("Error watermarking:", err)
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
imageData = filedata
|
||||
}
|
||||
|
||||
request, err := newfileUploadRequest(u.webhookURL, extraParams, "file", baseFilename, imageData)
|
||||
if err != nil {
|
||||
log.Printf("error creating upload request: %s", err)
|
||||
return fmt.Errorf("could not create upload request: %s", err)
|
||||
@@ -144,6 +210,7 @@ func (u *Upload) processUpload() error {
|
||||
if resp.StatusCode == 413 {
|
||||
// just fail immediately, we know this means the file was too big
|
||||
daulog.SendLog("413 received - file too large", daulog.LogTypeError)
|
||||
u.State = StateFailed
|
||||
return errors.New("received 413 - file too large")
|
||||
}
|
||||
|
||||
@@ -196,7 +263,7 @@ func (u *Upload) processUpload() error {
|
||||
daulog.SendLog(fmt.Sprintf("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate), daulog.LogTypeInfo)
|
||||
|
||||
u.Url = a.URL
|
||||
u.Uploaded = true
|
||||
u.State = StateComplete
|
||||
u.Width = a.Width
|
||||
u.Height = a.Height
|
||||
u.UploadedAt = time.Now()
|
||||
@@ -205,33 +272,27 @@ func (u *Upload) processUpload() error {
|
||||
}
|
||||
}
|
||||
|
||||
if u.watermark {
|
||||
daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", u.filenameToUpload), daulog.LogTypeDebug)
|
||||
os.Remove(u.filenameToUpload)
|
||||
}
|
||||
// remove any marked up file
|
||||
u.RemoveMarkupTempFile()
|
||||
|
||||
if retriesRemaining == 0 {
|
||||
daulog.SendLog("Failed to upload, even after all retries", daulog.LogTypeError)
|
||||
u.Failed = true
|
||||
u.State = StateFailed
|
||||
return errors.New("could not upload after all retries")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open file '%s': %s", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
func newfileUploadRequest(uri string, params map[string]string, paramName string, filename string, filedata io.Reader) (*http.Request, error) {
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
|
||||
part, err := writer.CreateFormFile(paramName, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = io.Copy(part, file)
|
||||
_, err = io.Copy(part, filedata)
|
||||
if err != nil {
|
||||
log.Fatal("Could not copy: ", err)
|
||||
}
|
||||
@@ -249,20 +310,15 @@ func newfileUploadRequest(uri string, params map[string]string, paramName, path
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (u *Upload) applyWatermark() {
|
||||
// applyWatermark applies the watermark to the image
|
||||
func (u *Upload) applyWatermark(in *os.File) (io.Reader, error) {
|
||||
|
||||
reader, err := os.Open(u.originalFilename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
defer in.Close()
|
||||
|
||||
im, _, err := image.Decode(reader)
|
||||
im, _, err := image.Decode(in)
|
||||
if err != nil {
|
||||
daulog.SendLog(fmt.Sprintf("Cannot decode image: %v - skipping watermarking", err), daulog.LogTypeError)
|
||||
u.watermark = false
|
||||
u.filenameToUpload = u.originalFilename
|
||||
return
|
||||
return nil, errors.New("cannot decode image")
|
||||
}
|
||||
bounds := im.Bounds()
|
||||
// var S float64 = float64(bounds.Max.X)
|
||||
@@ -283,16 +339,9 @@ func (u *Upload) applyWatermark() {
|
||||
|
||||
dc.DrawString("github.com/tardisx/discord-auto-upload", 5.0, float64(bounds.Max.Y)-5.0)
|
||||
|
||||
tempfile, err := ioutil.TempFile("", "dau")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tempfile.Close()
|
||||
os.Remove(tempfile.Name())
|
||||
actualName := tempfile.Name() + ".png"
|
||||
|
||||
dc.SavePNG(actualName)
|
||||
u.filenameToUpload = actualName
|
||||
b := bytes.Buffer{}
|
||||
dc.EncodePNG(&b)
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func sleepForRetries(retry int) {
|
||||
|
||||
@@ -2,7 +2,11 @@ package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -39,7 +43,7 @@ func TestSuccessfulUpload(t *testing.T) {
|
||||
// we will not really be uploading it here
|
||||
f, _ := os.CreateTemp("", "dautest-upload-*")
|
||||
defer os.Remove(f.Name())
|
||||
u := Upload{webhookURL: "https://127.0.0.1/", originalFilename: f.Name()}
|
||||
u := Upload{webhookURL: "https://127.0.0.1/", OriginalFilename: f.Name()}
|
||||
u.Client = &MockClient{DoFunc: DoGoodUpload}
|
||||
err := u.processUpload()
|
||||
if err != nil {
|
||||
@@ -58,7 +62,7 @@ func TestTooBigUpload(t *testing.T) {
|
||||
// we will not really be uploading it here
|
||||
f, _ := os.CreateTemp("", "dautest-upload-*")
|
||||
defer os.Remove(f.Name())
|
||||
u := Upload{webhookURL: "https://127.0.0.1/", originalFilename: f.Name()}
|
||||
u := Upload{webhookURL: "https://127.0.0.1/", OriginalFilename: f.Name()}
|
||||
u.Client = &MockClient{DoFunc: DoTooBigUpload}
|
||||
err := u.processUpload()
|
||||
if err == nil {
|
||||
@@ -66,4 +70,32 @@ func TestTooBigUpload(t *testing.T) {
|
||||
} else if err.Error() != "received 413 - file too large" {
|
||||
t.Errorf("wrong error occurred: %s", err.Error())
|
||||
}
|
||||
if u.State != StateFailed {
|
||||
t.Error("upload should have been marked failed")
|
||||
}
|
||||
}
|
||||
|
||||
func tempImageGt8Mb() {
|
||||
// about 12Mb
|
||||
width := 2000
|
||||
height := 2000
|
||||
|
||||
upLeft := image.Point{0, 0}
|
||||
lowRight := image.Point{width, height}
|
||||
|
||||
img := image.NewRGBA(image.Rectangle{upLeft, lowRight})
|
||||
|
||||
// Colors are defined by Red, Green, Blue, Alpha uint8 values.
|
||||
|
||||
// Set color for each pixel.
|
||||
for x := 0; x < width; x++ {
|
||||
for y := 0; y < height; y++ {
|
||||
color := color.RGBA{uint8(rand.Int31n(256)), uint8(rand.Int31n(256)), uint8(rand.Int31n(256)), 0xff}
|
||||
img.Set(x, y, color)
|
||||
}
|
||||
}
|
||||
|
||||
// Encode as PNG.
|
||||
f, _ := os.Create("image.png")
|
||||
png.Encode(f, img)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const CurrentVersion string = "v0.11.1"
|
||||
const CurrentVersion string = "v0.12.0"
|
||||
|
||||
func NewVersionAvailable(v string) bool {
|
||||
if !semver.IsValid(CurrentVersion) {
|
||||
|
||||
5
web/data/alpine.js
Normal file
5
web/data/alpine.js
Normal file
File diff suppressed because one or more lines are too long
@@ -58,6 +58,12 @@
|
||||
change only, and does not hide the uploaders actual identity.
|
||||
</p>
|
||||
|
||||
<p>A watcher can be configured to hold uploads. This causes the new images seen
|
||||
by the watcher to be held for review on the <a href="/uploads.html">uploads page</a>.
|
||||
This allows each image to be individually uploaded or skipped.
|
||||
|
||||
</p>
|
||||
|
||||
<p>Exclusions can be specified, zero or more arbitrary strings. If any
|
||||
file matches one of those strings then it will not be uploaded. This is most
|
||||
often used if you use software (like Steam) which automatically creates thumbnails
|
||||
@@ -105,6 +111,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Hold Uploads</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<button type="button" @click="config.Watchers[i].HoldUploads = ! config.Watchers[i].HoldUploads" class="btn btn-success" x-text="watcher.HoldUploads ? 'Enabled' : 'Disabled'"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
@@ -141,7 +156,7 @@
|
||||
|
||||
<div class="my-5">
|
||||
<button type="button" class="btn btn-secondary" href="#"
|
||||
@click.prevent="config.Watchers.push({Username: '', WebHookURL: 'https://webhook.url.here/', Path: '/directory/path/here', NoWatermark: false, Exclude: []});">
|
||||
@click.prevent="config.Watchers.push({Username: '', WebHookURL: 'https://webhook.url.here/', Path: '/directory/path/here', NoWatermark: false, HoldUploads: false, Exclude: []});">
|
||||
Add a new watcher</button>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +175,6 @@
|
||||
{{ end }}
|
||||
|
||||
{{ define "js" }}
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script>
|
||||
function configuration() {
|
||||
return {
|
||||
|
||||
@@ -28,6 +28,8 @@ html,
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
padding: 2em;
|
||||
max-width: 80em;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -35,13 +37,16 @@ body {
|
||||
display: flex; */
|
||||
color: #fff;
|
||||
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.DAU-container {
|
||||
max-width: 52em;
|
||||
}
|
||||
|
||||
.DAU-container-editor {
|
||||
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: black;
|
||||
color: aliceblue;
|
||||
|
||||
233
web/data/editor.html
Normal file
233
web/data/editor.html
Normal file
@@ -0,0 +1,233 @@
|
||||
{{ define "content" }}
|
||||
|
||||
<main role="main" class="" x-data="editor()" x-init="setup_canvas();">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<canvas id="c" x-bind:style="canvas_style">
|
||||
</canvas>
|
||||
<img :src="img_data">
|
||||
</div>
|
||||
<div class="col">
|
||||
<div id="tools-top" x-show="!toolbar">
|
||||
<button type="button" @click="add_some_text()" class="btn btn-primary">Add text</button>
|
||||
<!-- <button type="button" @click="crop()" class="btn btn-primary">Crop</button> -->
|
||||
<button type="button" @click="apply()" class="btn btn-primary">Apply</button>
|
||||
<button type="button" @click="cancel()" class="btn btn-primary">Cancel</button>
|
||||
</div>
|
||||
<div id="tools-delete" x-show="toolbar == 'text'">
|
||||
<button type="button" @click="delete_selected();" class="btn btn-primary" style="">delete</button>
|
||||
</div>
|
||||
<div id="tools-crop" x-show="toolbar == 'crop'">
|
||||
<button type="button" @click="apply_crop();" class="btn btn-primary" style="">crop</button>
|
||||
<button type="button" @click="cancel_crop();" class="btn btn-primary" style="">cancel</button>
|
||||
</div>
|
||||
<div id="tools-colour" x-show="toolbar == 'text'">
|
||||
<table>
|
||||
<tr>
|
||||
<th>foreground</th>
|
||||
<template x-for="colour in colours">
|
||||
<td>
|
||||
<button type="button" @click="set_colour(colour, 'fg')" class="btn btn-primary" :style="'background-color: '+colour"> </button>
|
||||
</td>
|
||||
</template>
|
||||
<td>
|
||||
<button type="button" @click="set_colour('#fff0', 'fg')" class="btn btn-primary" style="">-</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>background</th>
|
||||
<template x-for="colour in colours">
|
||||
<td>
|
||||
<button type="button" @click="set_colour(colour, 'bg')" class="btn btn-primary" :style="'background-color: '+colour"> </button>
|
||||
</td>
|
||||
</template>
|
||||
<td>
|
||||
<button type="button" @click="set_colour('#fff0', 'bg')" class="btn btn-primary" style="">-</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>outline</th>
|
||||
<template x-for="colour in colours">
|
||||
<td>
|
||||
<button type="button" @click="set_colour(colour, 'stroke')" class="btn btn-primary" :style="'background-color: '+colour"> </button>
|
||||
</td>
|
||||
</template>
|
||||
<td>
|
||||
<button type="button" @click="set_colour('#fff0', 'stroke')" class="btn btn-primary" style="">-</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ define "js" }}
|
||||
|
||||
<script>
|
||||
// for some reason, canvas does not work correctly if the object
|
||||
// is managed by alpine - see https://github.com/fabricjs/fabric.js/issues/7485
|
||||
var canvas = null;
|
||||
|
||||
function editor() {
|
||||
return {
|
||||
img_data: "", scaleFactor: 0.5,
|
||||
toolbar: null,
|
||||
crop_state: {},
|
||||
colours: [ 'red', 'blue', 'green', 'white', 'yellow', 'black', 'purple'],
|
||||
|
||||
canvas_style: "",
|
||||
// "position: absolute; width: 100%; height: 100%; left: 0px; top: 0px; touch-action: none; -webkit-user-select: none;",
|
||||
setup_canvas() {
|
||||
// seriously javascript? just imagine, in 2021....
|
||||
var url = new URL(window.location);
|
||||
var id = url.searchParams.get("id");
|
||||
var self = this;
|
||||
canvas = new fabric.Canvas('c');
|
||||
|
||||
canvas.on('selection:cleared', function(options) {
|
||||
self.toolbar = null;
|
||||
});
|
||||
|
||||
fabric.Image.fromURL('/rest/image/'+id, function(oImg) {
|
||||
self.scaleFactor = scalefactor(oImg.width, oImg.height);
|
||||
canvas.setDimensions({width: oImg.width, height: oImg.height});
|
||||
oImg.selectable = false;
|
||||
canvas.add(oImg);
|
||||
canvas.setHeight(canvas.getHeight() * (self.scaleFactor));
|
||||
canvas.setWidth(canvas.getWidth() * (self.scaleFactor));
|
||||
canvas.setZoom(self.scaleFactor);
|
||||
});
|
||||
},
|
||||
export_image() {
|
||||
this.img_data = canvas.toDataURL({multiplier: 1/this.scaleFactor});
|
||||
},
|
||||
add_some_text() {
|
||||
var text = new fabric.Textbox('double click to change', { left: 20, top: 20, width: 300, fontSize: 40 });
|
||||
canvas.add(text);
|
||||
canvas.setActiveObject(text);
|
||||
this.toolbar = 'text';
|
||||
var self = this;
|
||||
text.on('selected', function(options) {
|
||||
self.toolbar = 'text';
|
||||
});
|
||||
},
|
||||
delete_selected() {
|
||||
selected = canvas.getActiveObjects();
|
||||
selected.forEach(el => {
|
||||
canvas.discardActiveObject(el);
|
||||
canvas.remove(el);
|
||||
});
|
||||
},
|
||||
set_colour(colour, type) {
|
||||
selected = canvas.getActiveObjects();
|
||||
console.log();
|
||||
selected.forEach(el => {
|
||||
if (type === 'fg') {
|
||||
el.set('fill', colour);
|
||||
}
|
||||
if (type === 'bg') {
|
||||
el.set('textBackgroundColor', colour);
|
||||
}
|
||||
if (type === 'stroke') {
|
||||
el.set('stroke', colour);
|
||||
}
|
||||
|
||||
});
|
||||
canvas.renderAll();
|
||||
},
|
||||
|
||||
// crop mode - XXX not yet implemented
|
||||
crop() {
|
||||
this.toolbar = 'crop';
|
||||
this.crop_state = {};
|
||||
canvas.selection = false; // disable drag drop selection so we can see the crop rect
|
||||
let self = this;
|
||||
this.crop_state.rectangle = new fabric.Rect({
|
||||
fill: 'transparent',
|
||||
stroke: '#ccc',
|
||||
strokeDashArray: [2, 2],
|
||||
visible: false
|
||||
});
|
||||
console.log(this.crop_state.rectangle);
|
||||
var container = document.getElementById('c').getBoundingClientRect();
|
||||
canvas.add(this.crop_state.rectangle);
|
||||
canvas.on("mouse:down", function(event) {
|
||||
if(1) {
|
||||
console.log('wow mouse is down', event.e);
|
||||
self.crop_state.rectangle.width = 2;
|
||||
self.crop_state.rectangle.height = 2;
|
||||
self.crop_state.rectangle.left = event.e.offsetX / self.scaleFactor;
|
||||
self.crop_state.rectangle.top = event.e.offsetY / self.scaleFactor;
|
||||
self.crop_state.rectangle.visible = true;
|
||||
self.crop_state.mouseDown = event.e;
|
||||
canvas.bringToFront(self.crop_state.rectangle);
|
||||
}
|
||||
});
|
||||
// draw the rectangle as the mouse is moved after a down click
|
||||
canvas.on("mouse:move", function(event) {
|
||||
if(self.crop_state.mouseDown && 1) {
|
||||
|
||||
self.crop_state.rectangle.width = event.e.offsetX / self.scaleFactor - self.crop_state.rectangle.left;
|
||||
self.crop_state.rectangle.height = event.e.offsetY / self.scaleFactor - self.crop_state.rectangle.top;
|
||||
canvas.renderAll();
|
||||
|
||||
}
|
||||
});
|
||||
// when mouse click is released, end cropping mode
|
||||
canvas.on("mouse:up", function() {
|
||||
console.log('MOUSE UP');
|
||||
self.crop_state.mouseDown = null;
|
||||
});
|
||||
},
|
||||
apply_crop() {
|
||||
console.log(this.crop_state.rectangle.width);
|
||||
},
|
||||
apply() {
|
||||
|
||||
image_data = canvas.toDataURL({
|
||||
format: 'png',
|
||||
multiplier: 1.0/this.scaleFactor});
|
||||
let formData = new FormData();
|
||||
formData.append('image', image_data);
|
||||
var url = new URL(window.location);
|
||||
var id = url.searchParams.get("id");
|
||||
fetch('/rest/upload/'+id+'/markup', {method: 'POST', body: formData})
|
||||
.then(response => response.json()) // convert to json
|
||||
.then(json => {
|
||||
console.log(json);
|
||||
window.location = '/uploads.html';
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
window.location = '/uploads.html';
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function scalefactor(width, height) {
|
||||
max_width = window.innerWidth * 3/5;
|
||||
max_height = window.innerHeight * 5/6;
|
||||
|
||||
if (width <= max_width && height <= max_height) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
factor = max_width/width;
|
||||
if (height*factor <= max_height) {
|
||||
return factor;
|
||||
}
|
||||
|
||||
return 1/ (height/max_height);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{{ end }}
|
||||
1
web/data/fabric.min.js
vendored
Normal file
1
web/data/fabric.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,45 +1,141 @@
|
||||
{{ define "content" }}
|
||||
|
||||
<main role="main" class="inner DAU">
|
||||
<main role="main" x-data="uploads()" x-init="get_uploads();" class="inner DAU">
|
||||
<h1 class="DAU-heading">Uploads</h1>
|
||||
<p class="lead">Discord-auto-upload uploads</p>
|
||||
|
||||
<h2>Pending uploads</h2>
|
||||
|
||||
<table class="table table-condensed table-dark">
|
||||
<thead>
|
||||
<tr><th>uploaded</th><th>dt</th><th>thumb</th></tr>
|
||||
<tr>
|
||||
<th>filename</th>
|
||||
<th>actions</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="uploads">
|
||||
<tbody>
|
||||
<template x-for="ul in pending">
|
||||
<tr>
|
||||
<td x-text="ul.original_file"></td>
|
||||
<td>
|
||||
<button @click="start_upload(ul.id)" type="button" class="btn btn-primary">upload</button>
|
||||
<button @click="skip_upload(ul.id)" type="button" class="btn btn-primary">reject</button>
|
||||
</td>
|
||||
<td>
|
||||
<a x-bind:href="'/editor.html?id='+ul.id"><img x-bind:src="'/rest/image/'+ul.id+'/thumb'"></a>
|
||||
<a x-show="ul.markedup_file" x-bind:href="'/editor.html?id='+ul.id"><img x-bind:src="'/rest/image/'+ul.id+'/markedup_thumb'"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Current uploads</h2>
|
||||
|
||||
<table class="table table-condensed table-dark">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>filename</th>
|
||||
<th>actions</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<template x-for="ul in uploads">
|
||||
<tr>
|
||||
<td x-text="ul.original_file"></td>
|
||||
<td>
|
||||
<button @click="start_upload(ul.id)" type="button" class="btn btn-primary">upload</button>
|
||||
</td>
|
||||
<td>
|
||||
<img :src="'/rest/image/'+ul.id+'/thumb'">
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Completed uploads</h2>
|
||||
|
||||
<table class="table table-condensed table-dark">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>filename</th>
|
||||
<th>state</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<template x-for="ul in finished">
|
||||
<tr>
|
||||
<td x-text="ul.original_file"></td>
|
||||
<td x-text="ul.state"></td>
|
||||
<td>
|
||||
<img :src="'/rest/image/'+ul.id+'/thumb'">
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ define "js" }}
|
||||
<script>
|
||||
|
||||
$(document).ready(function() {
|
||||
get_uploads();
|
||||
});
|
||||
|
||||
function get_uploads() {
|
||||
$.ajax({ method: 'get', url: '/rest/uploads'})
|
||||
.done(function(data) {
|
||||
console.log(data);
|
||||
$('#uploads').empty();
|
||||
if (! data) { return }
|
||||
data.forEach(i => {
|
||||
// {uploaded: true, uploaded_at: "2021-06-08T21:59:52.855936+09:30", url: "https://cdn.discordapp.com/attachments/849615269706203171/851800197046468628/dau736004285.png", width: 640, height: 640}
|
||||
console.log(i);
|
||||
row = $('<tr>');
|
||||
row.append($('<td>').text(i.uploaded ? 'yes' : 'no'));
|
||||
row.append($('<td>').text(i.uploaded_at));
|
||||
row.append($('<td>').html($('<img>', { width : i.width/10, height : i.height/10, src : i.url })));
|
||||
$('#uploads').prepend(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
function uploads() {
|
||||
return {
|
||||
pending: [], uploads: [], finished: [],
|
||||
start_upload(id) {
|
||||
console.log(id);
|
||||
fetch('/rest/upload/'+id+'/start', {method: 'POST'})
|
||||
.then(response => response.json()) // convert to json
|
||||
.then(json => {
|
||||
console.log(json);
|
||||
})
|
||||
},
|
||||
skip_upload(id) {
|
||||
console.log(id);
|
||||
fetch('/rest/upload/'+id+'/skip', {method: 'POST'})
|
||||
.then(response => response.json()) // convert to json
|
||||
.then(json => {
|
||||
console.log(json);
|
||||
})
|
||||
},
|
||||
get_uploads() {
|
||||
fetch('/rest/uploads')
|
||||
.then(response => response.json()) // convert to json
|
||||
.then(json => {
|
||||
this.pending = [];
|
||||
this.uploads = [];
|
||||
this.finished = [];
|
||||
json.forEach(ul => {
|
||||
if (ul.state == 'Pending') {
|
||||
this.pending.push(ul);
|
||||
}
|
||||
else if (ul.state == 'Complete' || ul.state == 'Failed' || ul.state == 'Skipped') {
|
||||
this.finished.push(ul)
|
||||
}
|
||||
else {
|
||||
this.uploads.push(ul);
|
||||
}
|
||||
});
|
||||
this.config = json;
|
||||
console.log(json);
|
||||
let self = this;
|
||||
setTimeout(function() { self.get_uploads(); } , 1000);
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="/alpine.js" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fabric@4.6.0/dist/fabric.min.js"></script>
|
||||
|
||||
<style>
|
||||
.bd-placeholder-img {
|
||||
@@ -35,7 +33,7 @@
|
||||
|
||||
<body class="">
|
||||
|
||||
<div class="DAU-container d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||
<div class="DAU-container-editor d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||
<header class="masthead mb-auto">
|
||||
<div class="inner">
|
||||
<h3 class="masthead-brand">discord-auto-upload ({{.Version}})</h3>
|
||||
@@ -60,6 +58,7 @@
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
{{ template "js" . }}
|
||||
</html>
|
||||
|
||||
|
||||
233
web/server.go
233
web/server.go
@@ -2,17 +2,24 @@ package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tardisx/discord-auto-upload/config"
|
||||
"github.com/tardisx/discord-auto-upload/imageprocess"
|
||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||
"github.com/tardisx/discord-auto-upload/upload"
|
||||
"github.com/tardisx/discord-auto-upload/version"
|
||||
@@ -23,6 +30,19 @@ type WebService struct {
|
||||
Uploader *upload.Uploader
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type StartUploadRequest struct {
|
||||
Id int32 `json:"id"`
|
||||
}
|
||||
|
||||
type StartUploadResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
//go:embed data
|
||||
var webFS embed.FS
|
||||
|
||||
@@ -43,6 +63,7 @@ func (ws *WebService) getStatic(w http.ResponseWriter, r *http.Request) {
|
||||
extension := filepath.Ext(string(path))
|
||||
|
||||
if extension == ".html" { // html file
|
||||
|
||||
t, err := template.ParseFS(webFS, "data/wrapper.tmpl", "data/"+path)
|
||||
if err != nil {
|
||||
daulog.SendLog(fmt.Sprintf("when fetching: %s got: %s", path, err), daulog.LogTypeError)
|
||||
@@ -109,33 +130,23 @@ func (ws *WebService) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
func (ws *WebService) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
newConfig := config.ConfigV2{}
|
||||
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("bad body"))
|
||||
returnJSONError(w, "could not read body?")
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, &newConfig)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
j, _ := json.Marshal(ErrorResponse{Error: "badly formed JSON"})
|
||||
w.Write(j)
|
||||
returnJSONError(w, "badly formed JSON")
|
||||
return
|
||||
}
|
||||
ws.Config.Config = &newConfig
|
||||
err = ws.Config.Save()
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
j, _ := json.Marshal(ErrorResponse{Error: err.Error()})
|
||||
w.Write(j)
|
||||
|
||||
returnJSONError(w, err.Error())
|
||||
return
|
||||
}
|
||||
// config has changed, so tell the world
|
||||
@@ -155,27 +166,203 @@ func (ws *WebService) getUploads(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
text, err := json.Marshal(ups)
|
||||
if err != nil {
|
||||
daulog.SendLog(fmt.Sprintf("err: %v", err), daulog.LogTypeError)
|
||||
w.Write([]byte("could not marshall uploads?"))
|
||||
return
|
||||
// not sure how this would happen, so we probably want to find out the hard way
|
||||
panic(err)
|
||||
}
|
||||
w.Write([]byte(text))
|
||||
}
|
||||
|
||||
func (ws *WebService) imageThumb(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
processor := imageprocess.Processor{}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.ParseInt(vars["id"], 10, 32)
|
||||
if err != nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
|
||||
ul := ws.Uploader.UploadById(int32(id))
|
||||
if ul == nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
err = processor.ThumbPNG(ul, "orig", w)
|
||||
if err != nil {
|
||||
returnJSONError(w, "could not create thumb")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebService) imageMarkedupThumb(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
processor := imageprocess.Processor{}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.ParseInt(vars["id"], 10, 32)
|
||||
if err != nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
|
||||
ul := ws.Uploader.UploadById(int32(id))
|
||||
if ul == nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
err = processor.ThumbPNG(ul, "markedup", w)
|
||||
if err != nil {
|
||||
returnJSONError(w, "could not create thumb")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebService) image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.ParseInt(vars["id"], 10, 32)
|
||||
if err != nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
|
||||
ul := ws.Uploader.UploadById(int32(id))
|
||||
if ul == nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
|
||||
img, err := os.Open(ul.OriginalFilename)
|
||||
if err != nil {
|
||||
returnJSONError(w, "could not open image file")
|
||||
return
|
||||
}
|
||||
defer img.Close()
|
||||
io.Copy(w, img)
|
||||
}
|
||||
|
||||
func (ws *WebService) modifyUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if r.Method == "POST" {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
change := vars["change"]
|
||||
id, err := strconv.ParseInt(vars["id"], 10, 32)
|
||||
if err != nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
|
||||
anUpload := ws.Uploader.UploadById(int32(id))
|
||||
if anUpload == nil {
|
||||
returnJSONError(w, "bad id")
|
||||
return
|
||||
}
|
||||
|
||||
if anUpload.State == upload.StatePending {
|
||||
if change == "start" {
|
||||
anUpload.State = upload.StateQueued
|
||||
res := StartUploadResponse{Success: true, Message: "upload queued"}
|
||||
resString, _ := json.Marshal(res)
|
||||
w.Write(resString)
|
||||
return
|
||||
} else if change == "skip" {
|
||||
anUpload.State = upload.StateSkipped
|
||||
anUpload.RemoveMarkupTempFile()
|
||||
res := StartUploadResponse{Success: true, Message: "upload skipped"}
|
||||
resString, _ := json.Marshal(res)
|
||||
w.Write(resString)
|
||||
return
|
||||
} else if change == "markup" {
|
||||
newImageData := r.FormValue("image")
|
||||
//data:image/png;base64,xxxx
|
||||
// I know this is dumb, we should just send binary image data, but I can't
|
||||
// see that Fabric makes that possible.
|
||||
if strings.Index(newImageData, "data:image/png;base64,") != 0 {
|
||||
returnJSONError(w, "bad image data")
|
||||
return
|
||||
}
|
||||
imageDataBase64 := newImageData[22:]
|
||||
b, err := base64.StdEncoding.DecodeString(imageDataBase64)
|
||||
if err != nil {
|
||||
returnJSONError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// write to a temporary file
|
||||
tempfile, err := ioutil.TempFile("", "dau_markup")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
n, err := tempfile.Write(b)
|
||||
if n != len(b) {
|
||||
log.Fatalf("only wrote %d bytes??", n)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Could not write temp file: %v", err)
|
||||
}
|
||||
|
||||
tempfile.Close()
|
||||
anUpload.MarkedUpFilename = tempfile.Name()
|
||||
|
||||
} else {
|
||||
returnJSONError(w, "bad change type")
|
||||
return
|
||||
}
|
||||
}
|
||||
res := StartUploadResponse{Success: false, Message: "upload does not exist, or already queued"}
|
||||
resString, _ := json.Marshal(res)
|
||||
w.WriteHeader(400)
|
||||
w.Write(resString)
|
||||
return
|
||||
}
|
||||
returnJSONError(w, "bad request")
|
||||
|
||||
}
|
||||
|
||||
func (ws *WebService) StartWebServer() {
|
||||
|
||||
http.HandleFunc("/", ws.getStatic)
|
||||
r := mux.NewRouter()
|
||||
|
||||
http.HandleFunc("/rest/logs", ws.getLogs)
|
||||
http.HandleFunc("/rest/uploads", ws.getUploads)
|
||||
http.HandleFunc("/rest/config", ws.handleConfig)
|
||||
r.HandleFunc("/rest/logs", ws.getLogs)
|
||||
r.HandleFunc("/rest/uploads", ws.getUploads)
|
||||
r.HandleFunc("/rest/upload/{id:[0-9]+}/{change}", ws.modifyUpload)
|
||||
|
||||
r.HandleFunc("/rest/image/{id:[0-9]+}/thumb", ws.imageThumb)
|
||||
r.HandleFunc("/rest/image/{id:[0-9]+}/markedup_thumb", ws.imageMarkedupThumb)
|
||||
|
||||
r.HandleFunc("/rest/image/{id:[0-9]+}", ws.image)
|
||||
|
||||
r.HandleFunc("/rest/config", ws.handleConfig)
|
||||
r.PathPrefix("/").HandlerFunc(ws.getStatic)
|
||||
|
||||
go func() {
|
||||
listen := fmt.Sprintf(":%d", ws.Config.Config.Port)
|
||||
log.Printf("Starting web server on http://localhost%s", listen)
|
||||
err := http.ListenAndServe(listen, nil) // set listen port
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: listen,
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
func returnJSONError(w http.ResponseWriter, errMessage string) {
|
||||
w.WriteHeader(400)
|
||||
errJSON := ErrorResponse{
|
||||
Error: errMessage,
|
||||
}
|
||||
errString, _ := json.Marshal(errJSON)
|
||||
w.Write(errString)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ func TestGetConfig(t *testing.T) {
|
||||
t.Errorf("expected error to be nil got %v", err)
|
||||
}
|
||||
|
||||
if string(b) != `{"WatchInterval":10,"Version":2,"Port":9090,"Watchers":[{"WebHookURL":"https://webhook.url.here","Path":"/your/screenshot/dir/here","Username":"","NoWatermark":false,"Exclude":[]}]}` {
|
||||
t.Errorf("Got unexpected response %v", string(b))
|
||||
exp := `{"WatchInterval":10,"Version":2,"Port":9090,"Watchers":[{"WebHookURL":"https://webhook.url.here","Path":"/your/screenshot/dir/here","Username":"","NoWatermark":false,"HoldUploads":false,"Exclude":[]}]}`
|
||||
if string(b) != exp {
|
||||
t.Errorf("Got unexpected response\n%v\n%v", string(b), exp)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user