Compare commits
107 Commits
Author | SHA1 | Date | |
---|---|---|---|
76dbbacd98 | |||
ba31f52ee2 | |||
779f9e1992 | |||
553337bfab | |||
73c74cd37e | |||
2bd62a95a4 | |||
3497bfd194 | |||
ada43b176b | |||
326807b395 | |||
4dd166e65e | |||
9f7090a2f8 | |||
1bf557c3eb | |||
df0c6d090d | |||
b851a4f773 | |||
e612f8ae59 | |||
05ddf07fc4 | |||
9423bc32e9 | |||
243c349366 | |||
d8b16674dd | |||
9294ca9e33 | |||
2a99541f58 | |||
b04ed6aa62 | |||
06ac259e0a | |||
0c2fafdc7a | |||
f9433ae0cd | |||
ba7ae21248 | |||
fbd267e687 | |||
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 | |||
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 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: tardisx
|
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 ./...
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,6 +2,7 @@ dist
|
|||||||
release
|
release
|
||||||
discord-auto-upload
|
discord-auto-upload
|
||||||
discord-auto-upload.exe
|
discord-auto-upload.exe
|
||||||
assets
|
|
||||||
*.png
|
*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
|
.DS_Store
|
||||||
|
versioninfo.json
|
45
.goreleaser.yaml
Normal file
45
.goreleaser.yaml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
# clean up/install modules
|
||||||
|
- go mod tidy
|
||||||
|
- "go run tools/windows_metadata/release.go v{{- .Version }}"
|
||||||
|
builds:
|
||||||
|
- main:
|
||||||
|
hooks:
|
||||||
|
pre:
|
||||||
|
- "rm -f resource.syso"
|
||||||
|
- "go generate"
|
||||||
|
ignore:
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
|
ldflags:
|
||||||
|
- '{{ if eq .Os "windows" }}-H=windowsgui{{ end }}'
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
archives:
|
||||||
|
- replacements:
|
||||||
|
darwin: Darwin
|
||||||
|
linux: Linux
|
||||||
|
windows: Windows
|
||||||
|
386: i386
|
||||||
|
amd64: x86_64
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- '^Merge:'
|
||||||
|
- '^docs:'
|
||||||
|
- '^test:'
|
||||||
|
- '^[Bb]ump'
|
||||||
|
- '^[Cc]lean'
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"daulog",
|
||||||
|
"Debugf",
|
||||||
|
"inconsolata",
|
||||||
|
"Infof",
|
||||||
|
"Markedup",
|
||||||
|
"skratchdot"
|
||||||
|
]
|
||||||
|
}
|
109
CHANGELOG.md
Normal file
109
CHANGELOG.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.13.0] - 2022-11-01
|
||||||
|
|
||||||
|
- Resize images if needed to fit in discord 8Mb upload limits
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.12.4] - 2022-09-15
|
||||||
|
|
||||||
|
- Document that watcher intervals are in seconds
|
||||||
|
|
||||||
|
## [v0.12.3] - 2022-05-09
|
||||||
|
|
||||||
|
- Fix a race condition occasionally causing multiple duplicate uploads
|
||||||
|
|
||||||
|
## [v0.12.2] - 2022-05-01
|
||||||
|
|
||||||
|
- Automatically open your web browser to the `dau` web interface
|
||||||
|
(can be disabled in configuration)
|
||||||
|
- Add system tray/menubar icon with menus to open web interface, quit and
|
||||||
|
other links
|
||||||
|
- Superfluous text console removed on windows
|
||||||
|
|
||||||
|
## [v0.12.1] - 2022-05-01
|
||||||
|
|
||||||
|
- Show if a new version is available in the web interface
|
||||||
|
- Rework logging and fix the log display in the web interface
|
||||||
|
|
||||||
|
## [v0.12.0] - 2022-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
|
||||||
|
- 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
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
71
README.md
71
README.md
@ -1,11 +1,15 @@
|
|||||||
# Automatically upload screenshots into a discord channel
|
# Automatically upload screenshots into a discord 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:
|
This program automatically uploads new screenshots that appear in a folder on your computer to Discord and posts them in a channel:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
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
|
## What you'll need
|
||||||
|
|
||||||
* A folder where screenshots are stored
|
* A folder where screenshots are stored
|
||||||
@ -20,13 +24,18 @@ Binaries are available for Mac, Linux and Windows [here](https://github.com/tard
|
|||||||
|
|
||||||
#### From source
|
#### From source
|
||||||
|
|
||||||
You'll need to [download Go](https://golang.org/dl/) check the code out somewhere, run 'go generate' and then 'go build'.
|
You'll need to [download Go](https://golang.org/dl/), check the code out somewhere, run 'go generate' and then 'go build'.
|
||||||
|
|
||||||
## Using it
|
## Using it
|
||||||
|
|
||||||
`dau` configuration is managed via its internal web interface. When the executable is run, you can visit
|
`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
|
`http://localhost:9090` in your web browser to configure it. On Windows, a tray icon is created to provide
|
||||||
saved in a file called '.dau.json' in your home directory.
|
access to the web interface.
|
||||||
|
|
||||||
|
The web browser will be loaded automatically when you start the program, if possible. This option can be
|
||||||
|
disabled in the settings.
|
||||||
|
|
||||||
|
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
|
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.
|
`dau` to be useful.
|
||||||
@ -39,39 +48,51 @@ Thus, you do not have to worry about pointing `dau` at a directory full of image
|
|||||||
|
|
||||||
## Configuration options
|
## 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
|
* Server port - the port number the web server listens on. Requires restart
|
||||||
for more information on setting one up.
|
* Watch interval - how often each watcher will check the directory for new files, in seconds
|
||||||
|
|
||||||
### '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
|
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
|
actually hide the bot identity in any way). You might like to set it to your own
|
||||||
discord name.
|
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.
|
If the "Hold Uploads" option is selected, newly found files will not immediately be uploaded. They will be available
|
||||||
Note that subdirectories are also scanned. You need to enter the full filesystem
|
in the "uploads" tab of the web interface. This has two purposes:
|
||||||
path here.
|
|
||||||
|
|
||||||
### '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 :-)
|
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
|
||||||
### 'Files to exclude'
|
list. Click "upload" to upload your edited image.
|
||||||
|
|
||||||
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
|
## Limitations/bugs
|
||||||
|
|
||||||
@ -86,7 +107,7 @@ Please check the "log" page on the web interface for information when things are
|
|||||||
not working as you expect.
|
not working as you expect.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
This is just a relatively 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.
|
Please include any relevant logs from the console when reporting bugs.
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
#!/usr/bin/env perl
|
|
||||||
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
|
|
||||||
open my $fh, "<", "config/config.go" || die $!;
|
|
||||||
|
|
||||||
my $version;
|
|
||||||
while (<$fh>) {
|
|
||||||
$version = $1 if /^const\s+CurrentVersion.*?"([\d\.]+)"/;
|
|
||||||
}
|
|
||||||
close $fh;
|
|
||||||
|
|
||||||
die "no version?" unless defined $version;
|
|
||||||
|
|
||||||
# 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();
|
|
||||||
|
|
||||||
system(qw{go generate});
|
|
||||||
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
|
|
||||||
}
|
|
178
config/config.go
178
config/config.go
@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Config for the application
|
// Config for the application
|
||||||
var Config struct {
|
type ConfigV1 struct {
|
||||||
WebHookURL string
|
WebHookURL string
|
||||||
Path string
|
Path string
|
||||||
Watch int
|
Watch int
|
||||||
@ -22,46 +22,160 @@ var Config struct {
|
|||||||
Exclude string
|
Exclude string
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentVersion string = "0.8"
|
type Watcher struct {
|
||||||
|
WebHookURL string
|
||||||
|
Path string
|
||||||
|
Username string
|
||||||
|
NoWatermark bool
|
||||||
|
HoldUploads bool
|
||||||
|
Exclude []string
|
||||||
|
}
|
||||||
|
|
||||||
// Load the current config or initialise with defaults
|
type ConfigV2 struct {
|
||||||
func LoadOrInit() {
|
WatchInterval int
|
||||||
configPath := configPath()
|
Version int
|
||||||
daulog.SendLog(fmt.Sprintf("Trying to load config from %s", configPath), daulog.LogTypeDebug)
|
Port int
|
||||||
_, err := os.Stat(configPath)
|
Watchers []Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigV3 struct {
|
||||||
|
WatchInterval int
|
||||||
|
Version int
|
||||||
|
Port int
|
||||||
|
OpenBrowserOnStart bool
|
||||||
|
Watchers []Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigService struct {
|
||||||
|
Config *ConfigV3
|
||||||
|
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.Debugf("Trying to load config from %s\n", c.ConfigFilename)
|
||||||
|
_, err := os.Stat(c.ConfigFilename)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
daulog.SendLog("NOTE: No config file, writing out sample configuration", daulog.LogTypeInfo)
|
daulog.Info("NOTE: No config file, writing out sample configuration")
|
||||||
daulog.SendLog("You need to set the configuration via the web interface", daulog.LogTypeInfo)
|
daulog.Info("You need to set the configuration via the web interface")
|
||||||
|
c.Config = DefaultConfig()
|
||||||
Config.WebHookURL = ""
|
return c.Save()
|
||||||
Config.Path = homeDir() + string(os.PathSeparator) + "screenshots"
|
|
||||||
Config.Watch = 10
|
|
||||||
SaveConfig()
|
|
||||||
} else {
|
} else {
|
||||||
LoadConfig()
|
return c.Load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() {
|
func DefaultConfig() *ConfigV3 {
|
||||||
path := configPath()
|
c := ConfigV3{}
|
||||||
data, err := ioutil.ReadFile(path)
|
c.Version = 3
|
||||||
if err != nil {
|
c.WatchInterval = 10
|
||||||
log.Fatalf("cannot read config file %s: %s", path, err.Error())
|
c.Port = 9090
|
||||||
}
|
c.OpenBrowserOnStart = true
|
||||||
err = json.Unmarshal([]byte(data), &Config)
|
w := Watcher{
|
||||||
if err != nil {
|
WebHookURL: "https://webhook.url.here",
|
||||||
log.Fatalf("cannot decode config file %s: %s", path, err.Error())
|
Path: "/your/screenshot/dir/here",
|
||||||
|
Username: "",
|
||||||
|
NoWatermark: false,
|
||||||
|
Exclude: []string{},
|
||||||
}
|
}
|
||||||
|
c.Watchers = []Watcher{w}
|
||||||
|
return &c
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveConfig() {
|
// Load will load the configuration from a known-to-exist config file.
|
||||||
daulog.SendLog("saving configuration", daulog.LogTypeInfo)
|
func (c *ConfigService) Load() error {
|
||||||
path := configPath()
|
daulog.Debugf("Loading from %s", c.ConfigFilename)
|
||||||
jsonString, _ := json.Marshal(Config)
|
|
||||||
err := ioutil.WriteFile(path, jsonString, os.ModePerm)
|
data, err := ioutil.ReadFile(c.ConfigFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Cannot save config %s: %s", path, err.Error())
|
return fmt.Errorf("cannot read config file %s: %s", c.ConfigFilename, err.Error())
|
||||||
}
|
}
|
||||||
|
err = json.Unmarshal([]byte(data), &c.Config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot decode config file %s: %s", c.ConfigFilename, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 0 predates config migrations
|
||||||
|
if c.Config.Version == 0 {
|
||||||
|
// need to migrate this
|
||||||
|
daulog.Info("Migrating config to V2")
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.Version == 2 {
|
||||||
|
// need to migrate this
|
||||||
|
daulog.Info("Migrating config to V3")
|
||||||
|
c.Config.Version = 3
|
||||||
|
c.Config.OpenBrowserOnStart = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigService) Save() error {
|
||||||
|
daulog.Info("saving configuration")
|
||||||
|
// 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 {
|
func homeDir() string {
|
||||||
@ -72,7 +186,7 @@ func homeDir() string {
|
|||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
func configPath() string {
|
func defaultConfigPath() string {
|
||||||
homeDir := homeDir()
|
homeDir := homeDir()
|
||||||
return homeDir + string(os.PathSeparator) + ".dau.json"
|
return homeDir + string(os.PathSeparator) + ".dau.json"
|
||||||
}
|
}
|
||||||
|
106
config/config_test.go
Normal file
106
config/config_test.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoConfig(t *testing.T) {
|
||||||
|
c := ConfigService{}
|
||||||
|
|
||||||
|
c.ConfigFilename = emptyTempFile()
|
||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected failure from load: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.Version != 3 {
|
||||||
|
t.Error("not version 3 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 TestMigrateFromV1toV3(t *testing.T) {
|
||||||
|
c := ConfigService{}
|
||||||
|
|
||||||
|
c.ConfigFilename = v1Config()
|
||||||
|
err := c.LoadOrInit()
|
||||||
|
if err != nil {
|
||||||
|
t.Error("unexpected error from LoadOrInit()")
|
||||||
|
}
|
||||||
|
if c.Config.Version != 3 {
|
||||||
|
t.Errorf("Version %d not 3", c.Config.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.OpenBrowserOnStart != true {
|
||||||
|
t.Errorf("Open browser on start not true")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
return f.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileSize(file string) int {
|
||||||
|
fi, err := os.Stat(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return int(fi.Size())
|
||||||
|
|
||||||
|
}
|
156
data/config.html
156
data/config.html
@ -1,156 +0,0 @@
|
|||||||
|
|
||||||
<main role="main" class="inner DAU">
|
|
||||||
<h1 class="DAU-heading">Config</h1>
|
|
||||||
<p class="lead">Discord-auto-upload configuration</p>
|
|
||||||
<a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"><p class="lead">How to find your discord webhook</p></a>
|
|
||||||
|
|
||||||
|
|
||||||
<form class="">
|
|
||||||
<div class="form-row align-items-center config-item" data-key="webhook">
|
|
||||||
<div class="col-sm-5 my-1">
|
|
||||||
<span>Discord WebHook URL</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 my-1">
|
|
||||||
<label class="sr-only" for="inlineFormInputName">Name</label>
|
|
||||||
<input type="text" class="form-control rest-field" placeholder="https://....">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto my-1">
|
|
||||||
<button type="submit" class="btn btn-primary">update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="">
|
|
||||||
<div class="form-row align-items-center config-item" data-key="username">
|
|
||||||
<div class="col-sm-5 my-1">
|
|
||||||
<span>Bot username (optional)</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 my-1">
|
|
||||||
<label class="sr-only" for="inlineFormInputName">Name</label>
|
|
||||||
<input type="text" class="form-control rest-field" placeholder="">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto my-1">
|
|
||||||
<button type="submit" class="btn btn-primary">update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="">
|
|
||||||
<div class="form-row align-items-center config-item" data-key="directory">
|
|
||||||
<div class="col-sm-5 my-1">
|
|
||||||
<span>Directory to watch</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 my-1">
|
|
||||||
<label class="sr-only" for="inlineFormInputName">Name</label>
|
|
||||||
<input type="text" class="form-control rest-field" placeholder="/...">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto my-1">
|
|
||||||
<button type="submit" class="btn btn-primary">update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="">
|
|
||||||
<div class="form-row align-items-center config-item" data-key="watch">
|
|
||||||
<div class="col-sm-5 my-1">
|
|
||||||
<span>Period between filesystem checks (seconds)</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 my-1">
|
|
||||||
<label class="sr-only" for="inlineFormInputName">Seconds</label>
|
|
||||||
<input type="text" class="form-control rest-field " placeholder="/...">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto my-1">
|
|
||||||
<button type="submit" class="btn btn-primary">update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="">
|
|
||||||
<div class="form-row align-items-center config-item" data-key="nowatermark">
|
|
||||||
<div class="col-sm-5 my-1">
|
|
||||||
<span>Do not watermark images</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 my-1">
|
|
||||||
<div class="custom-control custom-switch">
|
|
||||||
<input type="checkbox" class="custom-control-input rest-field rest-field-boolean" id="input-nowatermark">
|
|
||||||
<label class="custom-control-label" for="input-nowatermark"> </label>
|
|
||||||
<span id="sadness" style="">😭</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto my-1">
|
|
||||||
<button type="submit" class="btn btn-primary">update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form class="">
|
|
||||||
<div class="form-row align-items-center config-item" data-key="exclude">
|
|
||||||
<div class="col-sm-5 my-1">
|
|
||||||
<span>Files to exclude</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 my-1">
|
|
||||||
<label class="sr-only" for="input-exclude">Name</label>
|
|
||||||
<input type="text" id="input-exclude" class="form-control rest-field" placeholder="">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto my-1">
|
|
||||||
<button type="submit" class="btn btn-primary">update</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function update_sadness () {
|
|
||||||
if ($('#input-nowatermark').prop('checked')) {
|
|
||||||
$('#sadness').css('visibility','');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('#sadness').css('visibility','hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$(document).ready(function() {
|
|
||||||
|
|
||||||
$('#input-nowatermark').on('click', function() { update_sadness(); });
|
|
||||||
|
|
||||||
// populate each field
|
|
||||||
$('.config-item').each(function() {
|
|
||||||
let el = $(this);
|
|
||||||
let key = el.data('key');
|
|
||||||
|
|
||||||
$.ajax({ method: 'get', url: '/rest/config/'+key})
|
|
||||||
.done(function(data) {
|
|
||||||
var this_el = $(".config-item[data-key='"+key+"']").find('.rest-field');
|
|
||||||
if (this_el.hasClass('rest-field-boolean')) {
|
|
||||||
this_el.prop('checked', data.value);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
this_el.val(data.value);
|
|
||||||
}
|
|
||||||
update_sadness();
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// respond to button clicks to update
|
|
||||||
$('.config-item button').on('click', function(e,f) {
|
|
||||||
key = $(this).parents('.config-item').data('key');
|
|
||||||
val = $(this).parents('.config-item').find('.rest-field').val();
|
|
||||||
if ($(this).parents('.config-item').find('.rest-field-boolean').length) {
|
|
||||||
val = $(this).parents('.config-item').find('.rest-field').prop('checked') ? 1 : 0;
|
|
||||||
}
|
|
||||||
$.post('/rest/config/'+key, { value: val })
|
|
||||||
.done(function(d) {
|
|
||||||
if (d.success) {
|
|
||||||
alert('Updated config');
|
|
||||||
} else {
|
|
||||||
alert("Error: " + d.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -1,18 +0,0 @@
|
|||||||
|
|
||||||
<main role="main" class="inner DAU">
|
|
||||||
<h1 class="DAU-heading">Config</h1>
|
|
||||||
<p class="lead">Discord-auto-upload logs</p>
|
|
||||||
|
|
||||||
<pre id="logs" class="text-left pre-scrollable">
|
|
||||||
</pre>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
$.ajax({ method: 'get', url: '/rest/logs'})
|
|
||||||
.done(function(data) {
|
|
||||||
console.log(data);
|
|
||||||
$('#logs').text(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
438
dau.go
438
dau.go
@ -1,361 +1,203 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
//go:generate go-bindata -pkg assets -o assets/static.go -prefix data/ data
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"encoding/json"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/fs"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"image"
|
|
||||||
|
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
|
||||||
"github.com/fogleman/gg"
|
|
||||||
"github.com/pborman/getopt"
|
|
||||||
|
|
||||||
// "github.com/skratchdot/open-golang/open"
|
|
||||||
"golang.org/x/image/font/inconsolata"
|
|
||||||
|
|
||||||
"github.com/tardisx/discord-auto-upload/config"
|
"github.com/tardisx/discord-auto-upload/config"
|
||||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
"github.com/tardisx/discord-auto-upload/upload"
|
||||||
|
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
|
||||||
|
// "github.com/tardisx/discord-auto-upload/upload"
|
||||||
|
"github.com/tardisx/discord-auto-upload/version"
|
||||||
"github.com/tardisx/discord-auto-upload/web"
|
"github.com/tardisx/discord-auto-upload/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
var lastCheck = time.Now()
|
type watch struct {
|
||||||
var newLastCheck = time.Now()
|
lastCheck time.Time
|
||||||
|
newLastCheck time.Time
|
||||||
|
config config.Watcher
|
||||||
|
uploader *upload.Uploader
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
parseOptions()
|
parseOptions()
|
||||||
|
|
||||||
|
// grab the conf, register to notice changes
|
||||||
|
conf := config.DefaultConfigService()
|
||||||
|
configChanged := make(chan bool)
|
||||||
|
conf.Changed = configChanged
|
||||||
|
conf.LoadOrInit()
|
||||||
|
|
||||||
|
// create the uploader
|
||||||
|
up := upload.NewUploader()
|
||||||
|
|
||||||
// log.Print("Opening web browser")
|
// log.Print("Opening web browser")
|
||||||
// open.Start("http://localhost:9090")
|
// open.Start("http://localhost:9090")
|
||||||
|
web := web.WebService{Config: conf, Uploader: up}
|
||||||
web.StartWebServer()
|
web.StartWebServer()
|
||||||
|
|
||||||
checkUpdates()
|
if conf.Config.OpenBrowserOnStart {
|
||||||
|
openWebBrowser(conf.Config.Port)
|
||||||
|
}
|
||||||
|
|
||||||
daulog.SendLog(fmt.Sprintf("Waiting for images to appear in %s", config.Config.Path), daulog.LogTypeInfo)
|
go func() {
|
||||||
// wander the path, forever
|
version.GetOnlineVersion()
|
||||||
|
if version.UpdateAvailable() {
|
||||||
|
daulog.Info("*** NEW VERSION AVAILABLE ***")
|
||||||
|
daulog.Infof("You are currently on version %s, but version %s is available\n", version.CurrentVersion, version.LatestVersionInfo.TagName)
|
||||||
|
daulog.Info("----------- Release Info -----------")
|
||||||
|
daulog.Info(version.LatestVersionInfo.Body)
|
||||||
|
daulog.Info("------------------------------------")
|
||||||
|
daulog.Info("Upgrade at https://github.com/tardisx/discord-auto-upload/releases/latest")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// create the watchers, restart them if config changes
|
||||||
|
// blocks forever
|
||||||
|
go func() {
|
||||||
|
startWatchers(conf, up, configChanged)
|
||||||
|
}()
|
||||||
|
mainloop(conf)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func startWatchers(config *config.ConfigService, up *upload.Uploader, configChange chan bool) {
|
||||||
for {
|
for {
|
||||||
if checkPath(config.Config.Path) {
|
daulog.Debug("Creating watchers")
|
||||||
err := filepath.Walk(config.Config.Path,
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err) })
|
for _, c := range config.Config.Watchers {
|
||||||
if err != nil {
|
daulog.Infof("Creating watcher for %s with interval %d", c.Path, config.Config.WatchInterval)
|
||||||
log.Fatal("could not watch path", err)
|
watcher := watch{uploader: up, lastCheck: time.Now(), newLastCheck: time.Now(), config: c}
|
||||||
|
go watcher.Watch(config.Config.WatchInterval, ctx)
|
||||||
}
|
}
|
||||||
lastCheck = newLastCheck
|
// wait for single that the config changed
|
||||||
|
<-configChange
|
||||||
|
cancel()
|
||||||
|
daulog.Info("starting new watchers due to config change")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watch) Watch(interval int, ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
daulog.Info("Killing old watcher")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
newFiles := w.ProcessNewFiles()
|
||||||
|
for _, f := range newFiles {
|
||||||
|
w.uploader.AddFile(f, w.config)
|
||||||
|
}
|
||||||
|
// upload them
|
||||||
|
w.uploader.Upload()
|
||||||
|
daulog.Debugf("sleeping for %ds before next check of %s", interval, w.config.Path)
|
||||||
|
time.Sleep(time.Duration(interval) * time.Second)
|
||||||
}
|
}
|
||||||
daulog.SendLog(fmt.Sprintf("sleeping for %ds before next check of %s", config.Config.Watch, config.Config.Path), daulog.LogTypeDebug)
|
|
||||||
time.Sleep(time.Duration(config.Config.Watch) * time.Second)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPath(path string) bool {
|
// ProcessNewFiles returns an array of new files that have appeared since
|
||||||
src, err := os.Stat(path)
|
// 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 {
|
if err != nil {
|
||||||
log.Printf("Problem with path '%s': %s", path, err)
|
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.Errorf("Problem with path '%s': %s", w.config.Path, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !src.IsDir() {
|
if !src.IsDir() {
|
||||||
log.Printf("Problem with path '%s': is not a directory", path)
|
daulog.Errorf("Problem with path '%s': is not a directory", w.config.Path)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUpdates() {
|
// 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 {
|
||||||
|
|
||||||
type GithubRelease struct {
|
extension := strings.ToLower(filepath.Ext(path))
|
||||||
HTMLURL string
|
|
||||||
TagName string
|
if !(extension == ".png" || extension == ".jpg" || extension == ".gif") {
|
||||||
Name string
|
return nil
|
||||||
Body string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
daulog.SendLog("checking for new version", daulog.LogTypeInfo)
|
fi, err := os.Stat(path)
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
daulog.SendLog(fmt.Sprintf("WARNING: Update check failed: %v", err), daulog.LogTypeError)
|
return err
|
||||||
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
|
if fi.ModTime().After(w.lastCheck) && fi.Mode().IsRegular() {
|
||||||
err = json.Unmarshal(body, &latest)
|
excluded := false
|
||||||
|
for _, exclusion := range exclusions {
|
||||||
if err != nil {
|
if strings.Contains(path, exclusion) {
|
||||||
log.Fatal("could not parse JSON: ", err)
|
excluded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !excluded {
|
||||||
|
*found = append(*found, path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.CurrentVersion < latest.TagName {
|
if w.newLastCheck.Before(fi.ModTime()) {
|
||||||
fmt.Printf("You are currently on version %s, but version %s is available\n", config.CurrentVersion, latest.TagName)
|
w.newLastCheck = fi.ModTime()
|
||||||
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"), daulog.LogTypeInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOptions() {
|
|
||||||
|
|
||||||
// Declare the flags to be used
|
|
||||||
helpFlag := getopt.BoolLong("help", 'h', "help")
|
|
||||||
versionFlag := getopt.BoolLong("version", 'v', "show version")
|
|
||||||
getopt.SetParameters("")
|
|
||||||
|
|
||||||
getopt.Parse()
|
|
||||||
|
|
||||||
if *helpFlag {
|
|
||||||
getopt.PrintUsage(os.Stderr)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *versionFlag {
|
|
||||||
fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
|
|
||||||
fmt.Printf("Version: %s\n", config.CurrentVersion)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// grab the config
|
|
||||||
config.LoadOrInit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkFile(path string, f os.FileInfo, err error) error {
|
|
||||||
if f.ModTime().After(lastCheck) && f.Mode().IsRegular() {
|
|
||||||
|
|
||||||
if fileEligible(path) {
|
|
||||||
// process file
|
|
||||||
processFile(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newLastCheck.Before(f.ModTime()) {
|
|
||||||
newLastCheck = f.ModTime()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileEligible(file string) bool {
|
func parseOptions() {
|
||||||
|
var versionFlag bool
|
||||||
|
flag.BoolVar(&versionFlag, "version", false, "show version")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
if config.Config.Exclude != "" && strings.Contains(file, config.Config.Exclude) {
|
if versionFlag {
|
||||||
return false
|
fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
|
||||||
|
fmt.Printf("Version: %s\n", version.CurrentVersion)
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension := strings.ToLower(filepath.Ext(file))
|
|
||||||
if extension == ".png" || extension == ".jpg" || extension == ".gif" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func processFile(file string) {
|
func openWebBrowser(port int) {
|
||||||
|
address := fmt.Sprintf("http://localhost:%d", port)
|
||||||
if !config.Config.NoWatermark {
|
open.Start(address)
|
||||||
daulog.SendLog("Copying to temp location and watermarking ", daulog.LogTypeInfo)
|
|
||||||
file = mungeFile(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Config.WebHookURL == "" {
|
|
||||||
daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
daulog.SendLog("Uploading", daulog.LogTypeInfo)
|
|
||||||
|
|
||||||
extraParams := map[string]string{}
|
|
||||||
|
|
||||||
if config.Config.Username != "" {
|
|
||||||
log.Print("Overriding username with " + config.Config.Username)
|
|
||||||
extraParams["username"] = config.Config.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
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(config.Config.WebHookURL, extraParams, "file", file)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
start := time.Now()
|
|
||||||
client := &http.Client{Timeout: time.Second * 30}
|
|
||||||
resp, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
log.Print("Error performing request:", err)
|
|
||||||
retriesRemaining--
|
|
||||||
sleepForRetries(retriesRemaining)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
log.Print("Bad response from server:", resp.StatusCode)
|
|
||||||
if b, err := ioutil.ReadAll(resp.Body); err == nil {
|
|
||||||
log.Print("Body:", string(b))
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
log.Printf("Uploaded to %s %dx%d", a.URL, a.Width, a.Height)
|
|
||||||
log.Printf("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.Config.NoWatermark {
|
|
||||||
daulog.SendLog(fmt.Sprintf("Removing temporary file: %s", file), daulog.LogTypeDebug)
|
|
||||||
os.Remove(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if retriesRemaining == 0 {
|
|
||||||
log.Fatal("Failed to upload, even after retries")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 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 mungeFile(path string) string {
|
|
||||||
|
|
||||||
reader, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
im, _, err := image.Decode(reader)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
return actualName
|
|
||||||
}
|
}
|
||||||
|
12
dau_nonwindows.go
Normal file
12
dau_nonwindows.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//go:build darwin || linux
|
||||||
|
// +build darwin linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/tardisx/discord-auto-upload/config"
|
||||||
|
|
||||||
|
func mainloop(c *config.ConfigService) {
|
||||||
|
|
||||||
|
ch := make(chan bool)
|
||||||
|
<-ch
|
||||||
|
}
|
105
dau_test.go
Normal file
105
dau_test.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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?")
|
||||||
|
}
|
||||||
|
|
||||||
|
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?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFileTree() string {
|
||||||
|
dir, err := ioutil.TempDir("", "dau-test")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
60
dau_windows.go
Normal file
60
dau_windows.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"github.com/getlantern/systray"
|
||||||
|
"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/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate goversioninfo
|
||||||
|
|
||||||
|
//go:embed dau.ico
|
||||||
|
var appIcon []byte
|
||||||
|
|
||||||
|
func mainloop(c *config.ConfigService) {
|
||||||
|
systray.Run(func() { onReady(c) }, onExit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onReady(c *config.ConfigService) {
|
||||||
|
|
||||||
|
systray.SetIcon(appIcon)
|
||||||
|
//systray.SetTitle("DAU")
|
||||||
|
systray.SetTooltip(fmt.Sprintf("discord-auto-upload %s", version.CurrentVersion))
|
||||||
|
openApp := systray.AddMenuItem("Open", "Open in web browser")
|
||||||
|
gh := systray.AddMenuItem("Github", "Open project page")
|
||||||
|
discord := systray.AddMenuItem("Discord", "Join us on discord")
|
||||||
|
ghr := systray.AddMenuItem("Release Notes", "Open project release notes")
|
||||||
|
quit := systray.AddMenuItem("Quit", "Quit")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-quit.ClickedCh
|
||||||
|
systray.Quit()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-openApp.ClickedCh:
|
||||||
|
openWebBrowser(c.Config.Port)
|
||||||
|
case <-gh.ClickedCh:
|
||||||
|
open.Start("https://github.com/tardisx/discord-auto-upload")
|
||||||
|
case <-ghr.ClickedCh:
|
||||||
|
open.Start(fmt.Sprintf("https://github.com/tardisx/discord-auto-upload/releases/tag/%s", version.CurrentVersion))
|
||||||
|
case <-discord.ClickedCh:
|
||||||
|
open.Start("https://discord.gg/eErG9sntbZ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Sets the icon of a menu item. Only available on Mac and Windows.
|
||||||
|
// mQuit.SetIcon(icon.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func onExit() {
|
||||||
|
// clean up here
|
||||||
|
daulog.Info("quitting on user request")
|
||||||
|
}
|
9
go.mod
9
go.mod
@ -1,12 +1,15 @@
|
|||||||
module github.com/tardisx/discord-auto-upload
|
module github.com/tardisx/discord-auto-upload
|
||||||
|
|
||||||
go 1.15
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fogleman/gg v1.3.0
|
github.com/fogleman/gg v1.3.0
|
||||||
|
github.com/getlantern/systray v1.2.1
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/pborman/getopt v1.1.0
|
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6
|
github.com/stretchr/testify v1.6.1 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e
|
||||||
|
golang.org/x/mod v0.7.0
|
||||||
)
|
)
|
||||||
|
63
go.sum
63
go.sum
@ -1,13 +1,68 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
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/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
|
||||||
|
github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
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/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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
188
image/image.go
Normal file
188
image/image.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
// Package image is responsible for thumbnailing, resizing and watermarking
|
||||||
|
// images.
|
||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
i "image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// the filenames below are ordered in a specific way
|
||||||
|
// In the simplest case we only need the original filename.
|
||||||
|
// In more complex cases, we might have other files, created
|
||||||
|
// temporarily. These all need to be cleaned up.
|
||||||
|
// We upload the "final" file, depending on what actions have
|
||||||
|
// been taken.
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
OriginalFilename string
|
||||||
|
OriginalFormat string // jpeg, png
|
||||||
|
ModifiedFilename string // if the user applied modifications
|
||||||
|
ResizedFilename string // if the file had to be resized to be uploaded
|
||||||
|
WatermarkedFilename string
|
||||||
|
MaxBytes int
|
||||||
|
Watermark bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCloser returns an io.ReadCloser providing the imagedata
|
||||||
|
// with the manglings that have been requested
|
||||||
|
func (s *Store) ReadCloser() (io.ReadCloser, error) {
|
||||||
|
// determine format
|
||||||
|
s.determineFormat()
|
||||||
|
|
||||||
|
// check if we will fit the number of bytes, resize if necessary
|
||||||
|
err := s.resizeToUnder(int64(s.MaxBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// the conundrum here is that the watermarking could modify the file size again, maybe going over
|
||||||
|
// the MaxBytes size. That would mostly be about jpeg compression levels I guess...
|
||||||
|
if s.Watermark {
|
||||||
|
s.applyWatermark()
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the reader
|
||||||
|
f, err := os.Open(s.uploadSourceFilename())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) determineFormat() error {
|
||||||
|
file, err := os.Open(s.OriginalFilename)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("could not open file: %s", err))
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, format, err := i.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("could not decode file: %s", err))
|
||||||
|
}
|
||||||
|
s.OriginalFormat = format
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resizeToUnder resizes the image, if necessary
|
||||||
|
func (s *Store) resizeToUnder(size int64) error {
|
||||||
|
fileToResize := s.uploadSourceFilename()
|
||||||
|
fi, err := os.Stat(s.uploadSourceFilename())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentSize := fi.Size()
|
||||||
|
if currentSize <= size {
|
||||||
|
return nil // nothing needs to be done
|
||||||
|
}
|
||||||
|
|
||||||
|
daulog.Infof("%s is %d bytes, need to resize to fit in %d", fileToResize, currentSize, size)
|
||||||
|
|
||||||
|
file, err := os.Open(fileToResize)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("could not open file: %s", err))
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
im, _, err := i.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("could not decode file: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the size is 10% too big, we reduce X and Y by 10% - this is overkill but should
|
||||||
|
// get us across the line in most cases
|
||||||
|
fraction := float64(currentSize) / float64(size) // say 1.1 for 10%
|
||||||
|
newXY := i.Point{
|
||||||
|
X: int(float64(im.Bounds().Max.X) / fraction),
|
||||||
|
Y: int(float64(im.Bounds().Max.Y) / fraction),
|
||||||
|
}
|
||||||
|
|
||||||
|
daulog.Infof("fraction is %f, will resize to %dx%d", fraction, newXY.X, newXY.Y)
|
||||||
|
|
||||||
|
dst := i.NewRGBA(i.Rect(0, 0, newXY.X, newXY.Y))
|
||||||
|
draw.BiLinear.Scale(dst, dst.Rect, im, im.Bounds(), draw.Over, nil)
|
||||||
|
|
||||||
|
resizedFile, err := os.CreateTemp("", "dau_resize_file_*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.OriginalFormat == "png" {
|
||||||
|
err = png.Encode(resizedFile, dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if s.OriginalFormat == "jpeg" {
|
||||||
|
err = jpeg.Encode(resizedFile, dst, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
panic("unknown format " + s.OriginalFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ResizedFilename = resizedFile.Name()
|
||||||
|
resizedFile.Close()
|
||||||
|
|
||||||
|
fi, err = os.Stat(s.uploadSourceFilename())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newSize := fi.Size()
|
||||||
|
if newSize <= size {
|
||||||
|
daulog.Infof("File resized, now %d", newSize)
|
||||||
|
return nil // nothing needs to be done
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to resize: was %d, now %d, needed %d", currentSize, newSize, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// uploadSourceFilename gives us the filename, which might be a watermarked, resized
|
||||||
|
// or markedup version, depending on what has happened to this file.
|
||||||
|
func (s Store) uploadSourceFilename() string {
|
||||||
|
if s.WatermarkedFilename != "" {
|
||||||
|
return s.WatermarkedFilename
|
||||||
|
}
|
||||||
|
if s.ResizedFilename != "" {
|
||||||
|
return s.ResizedFilename
|
||||||
|
}
|
||||||
|
if s.ModifiedFilename != "" {
|
||||||
|
return s.ModifiedFilename
|
||||||
|
}
|
||||||
|
return s.OriginalFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFilename provides a name to be assigned to the upload on Discord
|
||||||
|
func (s Store) UploadFilename() string {
|
||||||
|
return "image." + s.OriginalFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup removes all the temporary files that we might have created
|
||||||
|
func (s Store) Cleanup() {
|
||||||
|
daulog.Infof("cleaning temporary files %#v", s)
|
||||||
|
|
||||||
|
if s.ModifiedFilename != "" {
|
||||||
|
daulog.Infof("removing %s", s.ModifiedFilename)
|
||||||
|
os.Remove(s.ModifiedFilename)
|
||||||
|
}
|
||||||
|
if s.ResizedFilename != "" {
|
||||||
|
daulog.Infof("removing %s", s.ResizedFilename)
|
||||||
|
os.Remove(s.ResizedFilename)
|
||||||
|
}
|
||||||
|
if s.WatermarkedFilename != "" {
|
||||||
|
daulog.Infof("removing %s", s.WatermarkedFilename)
|
||||||
|
os.Remove(s.WatermarkedFilename)
|
||||||
|
}
|
||||||
|
}
|
62
image/thumb.go
Normal file
62
image/thumb.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
i "image"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
thumbnailMaxX = 128
|
||||||
|
thumbnailMaxY = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
type ThumbType = string
|
||||||
|
|
||||||
|
const ThumbTypeOriginal = "orig"
|
||||||
|
const ThumbTypeMarkedUp = "markedup"
|
||||||
|
|
||||||
|
// ThumbPNG writes a thumbnail out to an io.Writer
|
||||||
|
func (ip *Store) ThumbPNG(t ThumbType, w io.Writer) error {
|
||||||
|
|
||||||
|
var filename string
|
||||||
|
if t == ThumbTypeOriginal {
|
||||||
|
filename = ip.OriginalFilename
|
||||||
|
} else if t == ThumbTypeMarkedUp {
|
||||||
|
filename = ip.ModifiedFilename
|
||||||
|
} else {
|
||||||
|
log.Fatal("was passed incorrect 'type' arg")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open file: %s", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
im, _, err := i.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not decode file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newXY := i.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 := i.NewRGBA(i.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
|
||||||
|
|
||||||
|
}
|
64
image/watermark.go
Normal file
64
image/watermark.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
i "image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
"golang.org/x/image/font/inconsolata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// applyWatermark applies the watermark to the image
|
||||||
|
func (s *Store) applyWatermark() error {
|
||||||
|
|
||||||
|
in, err := os.Open(s.uploadSourceFilename())
|
||||||
|
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
im, _, err := i.Decode(in)
|
||||||
|
if err != nil {
|
||||||
|
daulog.Errorf("Cannot decode image: %v - skipping watermarking", err)
|
||||||
|
return fmt.Errorf("cannot decode image: %w", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
waterMarkedFile, err := os.CreateTemp("", "dau_watermark_file_*")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer waterMarkedFile.Close()
|
||||||
|
|
||||||
|
if s.OriginalFormat == "png" {
|
||||||
|
png.Encode(waterMarkedFile, dc.Image())
|
||||||
|
} else if s.OriginalFormat == "jpeg" {
|
||||||
|
jpeg.Encode(waterMarkedFile, dc.Image(), nil)
|
||||||
|
} else {
|
||||||
|
panic("Cannot handle " + s.OriginalFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WatermarkedFilename = waterMarkedFile.Name()
|
||||||
|
return nil
|
||||||
|
}
|
64
log/log.go
64
log/log.go
@ -1,9 +1,14 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
WriteEntry(l LogEntry)
|
||||||
|
}
|
||||||
|
|
||||||
type LogEntryType string
|
type LogEntryType string
|
||||||
|
|
||||||
type LogEntry struct {
|
type LogEntry struct {
|
||||||
@ -18,28 +23,73 @@ const (
|
|||||||
LogTypeDebug = "debug"
|
LogTypeDebug = "debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
var LogEntries []LogEntry
|
var loggers []Logger
|
||||||
var logInput chan LogEntry
|
var logInput chan LogEntry
|
||||||
|
var Memory *MemoryLogger
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
||||||
|
// create some loggers
|
||||||
|
Memory = &MemoryLogger{maxsize: 100}
|
||||||
|
stdout := &StdoutLogger{}
|
||||||
|
|
||||||
|
loggers = []Logger{Memory, stdout}
|
||||||
|
|
||||||
// wait for log entries
|
// wait for log entries
|
||||||
logInput = make(chan LogEntry)
|
logInput = make(chan LogEntry)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
aLog := <-logInput
|
aLog := <-logInput
|
||||||
LogEntries = append(LogEntries, aLog)
|
for _, l := range loggers {
|
||||||
for len(LogEntries) > 100 {
|
l.WriteEntry(aLog)
|
||||||
LogEntries = LogEntries[1:]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendLog(entry string, entryType LogEntryType) {
|
func Debug(entry string) {
|
||||||
|
|
||||||
logInput <- LogEntry{
|
logInput <- LogEntry{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Entry: entry,
|
Entry: entry,
|
||||||
Type: entryType,
|
Type: LogTypeDebug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func Debugf(entry string, args ...interface{}) {
|
||||||
|
logInput <- LogEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Entry: fmt.Sprintf(entry, args...),
|
||||||
|
Type: LogTypeDebug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(entry string) {
|
||||||
|
logInput <- LogEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Entry: entry,
|
||||||
|
Type: LogTypeInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(entry string, args ...interface{}) {
|
||||||
|
logInput <- LogEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Entry: fmt.Sprintf(entry, args...),
|
||||||
|
Type: LogTypeInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(entry string) {
|
||||||
|
logInput <- LogEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Entry: entry,
|
||||||
|
Type: LogTypeError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(entry string, args ...interface{}) {
|
||||||
|
logInput <- LogEntry{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Entry: fmt.Sprintf(entry, args...),
|
||||||
|
Type: LogTypeError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
log/memory.go
Normal file
29
log/memory.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemoryLogger struct {
|
||||||
|
size int
|
||||||
|
entries []LogEntry
|
||||||
|
maxsize int
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemoryLogger) WriteEntry(l LogEntry) {
|
||||||
|
// xxx needs mutex
|
||||||
|
// if m.entries == nil {
|
||||||
|
// m.entries = make([]LogEntry, 0)
|
||||||
|
// }
|
||||||
|
m.lock.Lock()
|
||||||
|
m.entries = append(m.entries, l)
|
||||||
|
if len(m.entries) > m.maxsize {
|
||||||
|
m.entries = m.entries[1:]
|
||||||
|
}
|
||||||
|
m.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemoryLogger) Entries() []LogEntry {
|
||||||
|
return m.entries
|
||||||
|
}
|
12
log/stdout.go
Normal file
12
log/stdout.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StdoutLogger struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StdoutLogger) WriteEntry(l LogEntry) {
|
||||||
|
log.Printf("%-6s %s", l.Type, l.Entry)
|
||||||
|
}
|
84
tools/windows_metadata/release.go
Normal file
84
tools/windows_metadata/release.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const versionInfoTemplate = `
|
||||||
|
{
|
||||||
|
"FixedFileInfo": {
|
||||||
|
"FileVersion": {
|
||||||
|
"Major": MAJOR,
|
||||||
|
"Minor": MINOR,
|
||||||
|
"Patch": PATCH,
|
||||||
|
"Build": 0
|
||||||
|
},
|
||||||
|
"ProductVersion": {
|
||||||
|
"Major": MAJOR,
|
||||||
|
"Minor": MINOR,
|
||||||
|
"Patch": PATCH,
|
||||||
|
"Build": 0
|
||||||
|
},
|
||||||
|
"FileFlagsMask": "3f",
|
||||||
|
"FileFlags ": "00",
|
||||||
|
"FileOS": "040004",
|
||||||
|
"FileType": "01",
|
||||||
|
"FileSubType": "00"
|
||||||
|
},
|
||||||
|
"StringFileInfo": {
|
||||||
|
"Comments": "",
|
||||||
|
"CompanyName": "tardisx@github",
|
||||||
|
"FileDescription": "https://github.com/tardisx/discord-auto-upload",
|
||||||
|
"FileVersion": "",
|
||||||
|
"InternalName": "",
|
||||||
|
"LegalCopyright": "https://github.com/tardisx/discord-auto-upload/blob/master/LICENSE",
|
||||||
|
"LegalTrademarks": "",
|
||||||
|
"OriginalFilename": "",
|
||||||
|
"PrivateBuild": "",
|
||||||
|
"ProductName": "discord-auto-upload",
|
||||||
|
"ProductVersion": "VERSION",
|
||||||
|
"SpecialBuild": ""
|
||||||
|
},
|
||||||
|
"VarFileInfo": {
|
||||||
|
"Translation": {
|
||||||
|
"LangID": "0409",
|
||||||
|
"CharsetID": "04B0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IconPath": "dau.ico",
|
||||||
|
"ManifestPath": ""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var nonAlphanumericRegex = regexp.MustCompile(`[^0-9]+`)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
version := os.Args[1]
|
||||||
|
if !semver.IsValid(version) {
|
||||||
|
panic("bad version" + version)
|
||||||
|
}
|
||||||
|
parts := strings.Split(version, ".")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
log.Fatalf("bad version: %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts[0] = nonAlphanumericRegex.ReplaceAllString(parts[0], "")
|
||||||
|
parts[1] = nonAlphanumericRegex.ReplaceAllString(parts[1], "")
|
||||||
|
parts[2] = nonAlphanumericRegex.ReplaceAllString(parts[2], "")
|
||||||
|
|
||||||
|
out := versionInfoTemplate
|
||||||
|
out = strings.Replace(out, "MAJOR", parts[0], -1)
|
||||||
|
out = strings.Replace(out, "MINOR", parts[1], -1)
|
||||||
|
out = strings.Replace(out, "PATCH", parts[2], -1)
|
||||||
|
out = strings.Replace(out, "VERSION", version, -1)
|
||||||
|
|
||||||
|
f, _ := os.Create("versioninfo.json")
|
||||||
|
f.Write([]byte(out))
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
}
|
297
upload/upload.go
Normal file
297
upload/upload.go
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
// Package upload encapsulates prepping an image for sending to discord,
|
||||||
|
// and actually uploading it there.
|
||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tardisx/discord-auto-upload/config"
|
||||||
|
"github.com/tardisx/discord-auto-upload/image"
|
||||||
|
|
||||||
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatePending State = "Pending" // waiting for decision to upload (could be edited)
|
||||||
|
StateQueued State = "Queued" // ready for upload
|
||||||
|
StateWatermarking State = "Adding Watermark" // thumbnail generation
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Uploader struct {
|
||||||
|
Uploads []*Upload `json:"uploads"`
|
||||||
|
Lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Upload struct {
|
||||||
|
Id int32 `json:"id"`
|
||||||
|
UploadedAt time.Time `json:"uploaded_at"`
|
||||||
|
|
||||||
|
Image *image.Store
|
||||||
|
|
||||||
|
webhookURL string
|
||||||
|
|
||||||
|
usernameOverride string
|
||||||
|
|
||||||
|
Url string `json:"url"` // url on the discord CDN
|
||||||
|
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
|
||||||
|
State State `json:"state"`
|
||||||
|
StateReason string `json:"state_reason"`
|
||||||
|
|
||||||
|
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) {
|
||||||
|
u.Lock.Lock()
|
||||||
|
atomic.AddInt32(¤tId, 1)
|
||||||
|
thisUpload := Upload{
|
||||||
|
Id: currentId,
|
||||||
|
UploadedAt: time.Time{},
|
||||||
|
Image: &image.Store{OriginalFilename: file, Watermark: !conf.NoWatermark, MaxBytes: 8_000_000},
|
||||||
|
webhookURL: conf.WebHookURL,
|
||||||
|
usernameOverride: conf.Username,
|
||||||
|
Url: "",
|
||||||
|
State: StateQueued,
|
||||||
|
Client: nil,
|
||||||
|
}
|
||||||
|
// if the user wants uploads to be held for editing etc,
|
||||||
|
// set it to Pending instead
|
||||||
|
if conf.HoldUploads {
|
||||||
|
thisUpload.State = StatePending
|
||||||
|
thisUpload.StateReason = ""
|
||||||
|
}
|
||||||
|
u.Uploads = append(u.Uploads, &thisUpload)
|
||||||
|
u.Lock.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload uploads any files that have not yet been uploaded
|
||||||
|
func (u *Uploader) Upload() {
|
||||||
|
u.Lock.Lock()
|
||||||
|
|
||||||
|
for _, upload := range u.Uploads {
|
||||||
|
if upload.State == StateQueued {
|
||||||
|
upload.processUpload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.Lock.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) UploadById(id int32) *Upload {
|
||||||
|
u.Lock.Lock()
|
||||||
|
defer u.Lock.Unlock()
|
||||||
|
|
||||||
|
for _, anUpload := range u.Uploads {
|
||||||
|
if anUpload.Id == int32(id) {
|
||||||
|
return anUpload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Upload) processUpload() error {
|
||||||
|
daulog.Infof("Uploading: %s", u.Image.OriginalFilename)
|
||||||
|
|
||||||
|
if u.webhookURL == "" {
|
||||||
|
daulog.Error("WebHookURL is not configured - cannot upload!")
|
||||||
|
return errors.New("webhook url not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
extraParams := map[string]string{}
|
||||||
|
|
||||||
|
if u.usernameOverride != "" {
|
||||||
|
daulog.Infof("Overriding username with '%s'", u.usernameOverride)
|
||||||
|
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 {
|
||||||
|
|
||||||
|
// open an io.ReadCloser for the file we intend to upload
|
||||||
|
var err error
|
||||||
|
|
||||||
|
imageData, err := u.Image.ReadCloser()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := newfileUploadRequest(u.webhookURL, extraParams, "file", u.Image.UploadFilename(), imageData)
|
||||||
|
if err != nil {
|
||||||
|
daulog.Errorf("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 {
|
||||||
|
daulog.Errorf("Error performing request: %s", err)
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if resp.StatusCode == 413 {
|
||||||
|
// just fail immediately, we know this means the file was too big
|
||||||
|
daulog.Error("413 received - file too large")
|
||||||
|
u.State = StateFailed
|
||||||
|
u.StateReason = "discord API said file too large"
|
||||||
|
return errors.New("received 413 - file too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
// {"message": "Request entity too large", "code": 40005}
|
||||||
|
daulog.Errorf("Bad response code from server: %d", resp.StatusCode)
|
||||||
|
if b, err := ioutil.ReadAll(resp.Body); err == nil {
|
||||||
|
daulog.Errorf("Body:\n%s", string(b))
|
||||||
|
}
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
daulog.Errorf("could not deal with body: %s", 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.Debugf("Response: %s", string(resBody[:]))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
daulog.Errorf("could not parse JSON: %s", err)
|
||||||
|
daulog.Errorf("Response was: %s", string(resBody[:]))
|
||||||
|
retriesRemaining--
|
||||||
|
sleepForRetries(retriesRemaining)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(res.Attachments) < 1 {
|
||||||
|
daulog.Error("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.Infof("Uploaded to %s %dx%d", a.URL, a.Width, a.Height)
|
||||||
|
daulog.Infof("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate)
|
||||||
|
|
||||||
|
u.Url = a.URL
|
||||||
|
u.State = StateComplete
|
||||||
|
u.StateReason = ""
|
||||||
|
u.Width = a.Width
|
||||||
|
u.Height = a.Height
|
||||||
|
u.UploadedAt = time.Now()
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any temporary files
|
||||||
|
u.Image.Cleanup()
|
||||||
|
|
||||||
|
if retriesRemaining == 0 {
|
||||||
|
daulog.Error("Failed to upload, even after all retries")
|
||||||
|
u.State = StateFailed
|
||||||
|
u.StateReason = "could not upload after all retries"
|
||||||
|
return errors.New("could not upload after all retries")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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, filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(part, filedata)
|
||||||
|
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 sleepForRetries(retry int) {
|
||||||
|
if retry == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
retryTime := (6-retry)*(6-retry) + 6
|
||||||
|
daulog.Errorf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry)
|
||||||
|
time.Sleep(time.Duration(retryTime) * time.Second)
|
||||||
|
}
|
97
upload/upload_test.go
Normal file
97
upload/upload_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
i "image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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/", Image: &image.Store{OriginalFilename: f.Name()}}
|
||||||
|
// u.Client = &MockClient{DoFunc: DoGoodUpload}
|
||||||
|
// err := u.processUpload()
|
||||||
|
// if err != nil {
|
||||||
|
// t.Errorf("error occured: %s", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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/", Image: &image.Store{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())
|
||||||
|
// }
|
||||||
|
// if u.State != StateFailed {
|
||||||
|
// t.Error("upload should have been marked failed")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func tempImageGt8Mb() {
|
||||||
|
// about 12Mb
|
||||||
|
width := 2000
|
||||||
|
height := 2000
|
||||||
|
|
||||||
|
upLeft := i.Point{0, 0}
|
||||||
|
lowRight := i.Point{width, height}
|
||||||
|
|
||||||
|
img := i.NewRGBA(i.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)
|
||||||
|
}
|
81
version/version.go
Normal file
81
version/version.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CurrentVersion string = "v0.13.0"
|
||||||
|
|
||||||
|
type GithubRelease struct {
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var LatestVersion string
|
||||||
|
var LatestVersionInfo GithubRelease
|
||||||
|
|
||||||
|
// UpdateAvailable returns true or false, depending on whether not a new version is available.
|
||||||
|
// It always returns false if the OnlineVersion has not yet been fetched.
|
||||||
|
func UpdateAvailable() bool {
|
||||||
|
if !semver.IsValid(CurrentVersion) {
|
||||||
|
panic(fmt.Sprintf("my current version '%s' is not valid", CurrentVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
if LatestVersion == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !semver.IsValid(LatestVersion) {
|
||||||
|
// maybe this should just be a warning
|
||||||
|
daulog.Errorf("online version '%s' is not valid - assuming no new version", LatestVersion)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
comp := semver.Compare(LatestVersion, CurrentVersion)
|
||||||
|
if comp == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if comp == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false // they are using a newer one than exists?
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOnlineVersion() {
|
||||||
|
|
||||||
|
daulog.Info("checking for new version")
|
||||||
|
|
||||||
|
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.Errorf("WARNING: Update check failed: %s", err)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
LatestVersion = latest.TagName
|
||||||
|
LatestVersionInfo = latest
|
||||||
|
daulog.Debugf("Latest version: %s", LatestVersion)
|
||||||
|
|
||||||
|
}
|
21
version/version_test.go
Normal file
21
version/version_test.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersioningUpdate(t *testing.T) {
|
||||||
|
// pretend there is a new version
|
||||||
|
LatestVersion = "v0.13.9"
|
||||||
|
if !UpdateAvailable() {
|
||||||
|
t.Error("should be a version newer than " + CurrentVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersioningNoUpdate(t *testing.T) {
|
||||||
|
// pretend there is not a new version
|
||||||
|
LatestVersion = "v0.12.1"
|
||||||
|
if UpdateAvailable() {
|
||||||
|
t.Error("should NOT be a version newer than " + 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
221
web/data/config.html
Normal file
221
web/data/config.html
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
{{ 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 in seconds (watchers are 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>Open browser on startup</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 my-1">
|
||||||
|
<label class="sr-only">Open browser</label>
|
||||||
|
<button type="button" @click="config.OpenBrowserOnStart = ! config.OpenBrowserOnStart" class="btn btn-success" x-text="config.OpenBrowserOnStart ? 'Enabled' : 'Disabled'"></button>
|
||||||
|
|
||||||
|
</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>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
|
||||||
|
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>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">
|
||||||
|
<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, HoldUploads: 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>
|
||||||
|
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 }}
|
@ -6,7 +6,7 @@
|
|||||||
a,
|
a,
|
||||||
a:focus,
|
a:focus,
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #fff;
|
color: #f44;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom default button */
|
/* Custom default button */
|
||||||
@ -28,18 +28,23 @@ html,
|
|||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
|
padding: 2em;
|
||||||
|
max-width: 80em;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: -ms-flexbox;
|
/* display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex; */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||||
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.DAU-container {
|
.DAU-container {
|
||||||
max-width: 42em;
|
max-width: 52em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DAU-container-editor {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
@ -108,3 +113,6 @@ pre {
|
|||||||
.mastfoot {
|
.mastfoot {
|
||||||
color: rgba(255, 255, 255, .5);
|
color: rgba(255, 255, 255, .5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* for alpine.js */
|
||||||
|
[x-cloak] { display: none !important; }
|
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,4 +1,4 @@
|
|||||||
|
{{ define "content" }}
|
||||||
<main role="main" class="inner DAU">
|
<main role="main" class="inner DAU">
|
||||||
<h1 class="DAU-heading">Discord Auto Upload</h1>
|
<h1 class="DAU-heading">Discord Auto Upload</h1>
|
||||||
<p class="lead">Hey look, it's DAU :-)</p>
|
<p class="lead">Hey look, it's DAU :-)</p>
|
||||||
@ -6,3 +6,7 @@
|
|||||||
<a href="https://github.com/tardisx/discord-auto-upload" class="btn btn-lg btn-secondary" target="_blank">Learn more</a>
|
<a href="https://github.com/tardisx/discord-auto-upload" class="btn btn-lg btn-secondary" target="_blank">Learn more</a>
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "js" }}
|
||||||
|
{{ end }}
|
47
web/data/logs.html
Normal file
47
web/data/logs.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
|
||||||
|
<main role="main" class="inner DAU" x-data="logs()" x-init="get_logs()">
|
||||||
|
<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" @click="debug = !debug" class="btn btn-primary" x-text="debug ? 'debug' : 'no debug'"></button>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<button type="button" @click="scroll = !scroll" class="btn btn-primary" x-text="scroll ? 'auto-scroll' : 'no scroll'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre id="logs" x-text="text" class="text-left pre-scrollable">
|
||||||
|
</pre>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "js" }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function logs() {
|
||||||
|
return {
|
||||||
|
text: '', scroll: true, debug: false,
|
||||||
|
get_logs() {
|
||||||
|
fetch('/rest/logs?' + new URLSearchParams({ debug: this.debug ? "1" : "0" }))
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(text => {
|
||||||
|
console.log(text);
|
||||||
|
this.text = text;
|
||||||
|
if (this.scroll) {
|
||||||
|
document.getElementById('logs').scrollTop =10000;
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
setTimeout(function() { self.get_logs(); }, 1000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{ end }}
|
146
web/data/uploads.html
Normal file
146
web/data/uploads.html
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
{{ define "content" }}
|
||||||
|
|
||||||
|
<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>filename</th>
|
||||||
|
<th>actions</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<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>state</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<template x-for="ul in uploads">
|
||||||
|
<tr>
|
||||||
|
<td x-text="ul.original_file"></td>
|
||||||
|
<td>
|
||||||
|
<span x-text="ul.state"></span>
|
||||||
|
<div x-if="ul.state_reason">(<span x-text="ul.state_reason"></span>)</div>
|
||||||
|
</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>
|
||||||
|
<span x-text="ul.state"></span>
|
||||||
|
<div x-if="ul.state_reason">(<span x-text="ul.state_reason"></span>)</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<img :src="'/rest/image/'+ul.id+'/thumb'">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "js" }}
|
||||||
|
<script>
|
||||||
|
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 }}
|
@ -1,14 +1,14 @@
|
|||||||
|
{{ define "layout" }}
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<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">
|
<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="/alpine.js" defer></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="https://cdn.jsdelivr.net/npm/fabric@4.6.0/dist/fabric.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bd-placeholder-img {
|
.bd-placeholder-img {
|
||||||
@ -31,21 +31,27 @@
|
|||||||
<link href="/dau.css" rel="stylesheet">
|
<link href="/dau.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="text-center">
|
<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">
|
<header class="masthead mb-auto">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<h3 class="masthead-brand">discord-auto-upload ({{.Version}})</h3>
|
<h3 class="masthead-brand">discord-auto-upload ({{.Version}})</h3>
|
||||||
<nav class="nav nav-masthead justify-content-center">
|
<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 "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 "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>
|
<a class="nav-link {{ if eq .Path "logs.html"}} active {{ end }}" href="/logs.html">Logs</a>
|
||||||
|
{{ if eq .NewVersionAvailable true }}
|
||||||
|
<a class="nav-link" href="{{ .NewVersionInfo.HTMLURL }}">Ver {{ .NewVersionInfo.TagName }} available!</a>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{.Body}}
|
{{ template "content" . }}
|
||||||
|
|
||||||
|
|
||||||
<footer class="mastfoot mt-auto">
|
<footer class="mastfoot mt-auto">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
@ -56,4 +62,8 @@
|
|||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
{{ template "js" . }}
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
{{ end }}
|
557
web/server.go
557
web/server.go
@ -1,307 +1,368 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"text/template"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tardisx/discord-auto-upload/assets"
|
"github.com/gorilla/mux"
|
||||||
"github.com/tardisx/discord-auto-upload/config"
|
"github.com/tardisx/discord-auto-upload/config"
|
||||||
|
"github.com/tardisx/discord-auto-upload/image"
|
||||||
daulog "github.com/tardisx/discord-auto-upload/log"
|
daulog "github.com/tardisx/discord-auto-upload/log"
|
||||||
|
"github.com/tardisx/discord-auto-upload/upload"
|
||||||
|
"github.com/tardisx/discord-auto-upload/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DAUWebServer - stuff for the web server
|
type WebService struct {
|
||||||
type DAUWebServer struct {
|
Config *config.ConfigService
|
||||||
ConfigChange chan int
|
Uploader *upload.Uploader
|
||||||
}
|
}
|
||||||
|
|
||||||
type valueStringResponse struct {
|
type ErrorResponse struct {
|
||||||
Success bool `json:"success"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type valueBooleanResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Value bool `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type errorResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatic(w http.ResponseWriter, r *http.Request) {
|
type StartUploadRequest struct {
|
||||||
// haha this is dumb and I should change it
|
Id int32 `json:"id"`
|
||||||
re := regexp.MustCompile(`[^a-zA-Z0-9\.]`)
|
}
|
||||||
path := r.URL.Path[1:]
|
|
||||||
sanitized_path := re.ReplaceAll([]byte(path), []byte("_"))
|
|
||||||
|
|
||||||
if string(sanitized_path) == "" {
|
type StartUploadResponse struct {
|
||||||
sanitized_path = []byte("index.html")
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//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"
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := assets.Asset(string(sanitized_path))
|
extension := filepath.Ext(string(path))
|
||||||
|
|
||||||
|
if extension == ".html" { // html file
|
||||||
|
|
||||||
|
t, err := template.ParseFS(webFS, "data/wrapper.tmpl", "data/"+path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Asset was not found.
|
daulog.Errorf("when fetching: %s got: %s", path, err)
|
||||||
fmt.Fprintln(w, err)
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte("not found"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
extension := filepath.Ext(string(sanitized_path))
|
|
||||||
|
|
||||||
// is this a HTML file? if so wrap it in the template
|
|
||||||
if extension == ".html" {
|
|
||||||
wrapper, _ := assets.Asset("wrapper.tmpl")
|
|
||||||
t := template.Must(template.New("wrapper").Parse(string(wrapper)))
|
|
||||||
var b struct {
|
var b struct {
|
||||||
Body string
|
Body string
|
||||||
Path string
|
Path string
|
||||||
Version string
|
Version string
|
||||||
|
NewVersionAvailable bool
|
||||||
|
NewVersionInfo version.GithubRelease
|
||||||
}
|
}
|
||||||
b.Body = string(data)
|
b.Path = path
|
||||||
b.Path = string(sanitized_path)
|
b.Version = version.CurrentVersion
|
||||||
b.Version = config.CurrentVersion
|
b.NewVersionAvailable = version.UpdateAvailable()
|
||||||
t.Execute(w, b)
|
b.NewVersionInfo = version.LatestVersionInfo
|
||||||
|
|
||||||
|
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.Errorf("when fetching: %s got: %s", path, err)
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte("not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise we are a static thing
|
|
||||||
w.Header().Set("Content-Type", mime.TypeByExtension(extension))
|
w.Header().Set("Content-Type", mime.TypeByExtension(extension))
|
||||||
|
|
||||||
w.Write(data)
|
w.Write(otherStatic)
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO there should be locks around all these config accesses
|
|
||||||
func getSetWebhook(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
getResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL}
|
|
||||||
|
|
||||||
// I can't see any way this will fail
|
|
||||||
js, _ := json.Marshal(getResponse)
|
|
||||||
w.Write(js)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
config.Config.WebHookURL = r.PostForm.Get("value")
|
|
||||||
config.SaveConfig()
|
|
||||||
postResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(postResponse)
|
|
||||||
w.Write(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO there should be locks around all these config accesses
|
|
||||||
func getSetUsername(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
getResponse := valueStringResponse{Success: true, Value: config.Config.Username}
|
|
||||||
|
|
||||||
// I can't see any way this will fail
|
|
||||||
js, _ := json.Marshal(getResponse)
|
|
||||||
w.Write(js)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
config.Config.Username = r.PostForm.Get("value")
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
postResponse := valueStringResponse{Success: true, Value: config.Config.Username}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(postResponse)
|
|
||||||
w.Write(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSetWatch(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
getResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)}
|
|
||||||
|
|
||||||
// I can't see any way this will fail
|
|
||||||
js, _ := json.Marshal(getResponse)
|
|
||||||
w.Write(js)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.Atoi(r.PostForm.Get("value"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for watch: %v", err)}
|
|
||||||
js, _ := json.Marshal(response)
|
|
||||||
w.Write(js)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if i < 1 {
|
|
||||||
response := errorResponse{Success: false, Error: "must be > 0"}
|
|
||||||
js, _ := json.Marshal(response)
|
|
||||||
w.Write(js)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Config.Watch = i
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
postResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(postResponse)
|
|
||||||
w.Write(js)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSetNoWatermark(w http.ResponseWriter, r *http.Request) {
|
func (ws *WebService) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
getResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark}
|
|
||||||
|
|
||||||
// I can't see any way this will fail
|
|
||||||
js, _ := json.Marshal(getResponse)
|
|
||||||
w.Write(js)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := r.PostForm.Get("value")
|
|
||||||
|
|
||||||
if v != "0" && v != "1" {
|
|
||||||
response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for nowatermark: %v", err)}
|
|
||||||
js, _ := json.Marshal(response)
|
|
||||||
w.Write(js)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if v == "0" {
|
|
||||||
config.Config.NoWatermark = false
|
|
||||||
} else {
|
|
||||||
config.Config.NoWatermark = true
|
|
||||||
}
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
postResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(postResponse)
|
|
||||||
w.Write(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSetDirectory(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
getResponse := valueStringResponse{Success: true, Value: config.Config.Path}
|
|
||||||
|
|
||||||
// I can't see any way this will fail
|
|
||||||
js, _ := json.Marshal(getResponse)
|
|
||||||
w.Write(js)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
newPath := r.PostForm.Get("value")
|
|
||||||
|
|
||||||
// sanity check this path
|
|
||||||
stat, err := os.Stat(newPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
// not exist
|
|
||||||
response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - does not exist", newPath)}
|
|
||||||
js, _ := json.Marshal(response)
|
|
||||||
w.Write(js)
|
|
||||||
return
|
|
||||||
} else if !stat.IsDir() {
|
|
||||||
// not a directory
|
|
||||||
response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - is not a directory", newPath)}
|
|
||||||
js, _ := json.Marshal(response)
|
|
||||||
w.Write(js)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Config.Path = newPath
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
postResponse := valueStringResponse{Success: true, Value: config.Config.Path}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(postResponse)
|
|
||||||
w.Write(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSetExclude(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
getResponse := valueStringResponse{Success: true, Value: config.Config.Exclude}
|
|
||||||
// I can't see any way this will fail
|
|
||||||
js, _ := json.Marshal(getResponse)
|
|
||||||
w.Write(js)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
config.Config.Exclude = r.PostForm.Get("value")
|
|
||||||
config.SaveConfig()
|
|
||||||
|
|
||||||
postResponse := valueStringResponse{Success: true, Value: config.Config.Exclude}
|
|
||||||
|
|
||||||
js, _ := json.Marshal(postResponse)
|
|
||||||
w.Write(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
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 := ""
|
text := ""
|
||||||
for _, log := range daulog.LogEntries {
|
for _, log := range daulog.Memory.Entries() {
|
||||||
|
if !showDebug && log.Type == daulog.LogTypeDebug {
|
||||||
|
continue
|
||||||
|
}
|
||||||
text = text + fmt.Sprintf(
|
text = text + fmt.Sprintf(
|
||||||
"%-6s %-19s %s\n", log.Type, log.Timestamp.Format("2006-01-02 15:04:05"), log.Entry,
|
"%-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))
|
w.Write([]byte(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebService) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
|
||||||
|
newConfig := config.ConfigV3{}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
returnJSONError(w, "could not read body?")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, &newConfig)
|
||||||
|
if err != nil {
|
||||||
|
returnJSONError(w, "badly formed JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ws.Config.Config = &newConfig
|
||||||
|
err = ws.Config.Save()
|
||||||
|
if err != nil {
|
||||||
|
returnJSONError(w, err.Error())
|
||||||
|
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 {
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
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 = ul.Image.ThumbPNG(image.ThumbTypeOriginal, 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")
|
||||||
|
|
||||||
|
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 = ul.Image.ThumbPNG(image.ThumbTypeMarkedUp, 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.Image.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.Image.Cleanup()
|
||||||
|
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.Image.ModifiedFilename = 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 StartWebServer() {
|
func (ws *WebService) StartWebServer() {
|
||||||
|
|
||||||
http.HandleFunc("/", getStatic)
|
r := mux.NewRouter()
|
||||||
http.HandleFunc("/rest/config/webhook", getSetWebhook)
|
|
||||||
http.HandleFunc("/rest/config/username", getSetUsername)
|
r.HandleFunc("/rest/logs", ws.getLogs)
|
||||||
http.HandleFunc("/rest/config/watch", getSetWatch)
|
r.HandleFunc("/rest/uploads", ws.getUploads)
|
||||||
http.HandleFunc("/rest/config/nowatermark", getSetNoWatermark)
|
r.HandleFunc("/rest/upload/{id:[0-9]+}/{change}", ws.modifyUpload)
|
||||||
http.HandleFunc("/rest/config/directory", getSetDirectory)
|
|
||||||
http.HandleFunc("/rest/config/exclude", getSetExclude)
|
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)
|
||||||
|
|
||||||
http.HandleFunc("/rest/logs", getLogs)
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Print("Starting web server on http://localhost:9090")
|
listen := fmt.Sprintf(":%d", ws.Config.Config.Port)
|
||||||
err := http.ListenAndServe(":9090", nil) // set listen port
|
daulog.Infof("Starting web server on http://localhost%s", listen)
|
||||||
if err != nil {
|
|
||||||
log.Fatal("ListenAndServe: ", err)
|
srv := &http.Server{
|
||||||
|
Handler: r,
|
||||||
|
Addr: listen,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
85
web/server_test.go
Normal file
85
web/server_test.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
exp := `{"WatchInterval":10,"Version":3,"Port":9090,"OpenBrowserOnStart":true,"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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user