Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79d14c00bc | |||
| f180900d79 | |||
| 2c4c9fdde6 | |||
| b9cacf6d33 | |||
| 6ad242e063 | |||
| 87d8222bc8 | |||
| 7670a0c5b7 | |||
| 2a3f4ea21a | |||
| 244cd7b9da | |||
| c2bbe13ca7 | |||
| 1ef5ed3ce4 | |||
| ec8b2453cd | |||
| 3776374747 | |||
| 6ece881a52 | |||
| 4534394abc | |||
| 4c963ba559 | |||
| c9f8ad60c3 | |||
| 4665380d15 | |||
| a739e62824 | |||
| c87d6ba79d | |||
| fd6f6884ee | |||
| 14ce147ec6 | |||
| 90f8c3588b | |||
| 3e6cf49394 | |||
| 1809033049 | |||
| f9614ffc48 | |||
| 26d4272aa2 | |||
| 2b06c37be8 | |||
| 8483fe7db9 | |||
| 7dddc92364 | |||
| 7f3161143f | |||
| e3a7fad7a9 | |||
| c85d134f7b | |||
| dd79dbed1d | |||
| 87acf0aefb | |||
| 3a65a60fcb | |||
| 1812486b19 | |||
| c47660addf | |||
| 2d0e294af6 | |||
| 283e0f3584 | |||
| 9ef6ab71c7 | |||
| 701583d3fd | |||
| 0c156a19f0 | |||
| cc54bb6469 | |||
| 71c097e578 | |||
| d23e31c0e0 | |||
| 9cb79a846e | |||
| a5ce0c7f63 | |||
| bcc4e145a2 | |||
| d8c0b7d0ea | |||
| fdf70daba7 | |||
| b69cdebf3b | |||
| 9e22490fe2 | |||
|
|
ae24f16631 | ||
|
|
b69acac0d0 | ||
|
|
c833f185cc | ||
|
|
b3ee0d9d1d | ||
| 23c0aa2a34 | |||
| 2d1ac3c803 | |||
| 942f81a378 | |||
| 287efab257 | |||
| 2b159e5532 | |||
| 851f073e99 | |||
| ec658520b7 | |||
| 6b1867f35f | |||
| 6e493522c8 | |||
| 4d09901fb3 | |||
| 9c9d4e492a | |||
| e1f5afa788 | |||
| 46a0f5a187 | |||
| 55bb5a8bae | |||
| c2b9bf410d | |||
| a4f958f846 | |||
| 14f8fe1933 | |||
| 752ff42a19 | |||
| 1ef062d19c | |||
| 6f09841209 | |||
| 4619bb5383 | |||
| e240f5dbd0 | |||
|
|
f976777f40 | ||
| 80a905b7d6 | |||
| 497d2e3e27 | |||
| ab54ace0d2 | |||
| 450765145b | |||
| 3970c611a4 | |||
| d8dc3e4ea8 | |||
| 3693d94297 | |||
| 8ded2b2e2d | |||
| e3e712d073 | |||
| 1ecac568f7 | |||
| 82ba3be742 | |||
|
|
4825dc56e6 | ||
| 65b9241492 | |||
|
|
73b33f5872 | ||
|
|
cc0fee57c2 | ||
|
|
05a3a0d09a | ||
|
|
72588642b6 | ||
|
|
7ff4685a70 | ||
|
|
f6b92ee8bd | ||
|
|
68d9ab7859 | ||
|
|
d2d7843b6f | ||
|
|
13589535a8 | ||
|
|
cb1f1d1a05 | ||
|
|
699ca9fcfc | ||
|
|
4e925136ba | ||
|
|
b37589985b | ||
| f60928fefb | |||
| 29fc0c67c9 | |||
| 6c3cb6066d | |||
| 1870313424 | |||
| 00218b6cc5 | |||
| fcf206b999 | |||
| 49c8ecd31a | |||
| 4c595d75d4 | |||
| 49d5ed58d0 | |||
| 77b7167d9c |
25
.github/workflows/go.yml
vendored
Normal file
25
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,20 +1,7 @@
|
||||
/blib/
|
||||
/.build/
|
||||
_build/
|
||||
cover_db/
|
||||
inc/
|
||||
Build
|
||||
!Build/
|
||||
Build.bat
|
||||
.last_cover_stats
|
||||
/Makefile
|
||||
/Makefile.old
|
||||
/MANIFEST.bak
|
||||
/META.yml
|
||||
/META.json
|
||||
/MYMETA.*
|
||||
nytprof.out
|
||||
/pm_to_blib
|
||||
*.o
|
||||
*.bs
|
||||
/_eumm/
|
||||
dist
|
||||
release
|
||||
discord-auto-upload
|
||||
discord-auto-upload.exe
|
||||
*.png
|
||||
*.jpg
|
||||
.DS_Store
|
||||
|
||||
17
BINARIES.md
17
BINARIES.md
@@ -1,17 +0,0 @@
|
||||
# Building "binaries"
|
||||
|
||||
For perl toolchain-free distribution.
|
||||
|
||||
Install PAR::Packer first, then:
|
||||
|
||||
## Mac
|
||||
|
||||
pp -M IO::Socket::SSL -o dau-mac dau
|
||||
|
||||
## Linux
|
||||
|
||||
pp -M IO::Socket::SSL -o dau-linux dau
|
||||
|
||||
## Windows
|
||||
|
||||
pp -M IO::Socket::SSL -o dau.exe dau
|
||||
69
CHANGELOG.md
Normal file
69
CHANGELOG.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.11.1] - 2021-10-11
|
||||
|
||||
- Improve logging and error handling
|
||||
- Improve tests
|
||||
- Fix problem where attachments too large for discord fail immediately and do not retry
|
||||
- Fix problem with version checking
|
||||
|
||||
## [v0.11.0] - 2021-10-10
|
||||
|
||||
- Switched to semantic versioning
|
||||
- Now supports multiple watchers - multiple directories can be monitored for new images
|
||||
- Complete UI rework to support new features and decrease ugliness
|
||||
- Add many tests
|
||||
|
||||
## [0.10.0] - 2021-06-08
|
||||
|
||||
This version adds a page showing recent uploads, with thumbnails.
|
||||
|
||||
This is not much use except as a log at this stage, but is the basis for future versions which will allow you to hold files before uploading, and edit them (crop, add text, etc) as well.
|
||||
|
||||
## [0.9.0] - 2021-06-04
|
||||
|
||||
Fix the version update check so that users are actually informed about new releases.
|
||||
|
||||
## [0.8.0] - 2021-06-03
|
||||
|
||||
This version makes the logs available in the web interface.
|
||||
|
||||
## [0.7.0] - 2021-02-09
|
||||
|
||||
The long awaited (!) web interface launches with this version. No more messing with command line arguments and .bat files.
|
||||
|
||||
Just run the exe and hit http://localhost:9090 to configure the app. See the updated README.md for more information on the configuration.
|
||||
|
||||
## [0.6.0] - 2017-02-28
|
||||
|
||||
Add --exclude option to avoid uploading files in thumbnail directories
|
||||
|
||||
## [0.5.0] - 2017-02-28
|
||||
|
||||
* Automatic watermarking of images to perform shameless self-promotion of this tool (disable with --no-watermark)
|
||||
* Automatically retry failed uploads
|
||||
* Internal cleanups
|
||||
|
||||
## [0.4.0] - 2017-02-28
|
||||
|
||||
* Fix crash if the specified directory did not exist
|
||||
* Better output for showing new version info
|
||||
* Show speed of upload
|
||||
|
||||
## [0.3.0] - 2017-02-21
|
||||
|
||||
* Support 'username' sending
|
||||
* Timeout on all HTTP connections
|
||||
* Default to current directory if --directory not specified
|
||||
|
||||
## [0.2.0] - 2017-02-21
|
||||
|
||||
* First golang version, improved output and parsing of responses.
|
||||
* Built in update checks.
|
||||
|
||||
## [0.1.0] - 2017-02-16
|
||||
|
||||
Initial release
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Justin Hawkins
|
||||
Copyright (c) 2021 Justin Hawkins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
107
README.md
107
README.md
@@ -1,6 +1,8 @@
|
||||
# Automatically upload screenshots from your computer into a discord channel
|
||||
# Automatically upload screenshots into a discord channel
|
||||
|
||||
This script automaticall uploads new screenshots that appear in a folder on your computer to Discord and posts them in a channel:
|
||||
[](https://github.com/tardisx/discord-auto-upload/actions/workflows/go.yml)
|
||||
|
||||
This program automatically uploads new screenshots that appear in a folder on your computer to Discord and posts them in a channel:
|
||||
|
||||

|
||||
|
||||
@@ -10,88 +12,83 @@ Point it at your Steam screenshot folder, or similar, and shortly after you hit
|
||||
|
||||
* A folder where screenshots are stored
|
||||
* A [discord webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks)
|
||||
* This script
|
||||
* perl installed (or the windows binary)
|
||||
* This program
|
||||
|
||||
## Getting started
|
||||
|
||||
### Linux
|
||||
### Binaries
|
||||
|
||||
* Download this script:
|
||||
Binaries are available for Mac, Linux and Windows [here](https://github.com/tardisx/discord-auto-upload/releases/latest).
|
||||
|
||||
`curl -O https://raw.githubusercontent.com/tardisx/discord-auto-upload/master/dau`
|
||||
#### From source
|
||||
|
||||
* Put it somewhere on your path (if you want to be able to run it from anywhere)
|
||||
* chmod +x it
|
||||
* Install the dependencies:
|
||||
|
||||
CPAN: `cpan install Mojolicious IO::Socket::SSL`
|
||||
|
||||
CPANM: `cpanm Mojolicious IO::Socket::SSL`
|
||||
|
||||
Ubuntu/Debian: `sudo apt-get install libmojolicious-perl libio-socket-ssl-perl`
|
||||
|
||||
* test it:
|
||||
|
||||
`dau --help`
|
||||
|
||||
### Mac
|
||||
|
||||
Basically the same as Linux above. [Perlbrew](https://perlbrew.pl) is highly recommended so as not to disturb the system perl. No need for superuser access then either.
|
||||
|
||||
### Windows
|
||||
|
||||
* Grab the windows exe file:
|
||||
|
||||
`http://tba`
|
||||
|
||||
* Optional, put it somewhere on your path
|
||||
* Open a command prompt
|
||||
* Test it
|
||||
|
||||
`\some\path\dau --help`
|
||||
|
||||
If you want to hack it, audit it, or don't trust my exe, you can install
|
||||
[Strawberry Perl](http://strawberryperl.com) and run it using that directly.
|
||||
You'll need the same dependencies mentioned above in the Linux setup.
|
||||
You'll need to [download Go](https://golang.org/dl/), check the code out somewhere, run 'go generate' and then 'go build'.
|
||||
|
||||
## Using it
|
||||
|
||||
`dau` is a command line driven program. When executed, it will continually scan a directory for new images, and each time it finds one it will upload it to discord, via the discord web hook.
|
||||
`dau` configuration is managed via its internal web interface. When the executable is run, you can visit
|
||||
`http://localhost:9090` in your web browser to configure the it. Configuration persists across runs, it is
|
||||
saved in a file called '.dau.json' in your home directory.
|
||||
|
||||
The first time you run it, you will need to configure at least the discord web hook and the watch path for
|
||||
`dau` to be useful.
|
||||
|
||||
While running, `dau` will continually scan a directory for new images, and each time it finds one it will upload it to discord, via the discord web hook.
|
||||
|
||||
`dau` will only upload "new" screenshots, where "new" means a file that appears in a directory that it is watching, if it appears *after* it has started executing.
|
||||
|
||||
Thus, you do not have to worry about pointing `dau` at a directory full of images, it will only upload new ones.
|
||||
|
||||
If `dau` is on your path, you can run it from your screenshot folder and there is then no need to specify the path to your images.
|
||||
## Configuration options
|
||||
|
||||
Note that currently `dau` does not look in subdirectories. Please submit an issue if this is a use case for you.
|
||||
See the web interface at http://localhost:9090 to configure `dau`.
|
||||
|
||||
The only mandatory command line parameter is the discord webhook URL:
|
||||
### 'Discord WebHook URL'
|
||||
|
||||
`--webhook URL` - the webhook URL (see [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for details).
|
||||
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.
|
||||
|
||||
Other parameters are:
|
||||
### 'Bot Username'
|
||||
|
||||
`--watch xx` - specify how many seconds to wait between scanning the directory. The default is 10 seconds.
|
||||
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.
|
||||
|
||||
`--directory <somedir>` - the directory to watch for images to appear in. If this option is not supplied, will look in the current directory.
|
||||
### 'Directory to watch'
|
||||
|
||||
You will have to quote the path on windows, or anywhere where the directory path contains spaces.
|
||||
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.
|
||||
|
||||
`--username` - supply a 'username' with the webhook submission. Slightly misleading, it basically provides some extra text next to the "Bot" display on the upload to the channel.
|
||||
### 'Period between filesystem checks'
|
||||
|
||||
In the example screenshot, this was set to "tardisx uploaded from EDD".
|
||||
This is the number of seconds between which `dau` will look for new images.
|
||||
|
||||
`--debug` - provide extra debugging.
|
||||
### 'Do not watermark images'
|
||||
|
||||
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.
|
||||
|
||||
## Limitations/bugs
|
||||
|
||||
* Only files ending jpg, gif or png are uploaded.
|
||||
* Subdirectories are not scanned.
|
||||
* If multiple screenshots occur quickly (<1 second apart) not all may be uploaded.
|
||||
* Files to upload are determined by the file modification time. If you drag and drop existing files they will
|
||||
not be detected and uploaded. Only newly created files will be detected.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Please check the "log" page on the web interface for information when things are
|
||||
not working as you expect.
|
||||
|
||||
## TODO
|
||||
This is just a quick hack. Open to suggestions on new features and improvements.
|
||||
|
||||
Open an [issue](https://github.com/tardisx/discord-auto-upload/issues/new) and let me know.
|
||||
Open an [issue](https://github.com/tardisx/discord-auto-upload/issues/new) and let me know what you'd like to see.
|
||||
|
||||
Please include any relevant logs from the console when reporting bugs.
|
||||
|
||||
45
build-release.pl
Executable file
45
build-release.pl
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
open my $fh, "<", "version/version.go" || die $!;
|
||||
|
||||
my $version;
|
||||
while (<$fh>) {
|
||||
$version = $1 if /^const\s+CurrentVersion.*?"(v[\d\.]+)"/;
|
||||
}
|
||||
close $fh;
|
||||
|
||||
die "no version?" unless defined $version;
|
||||
|
||||
# quit if tests fail
|
||||
system("go test ./...") && die "not building release with failing tests";
|
||||
|
||||
# so lazy
|
||||
system "rm", "-rf", "release", "dist";
|
||||
system "mkdir", "release";
|
||||
system "mkdir", "dist";
|
||||
|
||||
my %build = (
|
||||
win => { env => { GOOS => 'windows', GOARCH => 'amd64' }, filename => 'dau.exe' },
|
||||
linux => { env => { GOOS => 'linux', GOARCH => 'amd64' }, filename => 'dau' },
|
||||
mac => { env => { GOOS => 'darwin', GOARCH => 'amd64' }, filename => 'dau' },
|
||||
);
|
||||
|
||||
foreach my $type (keys %build) {
|
||||
mkdir "release/$type";
|
||||
}
|
||||
|
||||
add_extras();
|
||||
|
||||
foreach my $type (keys %build) {
|
||||
local $ENV{GOOS} = $build{$type}->{env}->{GOOS};
|
||||
local $ENV{GOARCH} = $build{$type}->{env}->{GOARCH};
|
||||
system "go", "build", "-o", "release/$type/" . $build{$type}->{filename};
|
||||
system "zip", "-j", "dist/dau-$type-$version.zip", ( glob "release/$type/*" );
|
||||
}
|
||||
|
||||
sub add_extras {
|
||||
# we used to have a .bat file here, but no longer needed
|
||||
}
|
||||
177
config/config.go
Normal file
177
config/config.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// Config for the application
|
||||
type ConfigV1 struct {
|
||||
WebHookURL string
|
||||
Path string
|
||||
Watch int
|
||||
Username string
|
||||
NoWatermark bool
|
||||
Exclude string
|
||||
}
|
||||
|
||||
type Watcher struct {
|
||||
WebHookURL string
|
||||
Path string
|
||||
Username string
|
||||
NoWatermark bool
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
type ConfigV2 struct {
|
||||
WatchInterval int
|
||||
Version int
|
||||
Port int
|
||||
Watchers []Watcher
|
||||
}
|
||||
|
||||
type ConfigService struct {
|
||||
Config *ConfigV2
|
||||
Changed chan bool
|
||||
ConfigFilename string
|
||||
}
|
||||
|
||||
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 (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)
|
||||
c.Config = DefaultConfig()
|
||||
return c.Save()
|
||||
} else {
|
||||
return c.Load()
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultConfig() *ConfigV2 {
|
||||
c := ConfigV2{}
|
||||
c.Version = 2
|
||||
c.WatchInterval = 10
|
||||
c.Port = 9090
|
||||
w := Watcher{
|
||||
WebHookURL: "https://webhook.url.here",
|
||||
Path: "/your/screenshot/dir/here",
|
||||
Username: "",
|
||||
NoWatermark: false,
|
||||
Exclude: []string{},
|
||||
}
|
||||
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 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 (c *ConfigService) Save() error {
|
||||
daulog.SendLog("saving configuration", daulog.LogTypeInfo)
|
||||
// sanity checks
|
||||
for _, watcher := range c.Config.Watchers {
|
||||
|
||||
// give the sample one a pass? this is kinda gross...
|
||||
if watcher.Path == "/your/screenshot/dir/here" {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(watcher.Path)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("path '%s' does not exist", watcher.Path)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("path '%s' is not a directory", watcher.Path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, watcher := range c.Config.Watchers {
|
||||
if strings.Index(watcher.WebHookURL, "https://") != 0 {
|
||||
return fmt.Errorf("webhook URL '%s' does not look valid", watcher.WebHookURL)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Config.WatchInterval < 1 {
|
||||
return fmt.Errorf("watch interval should be greater than 0 - '%d' invalid", c.Config.WatchInterval)
|
||||
}
|
||||
|
||||
jsonString, _ := json.Marshal(c.Config)
|
||||
err := ioutil.WriteFile(c.ConfigFilename, jsonString, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot save config %s: %s", c.ConfigFilename, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
dir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func defaultConfigPath() string {
|
||||
homeDir := homeDir()
|
||||
return homeDir + string(os.PathSeparator) + ".dau.json"
|
||||
}
|
||||
97
config/config_test.go
Normal file
97
config/config_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoConfig(t *testing.T) {
|
||||
c := ConfigService{}
|
||||
|
||||
c.ConfigFilename = emptyTempFile()
|
||||
os.Remove(c.ConfigFilename)
|
||||
defer os.Remove(c.ConfigFilename) // because we are about to create it
|
||||
|
||||
err := c.LoadOrInit()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected failure from load: %s", err)
|
||||
}
|
||||
|
||||
if c.Config.Version != 2 {
|
||||
t.Error("not version 2 starting config")
|
||||
}
|
||||
|
||||
if fileSize(c.ConfigFilename) < 40 {
|
||||
t.Errorf("File is too small %d bytes", fileSize(c.ConfigFilename))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEmptyFileConfig(t *testing.T) {
|
||||
c := ConfigService{}
|
||||
|
||||
c.ConfigFilename = emptyTempFile()
|
||||
defer os.Remove(c.ConfigFilename)
|
||||
|
||||
err := c.LoadOrInit()
|
||||
if err == nil {
|
||||
t.Error("unexpected success from LoadOrInit()")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
f, err := ioutil.TempFile("", "dautest-*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func fileSize(file string) int {
|
||||
fi, err := os.Stat(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(fi.Size())
|
||||
|
||||
}
|
||||
128
dau
128
dau
@@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use feature 'say';
|
||||
|
||||
use Mojo::UserAgent;
|
||||
use Getopt::Long qw/GetOptions/;
|
||||
use Data::Dumper qw/Dumper/;
|
||||
|
||||
my $webhook_url;
|
||||
my $directory = "./";
|
||||
my $username;
|
||||
my $watch = 10;
|
||||
my $help;
|
||||
my $debug;
|
||||
my $now = time();
|
||||
my $error_count = 0;
|
||||
my $error_max = 10;
|
||||
my $version = '0.1';
|
||||
|
||||
GetOptions(
|
||||
"webhook=s" => \$webhook_url,
|
||||
"directory=s" => \$directory,
|
||||
"username=s" => \$username,
|
||||
"watch=i" => \$watch,
|
||||
"help" => \$help,
|
||||
"debug" => \$debug,
|
||||
) || die usage();
|
||||
|
||||
usage() if $help;
|
||||
|
||||
if (! $webhook_url) {
|
||||
usage("--webhook must be supplied");
|
||||
}
|
||||
|
||||
sub usage {
|
||||
my $error = shift || "";
|
||||
my $indent = " " x length($0);
|
||||
say "dau $version - https://github.com/tardisx/discord-auto-upload\n";
|
||||
say "$0 --webhook <url> [--directory </some/path>]";
|
||||
say "$indent [--username <\"custom username\">] [--watch <n>]\n";
|
||||
say "The current directory will be used if no directory is specified.\n";
|
||||
say "error: $error" if $error;
|
||||
exit defined $error ? 1 : 0;
|
||||
}
|
||||
|
||||
chdir $directory || die "cannot chdir to $directory: $!\n";
|
||||
|
||||
watch_dir();
|
||||
|
||||
sub watch_dir {
|
||||
while (1) {
|
||||
my @files = glob("*");
|
||||
@files = grep { qualifies($_) } @files;
|
||||
foreach my $file (sort { mtime($a) <=> mtime($b) } @files) {
|
||||
debug("examining $file");
|
||||
if (mtime($file) > $now) {
|
||||
$now = mtime($file);
|
||||
upload($file);
|
||||
}
|
||||
}
|
||||
sleep $watch;
|
||||
}
|
||||
}
|
||||
|
||||
sub mtime {
|
||||
my $f = shift;
|
||||
return (stat($f))[9];
|
||||
}
|
||||
|
||||
sub qualifies {
|
||||
my $filename = shift;
|
||||
return 1 if ($filename =~ /\.jpg$|\.gif$|\.png$/i);
|
||||
return;
|
||||
}
|
||||
|
||||
sub info {
|
||||
say "- " . shift;
|
||||
}
|
||||
|
||||
sub debug {
|
||||
return unless $debug;
|
||||
say "! " . shift;
|
||||
}
|
||||
|
||||
sub error {
|
||||
say "* " . shift;
|
||||
}
|
||||
|
||||
sub upload {
|
||||
my $file = shift;
|
||||
info("uploading $file");
|
||||
|
||||
my $ua = Mojo::UserAgent->new;
|
||||
my $data = {
|
||||
upload => { file => $file },
|
||||
$username ? ( username => $username ) : ()
|
||||
};
|
||||
|
||||
my $tx = $ua->post($webhook_url, form => $data);
|
||||
|
||||
if (my $res = $tx->success) {
|
||||
debug(Dumper($res->json));
|
||||
my $url = $res->json->{attachments}->[0]->{url};
|
||||
my $size = $res->json->{attachments}->[0]->{size};
|
||||
my $width = $res->json->{attachments}->[0]->{width};
|
||||
my $height = $res->json->{attachments}->[0]->{height};
|
||||
info("uploaded ${width}x${height} $size bytes images to $url, submitted to webhook successfully");
|
||||
}
|
||||
else {
|
||||
debug(Dumper($tx));
|
||||
|
||||
my $err = $tx->error;
|
||||
if ($err->{code}) {
|
||||
error("$err->{code} response: $err->{message}");
|
||||
}
|
||||
else {
|
||||
error("Connection error: $err->{message}");
|
||||
}
|
||||
$error_count++;
|
||||
if ($error_count >= $error_max) {
|
||||
error("Sorry - too many errors - quitting");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
228
dau.go
Normal file
228
dau.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
// "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/upload"
|
||||
|
||||
// "github.com/tardisx/discord-auto-upload/upload"
|
||||
"github.com/tardisx/discord-auto-upload/version"
|
||||
"github.com/tardisx/discord-auto-upload/web"
|
||||
)
|
||||
|
||||
type watch struct {
|
||||
lastCheck time.Time
|
||||
newLastCheck time.Time
|
||||
config config.Watcher
|
||||
uploader *upload.Uploader
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
parseOptions()
|
||||
|
||||
// grab the config, register to notice changes
|
||||
config := config.DefaultConfigService()
|
||||
configChanged := make(chan bool)
|
||||
config.Changed = configChanged
|
||||
config.LoadOrInit()
|
||||
|
||||
// create the uploader
|
||||
up := upload.NewUploader()
|
||||
|
||||
// log.Print("Opening web browser")
|
||||
// open.Start("http://localhost:9090")
|
||||
web := web.WebService{Config: config, Uploader: up}
|
||||
web.StartWebServer()
|
||||
|
||||
go func() { checkUpdates() }()
|
||||
|
||||
// create the watchers, restart them if config changes
|
||||
// blocks forever
|
||||
startWatchers(config, up, configChanged)
|
||||
|
||||
}
|
||||
|
||||
func startWatchers(config *config.ConfigService, up *upload.Uploader, configChange chan bool) {
|
||||
for {
|
||||
daulog.SendLog("Creating watchers", daulog.LogTypeInfo)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
for _, c := range config.Config.Watchers {
|
||||
log.Printf("Creating watcher for %s interval %d", c.Path, config.Config.WatchInterval)
|
||||
watcher := watch{uploader: up, lastCheck: time.Now(), newLastCheck: time.Now(), config: c}
|
||||
go watcher.Watch(config.Config.WatchInterval, ctx)
|
||||
}
|
||||
// wait for single that the config changed
|
||||
<-configChange
|
||||
cancel()
|
||||
daulog.SendLog("starting new watchers due to config change", daulog.LogTypeInfo)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (w *watch) Watch(interval int, ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
daulog.SendLog("Killing old watcher", daulog.LogTypeInfo)
|
||||
return
|
||||
default:
|
||||
newFiles := w.ProcessNewFiles()
|
||||
for _, f := range newFiles {
|
||||
w.uploader.AddFile(f, w.config)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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, w.config.Exclude)
|
||||
})
|
||||
|
||||
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 {
|
||||
daulog.SendLog(fmt.Sprintf("Problem with path '%s': %s", w.config.Path, err), daulog.LogTypeError)
|
||||
return false
|
||||
}
|
||||
if !src.IsDir() {
|
||||
daulog.SendLog(fmt.Sprintf("Problem with path '%s': is not a directory", w.config.Path), daulog.LogTypeError)
|
||||
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 eligible, not excluded and new enough to care we add it
|
||||
// to the passed in array of files
|
||||
func (w *watch) checkFile(path string, found *[]string, exclusions []string) error {
|
||||
|
||||
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() {
|
||||
excluded := false
|
||||
for _, exclusion := range exclusions {
|
||||
if strings.Contains(path, exclusion) {
|
||||
excluded = true
|
||||
}
|
||||
}
|
||||
if !excluded {
|
||||
*found = append(*found, path)
|
||||
}
|
||||
}
|
||||
|
||||
if w.newLastCheck.Before(fi.ModTime()) {
|
||||
w.newLastCheck = fi.ModTime()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUpdates() {
|
||||
|
||||
type GithubRelease struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
daulog.SendLog("checking for new version", daulog.LogTypeInfo)
|
||||
|
||||
client := &http.Client{Timeout: time.Second * 5}
|
||||
resp, err := client.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest")
|
||||
if err != nil {
|
||||
daulog.SendLog(fmt.Sprintf("WARNING: Update check failed: %v", err), daulog.LogTypeError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatal("could not check read update response")
|
||||
}
|
||||
|
||||
var latest GithubRelease
|
||||
err = json.Unmarshal(body, &latest)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("could not parse JSON: ", err)
|
||||
}
|
||||
|
||||
// pre v0.11.0 version (ie before semver) did a simple string comparison,
|
||||
// but since "0.10.0" < "v0.11.0" they should still get prompted to upgrade
|
||||
// ok
|
||||
if version.NewVersionAvailable(latest.TagName) {
|
||||
fmt.Printf("You are currently on version %s, but version %s is available\n", version.CurrentVersion, latest.TagName)
|
||||
fmt.Println("----------- Release Info -----------")
|
||||
fmt.Println(latest.Body)
|
||||
fmt.Println("------------------------------------")
|
||||
fmt.Println("Upgrade at https://github.com/tardisx/discord-auto-upload/releases/latest")
|
||||
daulog.SendLog(fmt.Sprintf("New version available: %s - download at https://github.com/tardisx/discord-auto-upload/releases/latest", latest.TagName), daulog.LogTypeInfo)
|
||||
}
|
||||
|
||||
daulog.SendLog("already running latest version", daulog.LogTypeInfo)
|
||||
}
|
||||
|
||||
func parseOptions() {
|
||||
var versionFlag bool
|
||||
flag.BoolVar(&versionFlag, "version", false, "show version")
|
||||
flag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
|
||||
fmt.Printf("Version: %s\n", version.CurrentVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
}
|
||||
99
dau_test.go
Normal file
99
dau_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tardisx/discord-auto-upload/config"
|
||||
"github.com/tardisx/discord-auto-upload/upload"
|
||||
)
|
||||
|
||||
func TestWatchNewFiles(t *testing.T) {
|
||||
dir := createFileTree()
|
||||
defer os.RemoveAll(dir)
|
||||
time.Sleep(time.Second)
|
||||
|
||||
w := watch{
|
||||
config: config.Watcher{Path: dir},
|
||||
uploader: upload.NewUploader(),
|
||||
lastCheck: time.Now(),
|
||||
newLastCheck: time.Now(),
|
||||
}
|
||||
files := w.ProcessNewFiles()
|
||||
if len(files) != 0 {
|
||||
t.Errorf("was not zero files (%d): %v", len(files), files)
|
||||
}
|
||||
|
||||
// create a new file
|
||||
time.Sleep(time.Second)
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "b.gif"))
|
||||
files = w.ProcessNewFiles()
|
||||
if len(files) != 1 {
|
||||
t.Errorf("was not one file - got: %v", files)
|
||||
}
|
||||
if files[0] != fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "b.gif") {
|
||||
t.Error("wrong file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExclsion(t *testing.T) {
|
||||
dir := createFileTree()
|
||||
defer os.RemoveAll(dir)
|
||||
time.Sleep(time.Second)
|
||||
|
||||
w := watch{
|
||||
config: config.Watcher{Path: dir, Exclude: []string{"thumb", "tiny"}},
|
||||
uploader: upload.NewUploader(),
|
||||
lastCheck: time.Now(),
|
||||
newLastCheck: time.Now(),
|
||||
}
|
||||
files := w.ProcessNewFiles()
|
||||
if len(files) != 0 {
|
||||
t.Errorf("was not zero files (%d): %v", len(files), files)
|
||||
}
|
||||
// create a new file that would not hit exclusion, and two that would
|
||||
time.Sleep(time.Second)
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "b.gif"))
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "b_thumb.gif"))
|
||||
os.Create(fmt.Sprintf("%s%c%s", dir, os.PathSeparator, "tiny_b.jpg"))
|
||||
files = w.ProcessNewFiles()
|
||||
if len(files) != 1 {
|
||||
t.Error("was not one new file")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCheckPath(t *testing.T) {
|
||||
dir := createFileTree()
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
w := watch{
|
||||
config: config.Watcher{Path: dir},
|
||||
uploader: upload.NewUploader(),
|
||||
lastCheck: time.Now(),
|
||||
newLastCheck: time.Now(),
|
||||
}
|
||||
if !w.checkPath() {
|
||||
t.Error("checkPath failed?")
|
||||
}
|
||||
os.RemoveAll(dir)
|
||||
if w.checkPath() {
|
||||
t.Error("checkPath succeeded when shouldn't?")
|
||||
}
|
||||
}
|
||||
|
||||
func createFileTree() string {
|
||||
dir, err := ioutil.TempDir("", "dau-test")
|
||||
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"))
|
||||
|
||||
return dir
|
||||
}
|
||||
11
go.mod
Normal file
11
go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module github.com/tardisx/discord-auto-upload
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // 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
|
||||
)
|
||||
21
go.sum
Normal file
21
go.sum
Normal file
@@ -0,0 +1,21 @@
|
||||
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/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=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
46
log/log.go
Normal file
46
log/log.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogEntryType string
|
||||
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time `json:"ts"`
|
||||
Type LogEntryType `json:"type"`
|
||||
Entry string `json:"log"`
|
||||
}
|
||||
|
||||
const (
|
||||
LogTypeInfo = "info"
|
||||
LogTypeError = "error"
|
||||
LogTypeDebug = "debug"
|
||||
)
|
||||
|
||||
var LogEntries []LogEntry
|
||||
var logInput chan LogEntry
|
||||
|
||||
func init() {
|
||||
// wait for log entries
|
||||
logInput = make(chan LogEntry)
|
||||
go func() {
|
||||
for {
|
||||
aLog := <-logInput
|
||||
LogEntries = append(LogEntries, aLog)
|
||||
for len(LogEntries) > 100 {
|
||||
LogEntries = LogEntries[1:]
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func SendLog(entry string, entryType LogEntryType) {
|
||||
logInput <- LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Entry: entry,
|
||||
Type: entryType,
|
||||
}
|
||||
log.Printf("%6s: %s", entryType, entry)
|
||||
}
|
||||
305
upload/upload.go
Normal file
305
upload/upload.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// Package upload encapsulates prepping an image for sending to discord,
|
||||
// and actually uploading it there.
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/tardisx/discord-auto-upload/config"
|
||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||
"golang.org/x/image/font/inconsolata"
|
||||
)
|
||||
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type Uploader struct {
|
||||
Uploads []*Upload `json:"uploads"`
|
||||
}
|
||||
|
||||
type Upload struct {
|
||||
Uploaded bool `json:"uploaded"` // has this file been uploaded to discord
|
||||
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
|
||||
|
||||
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"`
|
||||
|
||||
Client HTTPClient `json:"-"`
|
||||
}
|
||||
|
||||
func NewUploader() *Uploader {
|
||||
u := Uploader{}
|
||||
uploads := make([]*Upload, 0)
|
||||
u.Uploads = uploads
|
||||
return &u
|
||||
}
|
||||
|
||||
func (u *Uploader) AddFile(file string, conf config.Watcher) {
|
||||
thisUpload := Upload{
|
||||
Uploaded: false,
|
||||
originalFilename: file,
|
||||
watermark: !conf.NoWatermark,
|
||||
webhookURL: conf.WebHookURL,
|
||||
usernameOverride: conf.Username,
|
||||
}
|
||||
u.Uploads = append(u.Uploads, &thisUpload)
|
||||
}
|
||||
|
||||
// Upload uploads any files that have not yet been uploaded
|
||||
func (u *Uploader) Upload() {
|
||||
|
||||
for _, upload := range u.Uploads {
|
||||
if !upload.Uploaded && !upload.Failed {
|
||||
upload.processUpload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Upload) processUpload() error {
|
||||
daulog.SendLog(fmt.Sprintf("Uploading: %s", u.originalFilename), daulog.LogTypeInfo)
|
||||
|
||||
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 != "" {
|
||||
daulog.SendLog("Overriding username with "+u.usernameOverride, daulog.LogTypeInfo)
|
||||
extraParams["username"] = u.usernameOverride
|
||||
}
|
||||
|
||||
type DiscordAPIResponseAttachment struct {
|
||||
URL string
|
||||
ProxyURL string
|
||||
Size int
|
||||
Width int
|
||||
Height int
|
||||
Filename string
|
||||
}
|
||||
|
||||
type DiscordAPIResponse struct {
|
||||
Attachments []DiscordAPIResponseAttachment
|
||||
ID int64 `json:",string"`
|
||||
}
|
||||
|
||||
var retriesRemaining = 5
|
||||
for retriesRemaining > 0 {
|
||||
|
||||
request, err := newfileUploadRequest(u.webhookURL, extraParams, "file", u.filenameToUpload)
|
||||
if err != nil {
|
||||
log.Printf("error creating upload request: %s", err)
|
||||
return fmt.Errorf("could not create upload request: %s", err)
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
if u.Client == nil {
|
||||
// if no client was specified (a unit test) then create
|
||||
// a default one
|
||||
u.Client = &http.Client{Timeout: time.Second * 30}
|
||||
}
|
||||
|
||||
resp, err := u.Client.Do(request)
|
||||
if err != nil {
|
||||
log.Print("Error performing request:", err)
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
} else {
|
||||
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)
|
||||
return errors.New("received 413 - file too large")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
// {"message": "Request entity too large", "code": 40005}
|
||||
log.Print("Bad response from server:", resp.StatusCode)
|
||||
if b, err := ioutil.ReadAll(resp.Body); err == nil {
|
||||
log.Print("Body:", string(b))
|
||||
daulog.SendLog(fmt.Sprintf("Bad response: %s", string(b)), daulog.LogTypeError)
|
||||
}
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
|
||||
resBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Print("could not deal with body: ", err)
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var res DiscordAPIResponse
|
||||
err = json.Unmarshal(resBody, &res)
|
||||
|
||||
// {"id": "851092588608880670", "type": 0, "content": "", "channel_id": "849615269706203171", "author": {"bot": true, "id": "849615314274484224", "username": "abcdedf", "avatar": null, "discriminator": "0000"}, "attachments": [{"id": "851092588332449812", "filename": "dau480457962.png", "size": 859505, "url": "https://cdn.discordapp.com/attachments/849615269706203171/851092588332449812/dau480457962.png", "proxy_url": "https://media.discordapp.net/attachments/849615269706203171/851092588332449812/dau480457962.png", "width": 640, "height": 640, "content_type": "image/png"}], "embeds": [], "mentions": [], "mention_roles": [], "pinned": false, "mention_everyone": false, "tts": false, "timestamp": "2021-06-06T13:38:05.660000+00:00", "edited_timestamp": null, "flags": 0, "components": [], "webhook_id": "849615314274484224"}
|
||||
|
||||
daulog.SendLog(fmt.Sprintf("Response: %s", string(resBody[:])), daulog.LogTypeDebug)
|
||||
|
||||
if err != nil {
|
||||
log.Print("could not parse JSON: ", err)
|
||||
fmt.Println("Response was:", string(resBody[:]))
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
if len(res.Attachments) < 1 {
|
||||
log.Print("bad response - no attachments?")
|
||||
retriesRemaining--
|
||||
sleepForRetries(retriesRemaining)
|
||||
continue
|
||||
}
|
||||
var a = res.Attachments[0]
|
||||
elapsed := time.Since(start)
|
||||
rate := float64(a.Size) / elapsed.Seconds() / 1024.0
|
||||
|
||||
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)
|
||||
|
||||
u.Url = a.URL
|
||||
u.Uploaded = true
|
||||
u.Width = a.Width
|
||||
u.Height = a.Height
|
||||
u.UploadedAt = time.Now()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if u.watermark {
|
||||
daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", u.filenameToUpload), daulog.LogTypeDebug)
|
||||
os.Remove(u.filenameToUpload)
|
||||
}
|
||||
|
||||
if retriesRemaining == 0 {
|
||||
daulog.SendLog("Failed to upload, even after all retries", daulog.LogTypeError)
|
||||
u.Failed = true
|
||||
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()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = io.Copy(part, file)
|
||||
if err != nil {
|
||||
log.Fatal("Could not copy: ", err)
|
||||
}
|
||||
|
||||
for key, val := range params {
|
||||
_ = writer.WriteField(key, val)
|
||||
}
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", uri, body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (u *Upload) applyWatermark() {
|
||||
|
||||
reader, err := os.Open(u.originalFilename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
im, _, err := image.Decode(reader)
|
||||
if err != nil {
|
||||
daulog.SendLog(fmt.Sprintf("Cannot decode image: %v - skipping watermarking", err), daulog.LogTypeError)
|
||||
u.watermark = false
|
||||
u.filenameToUpload = u.originalFilename
|
||||
return
|
||||
}
|
||||
bounds := im.Bounds()
|
||||
// var S float64 = float64(bounds.Max.X)
|
||||
|
||||
dc := gg.NewContext(bounds.Max.X, bounds.Max.Y)
|
||||
dc.Clear()
|
||||
dc.SetRGB(0, 0, 0)
|
||||
|
||||
dc.SetFontFace(inconsolata.Regular8x16)
|
||||
|
||||
dc.DrawImage(im, 0, 0)
|
||||
|
||||
dc.DrawRoundedRectangle(0, float64(bounds.Max.Y-18.0), 320, float64(bounds.Max.Y), 0)
|
||||
dc.SetRGB(0, 0, 0)
|
||||
dc.Fill()
|
||||
|
||||
dc.SetRGB(1, 1, 1)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func sleepForRetries(retry int) {
|
||||
if retry == 0 {
|
||||
return
|
||||
}
|
||||
retryTime := (6-retry)*(6-retry) + 6
|
||||
daulog.SendLog(fmt.Sprintf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry), daulog.LogTypeError)
|
||||
time.Sleep(time.Duration(retryTime) * time.Second)
|
||||
}
|
||||
69
upload/upload_test.go
Normal file
69
upload/upload_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
// "github.com/tardisx/discord-auto-upload/config"
|
||||
)
|
||||
|
||||
// https://www.thegreatcodeadventure.com/mocking-http-requests-in-golang/
|
||||
type MockClient struct {
|
||||
DoFunc func(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (m *MockClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return m.DoFunc(req)
|
||||
}
|
||||
|
||||
func DoGoodUpload(req *http.Request) (*http.Response, error) {
|
||||
r := ioutil.NopCloser(bytes.NewReader([]byte(`{"id": "123456789012345678", "type": 0, "content": "", "channel_id": "849615269706203171", "author": {"bot": true, "id": "849615314274484224", "username": "abcdedf", "avatar": null, "discriminator": "0000"}, "attachments": [{"id": "851092588332449812", "filename": "dau480457962.png", "size": 859505, "url": "https://cdn.discordapp.com/attachments/849615269706203171/851092588332449812/dau480457962.png", "proxy_url": "https://media.discordapp.net/attachments/849615269706203171/851092588332449812/dau480457962.png", "width": 640, "height": 640, "content_type": "image/png"}], "embeds": [], "mentions": [], "mention_roles": [], "pinned": false, "mention_everyone": false, "tts": false, "timestamp": "2021-06-06T13:38:05.660000+00:00", "edited_timestamp": null, "flags": 0, "components": [], "webhook_id": "123456789012345678"}`)))
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: r,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DoTooBigUpload(req *http.Request) (*http.Response, error) {
|
||||
r := ioutil.NopCloser(bytes.NewReader([]byte(`{"message": "Request entity too large", "code": 40005}`)))
|
||||
return &http.Response{
|
||||
StatusCode: 413,
|
||||
Body: r,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestSuccessfulUpload(t *testing.T) {
|
||||
// create temporary file, processUpload requires that it exists, even though
|
||||
// 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.Client = &MockClient{DoFunc: DoGoodUpload}
|
||||
err := u.processUpload()
|
||||
if err != nil {
|
||||
t.Errorf("error occured: %s", err)
|
||||
}
|
||||
if u.Width != 640 || u.Height != 640 {
|
||||
t.Error("dimensions wrong")
|
||||
}
|
||||
if u.Url != "https://cdn.discordapp.com/attachments/849615269706203171/851092588332449812/dau480457962.png" {
|
||||
t.Error("URL wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTooBigUpload(t *testing.T) {
|
||||
// create temporary file, processUpload requires that it exists, even though
|
||||
// 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.Client = &MockClient{DoFunc: DoTooBigUpload}
|
||||
err := u.processUpload()
|
||||
if err == nil {
|
||||
t.Error("error did not occur?")
|
||||
} else if err.Error() != "received 413 - file too large" {
|
||||
t.Errorf("wrong error occurred: %s", err.Error())
|
||||
}
|
||||
}
|
||||
29
version/version.go
Normal file
29
version/version.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const CurrentVersion string = "v0.11.1"
|
||||
|
||||
func NewVersionAvailable(v string) bool {
|
||||
if !semver.IsValid(CurrentVersion) {
|
||||
panic(fmt.Sprintf("my current version '%s' is not valid", CurrentVersion))
|
||||
}
|
||||
if !semver.IsValid(v) {
|
||||
// maybe this should just be a warning
|
||||
log.Printf("passed in version '%s' is not valid - assuming no new version", v)
|
||||
return false
|
||||
}
|
||||
comp := semver.Compare(v, CurrentVersion)
|
||||
if comp == 0 {
|
||||
return false
|
||||
}
|
||||
if comp == 1 {
|
||||
return true
|
||||
}
|
||||
return false // they are using a newer one than exists?
|
||||
}
|
||||
11
version/version_test.go
Normal file
11
version/version_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersioning(t *testing.T) {
|
||||
if !NewVersionAvailable("v1.0.0") {
|
||||
t.Error("should be a version newer than v1.0.0")
|
||||
}
|
||||
}
|
||||
196
web/data/config.html
Normal file
196
web/data/config.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{{ define "content" }}
|
||||
|
||||
<main role="main" class="inner DAU" x-data="configuration()" x-init="get_config()">
|
||||
<h1 class="DAU-heading">Config</h1>
|
||||
|
||||
<div x-cloak x-show="error" class="alert alert-danger" role="alert" x-text="error">
|
||||
</div>
|
||||
<div x-cloak x-show="success" class="alert alert-success" role="alert" x-text="success">
|
||||
</div>
|
||||
|
||||
<form x-cloak class="">
|
||||
|
||||
<p>Configuration changes are not made until the Save button is pressed
|
||||
at the bottom of this page.
|
||||
</p>
|
||||
|
||||
<h3>global configuration</h3>
|
||||
|
||||
<p>The server port dictates which TCP port the web server listens on.
|
||||
If you change this number you will need to restart.
|
||||
</p>
|
||||
|
||||
<p>The Watch Interval is how often new files will be discovered by your
|
||||
watchers (configured below).</p>
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Server port</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<label class="sr-only">Server port</label>
|
||||
<input type="text" class="form-control" placeholder="" x-model.number="config.Port">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Watch interval</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<label class="sr-only">Watch interval</label>
|
||||
<input type="text" class="form-control" placeholder="" x-model.number="config.WatchInterval">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h3>watcher configuration</h3>
|
||||
|
||||
<p>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.
|
||||
</p>
|
||||
|
||||
<p><a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks">
|
||||
Click here</a> for information on how to find your discord webhook URL.</p>
|
||||
|
||||
<p>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.
|
||||
</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
|
||||
in the same directory as the screenshots.
|
||||
</p>
|
||||
|
||||
<template x-for="(watcher, i) in config.Watchers">
|
||||
<div class="my-5">
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Directory to watch</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<label class="sr-only" for="">Directory</label>
|
||||
<input type="text" class="form-control" placeholder="" x-model="watcher.Path">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Webhook URL</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<label class="sr-only" for="">WebHook URL</label>
|
||||
<input type="text" class="form-control" placeholder="" x-model="watcher.WebHookURL">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Username</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<label class="sr-only" for="">Username</label>
|
||||
<input type="text" class="form-control" placeholder="" x-model="watcher.Username">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Watermark</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<button type="button" @click="config.Watchers[i].NoWatermark = ! config.Watchers[i].NoWatermark" class="btn btn-success" x-text="watcher.NoWatermark ? 'Disabled 😢' : 'Enabled'"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-row align-items-center">
|
||||
<div class="col-sm-6 my-1">
|
||||
<span>Exclusions</span>
|
||||
</div>
|
||||
<div class="col-sm-6 my-1">
|
||||
<template x-for="(exclude, j) in config.Watchers[i].Exclude">
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" x-model="config.Watchers[i].Exclude[j]">
|
||||
</div>
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-danger" href="#" @click.prevent="config.Watchers[i].Exclude.splice(j, 1);">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<button type="button" class="btn btn-secondary" href="#"
|
||||
@click.prevent="config.Watchers[i].Exclude.push('');">
|
||||
+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button type="button" class="btn btn-primary" href="#" @click.prevent="config.Watchers.splice(i, 1);">Remove
|
||||
this watcher</button>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<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: []});">
|
||||
Add a new watcher</button>
|
||||
</div>
|
||||
|
||||
<div class="my-5">
|
||||
|
||||
<button type="button" class="my-4 btn btn-danger" href="#" @click="save_config()">
|
||||
Save all Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "js" }}
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script>
|
||||
function configuration() {
|
||||
return {
|
||||
config: {}, error: '', success: '',
|
||||
get_config() {
|
||||
fetch('/rest/config')
|
||||
.then(response => response.json()) // convert to json
|
||||
.then(json => {
|
||||
this.config = json;
|
||||
console.log(json);
|
||||
})
|
||||
},
|
||||
save_config() {
|
||||
this.error = '';
|
||||
this.success = '';
|
||||
fetch('/rest/config', { method: 'POST', body: JSON.stringify(this.config) })
|
||||
.then(response => response.json()) // convert to json
|
||||
.then(json => {
|
||||
if (json.error) {
|
||||
this.error = json.error
|
||||
} else {
|
||||
this.success = 'Configuration saved';
|
||||
this.config = json;
|
||||
}
|
||||
window.scrollTo(0,0);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{{ end }}
|
||||
113
web/data/dau.css
Normal file
113
web/data/dau.css
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Globals
|
||||
*/
|
||||
|
||||
/* Links */
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #f44;
|
||||
}
|
||||
|
||||
/* Custom default button */
|
||||
.btn-secondary,
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus {
|
||||
color: #333;
|
||||
text-shadow: none; /* Prevent inheritance from `body` */
|
||||
background-color: #fff;
|
||||
border: .05rem solid #fff;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Base structure
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
/* display: -ms-flexbox;
|
||||
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;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: black;
|
||||
color: aliceblue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Header
|
||||
*/
|
||||
.masthead {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.masthead-brand {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link {
|
||||
padding: .25rem 0;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
background-color: transparent;
|
||||
border-bottom: .25rem solid transparent;
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link:hover,
|
||||
.nav-masthead .nav-link:focus {
|
||||
border-bottom-color: rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link + .nav-link {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.nav-masthead .active {
|
||||
color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
@media (min-width: 48em) {
|
||||
.masthead-brand {
|
||||
float: left;
|
||||
}
|
||||
.nav-masthead {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Cover
|
||||
*/
|
||||
.cover {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
.cover .btn-lg {
|
||||
padding: .75rem 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Footer
|
||||
*/
|
||||
.mastfoot {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
/* for alpine.js */
|
||||
[x-cloak] { display: none !important; }
|
||||
12
web/data/index.html
Normal file
12
web/data/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "content" }}
|
||||
<main role="main" class="inner DAU">
|
||||
<h1 class="DAU-heading">Discord Auto Upload</h1>
|
||||
<p class="lead">Hey look, it's DAU :-)</p>
|
||||
<p class="lead">
|
||||
<a href="https://github.com/tardisx/discord-auto-upload" class="btn btn-lg btn-secondary" target="_blank">Learn more</a>
|
||||
</p>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "js" }}
|
||||
{{ end }}
|
||||
61
web/data/logs.html
Normal file
61
web/data/logs.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{{ define "content" }}
|
||||
|
||||
<main role="main" class="inner DAU">
|
||||
<h1 class="DAU-heading">Logs</h1>
|
||||
<p class="lead">Discord-auto-upload logs</p>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<button type="button" onClick="debug=1; get_logs();" class="btn btn-primary">all logs</button>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<button type="button" onClick="debug=0; get_logs();" class="btn btn-primary">no debug</button>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<button type="button" id="scroll-button" onClick="toggle_scroll();" class="btn btn-primary">disable auto-scroll</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre id="logs" class="text-left pre-scrollable">
|
||||
</pre>
|
||||
</main>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ define "js" }}
|
||||
|
||||
<script>
|
||||
var debug = 0;
|
||||
var scrl = true;
|
||||
$(document).ready(function() {
|
||||
get_logs();
|
||||
setInterval(function() { get_logs(); }, 1000);
|
||||
});
|
||||
|
||||
function toggle_scroll() {
|
||||
scrl = !scrl;
|
||||
if (scrl) {
|
||||
$('#scroll-button').text('disable auto-scroll');
|
||||
}
|
||||
else {
|
||||
$('#scroll-button').text('auto-scroll');
|
||||
}
|
||||
}
|
||||
|
||||
function get_logs() {
|
||||
$.ajax({ method: 'get', url: '/rest/logs', data: { debug : debug }})
|
||||
.done(function(data) {
|
||||
$('#logs').text(data);
|
||||
console.log('scrl is ', scrl);
|
||||
if (scrl) {
|
||||
$('#logs').scrollTop(10000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{{ end }}
|
||||
45
web/data/uploads.html
Normal file
45
web/data/uploads.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{{ define "content" }}
|
||||
|
||||
<main role="main" class="inner DAU">
|
||||
<h1 class="DAU-heading">Uploads</h1>
|
||||
<p class="lead">Discord-auto-upload uploads</p>
|
||||
|
||||
<table class="table table-condensed table-dark">
|
||||
<thead>
|
||||
<tr><th>uploaded</th><th>dt</th><th>thumb</th></tr>
|
||||
</thead>
|
||||
<tbody id="uploads">
|
||||
|
||||
</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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
{{ end }}
|
||||
66
web/data/wrapper.tmpl
Normal file
66
web/data/wrapper.tmpl
Normal file
@@ -0,0 +1,66 @@
|
||||
{{ define "layout" }}
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
|
||||
|
||||
<style>
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
text-anchor: middle;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bd-placeholder-img-lg {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Custom styles for this template -->
|
||||
<link href="/dau.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
|
||||
<div class="DAU-container 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>
|
||||
<nav class="nav nav-masthead justify-content-center">
|
||||
<a class="nav-link {{ if eq .Path "index.html"}} active {{ end }}" href="/">Home</a>
|
||||
<a class="nav-link {{ if eq .Path "config.html"}} active {{ end }}" href="/config.html">Config</a>
|
||||
<a class="nav-link {{ if eq .Path "uploads.html"}} active {{ end }}" href="/uploads.html">Uploads</a>
|
||||
<a class="nav-link {{ if eq .Path "logs.html"}} active {{ end }}" href="/logs.html">Logs</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{ template "content" . }}
|
||||
|
||||
|
||||
<footer class="mastfoot mt-auto">
|
||||
<div class="inner">
|
||||
<!-- <p>DAU template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> -->
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
{{ template "js" . }}
|
||||
</html>
|
||||
|
||||
{{ end }}
|
||||
181
web/server.go
Normal file
181
web/server.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/tardisx/discord-auto-upload/config"
|
||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||
"github.com/tardisx/discord-auto-upload/upload"
|
||||
"github.com/tardisx/discord-auto-upload/version"
|
||||
)
|
||||
|
||||
type WebService struct {
|
||||
Config *config.ConfigService
|
||||
Uploader *upload.Uploader
|
||||
}
|
||||
|
||||
//go:embed data
|
||||
var webFS embed.FS
|
||||
|
||||
// DAUWebServer - stuff for the web server
|
||||
type DAUWebServer struct {
|
||||
// ConfigChange chan int
|
||||
|
||||
}
|
||||
|
||||
func (ws *WebService) getStatic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
path := r.URL.Path
|
||||
path = strings.TrimLeft(path, "/")
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
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)
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("not found"))
|
||||
return
|
||||
}
|
||||
|
||||
var b struct {
|
||||
Body string
|
||||
Path string
|
||||
Version string
|
||||
}
|
||||
b.Path = path
|
||||
b.Version = version.CurrentVersion
|
||||
|
||||
err = t.ExecuteTemplate(w, "layout", b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
} else { // anything else
|
||||
otherStatic, err := webFS.ReadFile("data/" + path)
|
||||
|
||||
if err != nil {
|
||||
daulog.SendLog(fmt.Sprintf("when fetching: %s got: %s", path, err), daulog.LogTypeError)
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("not found"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension(extension))
|
||||
|
||||
w.Write(otherStatic)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (ws *WebService) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
showDebug := false
|
||||
debug, present := r.URL.Query()["debug"]
|
||||
if present && len(debug[0]) > 0 && debug[0] != "0" {
|
||||
showDebug = true
|
||||
}
|
||||
|
||||
text := ""
|
||||
for _, log := range daulog.LogEntries {
|
||||
if !showDebug && log.Type == daulog.LogTypeDebug {
|
||||
continue
|
||||
}
|
||||
text = text + fmt.Sprintf(
|
||||
"%-6s %-19s %s\n", log.Type, log.Timestamp.Format("2006-01-02 15:04:05"), log.Entry,
|
||||
)
|
||||
}
|
||||
|
||||
// js, _ := json.Marshal(daulog.LogEntries)
|
||||
w.Write([]byte(text))
|
||||
}
|
||||
|
||||
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"))
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, &newConfig)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
j, _ := json.Marshal(ErrorResponse{Error: "badly formed JSON"})
|
||||
w.Write(j)
|
||||
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)
|
||||
|
||||
return
|
||||
}
|
||||
// config has changed, so tell the world
|
||||
if ws.Config.Changed != nil {
|
||||
ws.Config.Changed <- true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(ws.Config.Config)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (ws *WebService) getUploads(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
ups := ws.Uploader.Uploads
|
||||
|
||||
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
|
||||
}
|
||||
w.Write([]byte(text))
|
||||
}
|
||||
|
||||
func (ws *WebService) StartWebServer() {
|
||||
|
||||
http.HandleFunc("/", ws.getStatic)
|
||||
|
||||
http.HandleFunc("/rest/logs", ws.getLogs)
|
||||
http.HandleFunc("/rest/uploads", ws.getUploads)
|
||||
http.HandleFunc("/rest/config", ws.handleConfig)
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
84
web/server_test.go
Normal file
84
web/server_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"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()
|
||||
s.getStatic(w, req)
|
||||
res := w.Result()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil got %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "DAU") {
|
||||
t.Errorf("does not look like correct homepage at /")
|
||||
}
|
||||
if res.Header.Get("Content-Type") != "text/html; charset=utf-8" {
|
||||
t.Errorf("wrong content type for / - %s", res.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
s := WebService{}
|
||||
|
||||
notFounds := []string{
|
||||
"/abc.html", "/foo.html", "/foo.html", "/../foo.html",
|
||||
"/foo.gif",
|
||||
}
|
||||
|
||||
for _, nf := range notFounds {
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, nf, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.getStatic(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) != "not found" {
|
||||
t.Errorf("expected body to be not found, not '%s'", string(b))
|
||||
}
|
||||
if res.Header.Get("Content-Type") != "text/plain" {
|
||||
t.Error("Wrong content type for not found")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,"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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user