47 Commits
0.6 ... 0.10

Author SHA1 Message Date
c47660addf Add uploads page 2021-06-08 22:20:11 +09:30
2d0e294af6 Fix title 2021-06-08 22:19:54 +09:30
283e0f3584 Bump version 2021-06-08 22:19:37 +09:30
9ef6ab71c7 Add upload package and update dependencies 2021-06-07 21:13:57 +09:30
701583d3fd Update year 2021-06-07 21:13:18 +09:30
0c156a19f0 Fix punctuation 2021-06-06 22:22:44 +09:30
cc54bb6469 Enable filtering by debug and automatic scroll to bottom 2021-06-06 17:15:38 +09:30
71c097e578 Update gitignore 2021-06-06 17:15:19 +09:30
d23e31c0e0 Bump version 2021-06-04 10:42:26 +09:30
9cb79a846e Umm, how did this ever work? 😫 2021-06-04 10:41:23 +09:30
a5ce0c7f63 Documenation update 2021-06-03 19:42:12 +09:30
bcc4e145a2 Limit number of log entries stored 2021-06-03 19:40:53 +09:30
d8c0b7d0ea Fix error popup 2021-06-03 19:40:43 +09:30
fdf70daba7 Improve log display, use a <pre> so it can be easily cut and pasted. 2021-06-03 19:36:48 +09:30
b69cdebf3b Send logs to web server for display there 2021-06-02 23:42:29 +09:30
9e22490fe2 Merge pull request #10 from NoahBohme/master
Open discord link in a new tab
2021-03-15 23:30:56 +10:30
NoahBohme
ae24f16631 Delete .github/workflows directory 2021-03-15 11:51:07 +01:00
NoahBohme
b69acac0d0 Create go.yml 2021-02-23 09:46:47 +01:00
noah
c833f185cc add link for discord webhook 2021-02-23 08:23:10 +01:00
noah
b3ee0d9d1d Open discord link in a new tab 2021-02-23 08:16:59 +01:00
23c0aa2a34 Add Exclude to the config as well. 2021-02-09 22:07:40 +10:30
2d1ac3c803 Clean up logging and improve web interface 2021-02-07 22:06:19 +10:30
942f81a378 Bump version 2021-02-07 22:05:49 +10:30
287efab257 We no longer create a .bat file. 2021-02-07 22:05:16 +10:30
2b159e5532 Update README for the new version 2021-02-07 22:05:01 +10:30
851f073e99 Read/Write config to $HOME 2021-02-07 11:42:13 +10:30
ec658520b7 go fmt 2021-02-07 08:10:27 +10:30
6b1867f35f Support noWatermark config 2021-02-07 08:10:06 +10:30
6e493522c8 Add username and watch period web configuration 2021-02-06 12:50:44 +10:30
4d09901fb3 Fix up the javascript for loading config data. 2021-02-03 21:43:23 +10:30
9c9d4e492a Wrapper for HTML pages and start of the config web interface 2021-02-02 22:09:18 +10:30
e1f5afa788 go fmt 2021-01-31 18:48:48 +10:30
46a0f5a187 Rework configuration to it's own package and make it available to web server. Start a template driven web interface. 2021-01-31 17:53:32 +10:30
55bb5a8bae Fix build instructions 2021-01-31 09:56:09 +10:30
c2b9bf410d Bring into 2021 2021-01-30 21:23:58 +10:30
a4f958f846 Continue web server integration 2020-03-26 11:40:35 +10:30
14f8fe1933 Fix go generate call 2020-03-20 05:51:25 +10:30
752ff42a19 Add asset generation. 2020-03-20 05:49:12 +10:30
1ef062d19c Generate assets when releasing 2017-07-27 22:11:15 +09:30
6f09841209 Ignore generated asset directory 2017-07-27 22:11:03 +09:30
4619bb5383 Automatically open web browser on startup 2017-07-27 22:04:47 +09:30
e240f5dbd0 Embedded assets 2017-07-27 22:04:32 +09:30
Justin Hawkins
f976777f40 Start fleshing out web server 2017-07-27 12:18:02 +09:30
80a905b7d6 Improve update check error and make it non-fatal 2017-07-26 22:44:56 +09:30
497d2e3e27 Basic web server startup 2017-07-26 22:40:21 +09:30
ab54ace0d2 Add empty web class for the upcoming build-in web server 2017-07-26 14:24:02 +09:30
450765145b Add note about existing file handling 2017-07-26 13:24:07 +09:30
17 changed files with 1251 additions and 287 deletions

4
.gitignore vendored
View File

@@ -2,3 +2,7 @@ dist
release
discord-auto-upload
discord-auto-upload.exe
assets
*.png
*.jpg
.DS_Store

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017 Justin Hawkins
Copyright (c) 2021 Justin Hawkins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -18,56 +18,75 @@ Point it at your Steam screenshot folder, or similar, and shortly after you hit
Binaries are available for Mac, Linux and Windows [here](https://github.com/tardisx/discord-auto-upload/releases/latest).
Put them somewhere on your path and run from the command line.
The windows version comes with a .bat file to make this a little easier - edit the `dau.bat` file to include your webhook URL and
other parameters, then you can simply double click `dau.bat` to start `dau` running.
#### From source
You'll need to [download Go](https://golang.org/dl/) check the code out somewhere, and '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
`dau` is a command line driven program. When executed, it will continually scan a directory for new images, and each time it finds one it will upload it to discord, via the discord web hook.
`dau` configuration is managed via its internal web interface. When the executable is run, you can visit
`http://localhost:9090` in your web browser to configure the it. Configuration persists across runs, it is
saved in a file called '.dau.json' in your home directory.
The first time you run it, you will need to configure at least the discord web hook and the watch path for
`dau` to be useful.
While running, `dau` will continually scan a directory for new images, and each time it finds one it will upload it to discord, via the discord web hook.
`dau` will only upload "new" screenshots, where "new" means a file that appears in a directory that it is watching, if it appears *after* it has started executing.
Thus, you do not have to worry about pointing `dau` at a directory full of images, it will only upload new ones.
If `dau` is on your path, you can run it from your screenshot folder and there is then no need to specify the path to your images.
## Configuration options
The only two mandatory command line parameters are the discord webhook URL:
See the web interface at http://localhost:9090 to configure `dau`.
`--webhook URL` - the webhook URL (see [here](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) for details).
### 'Discord WebHook URL'
and the directory to watch:
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.
`--directory /some/path/here` - the directory that screenshots will appear in.
### 'Bot Username'
You will have to quote the path on windows, or anywhere where the directory path contains spaces. Note that
subdirectories will also be scanned.
This is completely optional and can be any arbitrary string. It makes the upload
appear to come from a different user (though this is visual only, and does not
actually hide the bot identity in any way). You might like to set it to your own
discord name.
Other parameters are:
### 'Directory to watch'
`--exclude <string>` - exclude any files that contain this string (commonly used to avoid uploading thumbnails).
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.
`--watch xx` - specify how many seconds to wait between scanning the directory. The default is 10 seconds.
### 'Period between filesystem checks'
`--username <username>` - an arbitrary string to show as the bot's username in the channel.
This is the number of seconds between which `dau` will look for new images.
`--no-watermark` - don't watermark images with a reference to this tool.
### 'Do not watermark images'
`--help` - show command line help.
This will disable the watermarking of images. I like it when you don't set this :-)
`--version` - show the version.
### 'Files to exclude'
This is a string to match against the filename to check for exclusions. The common
use case is to use 'thumbnail' or similar if your image directory contains additional
thumbnail files.
## Limitations/bugs
* Only files ending jpg, gif or png are uploaded.
* If multiple screenshots occur quickly (<1 second apart) not all may be uploaded.
* Files to upload are determined by the file modification time. If you drag and drop existing files they will
not be detected and uploaded. Only newly created files will be detected.
## Troubleshooting
Please check the "log" page on the web interface for information when things are
not working as you expect.
## TODO
This is just a quick hack. Open to suggestions on new features and improvements.
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.
Please include any relevant logs from the console when reporting bugs.

View File

@@ -3,11 +3,11 @@
use strict;
use warnings;
open my $fh, "<", "dau.go" || die $!;
open my $fh, "<", "config/config.go" || die $!;
my $version;
while (<$fh>) {
$version = $1 if /^const\s+currentVersion.*?"([\d\.]+)"/;
$version = $1 if /^const\s+CurrentVersion.*?"([\d\.]+)"/;
}
close $fh;
@@ -19,9 +19,9 @@ system "mkdir", "release";
system "mkdir", "dist";
my %build = (
win => { env => { GOOS => 'windows', GOARCH => '386' }, filename => 'dau.exe' },
linux => { env => { GOOS => 'linux', GOARCH => '386' }, filename => 'dau' },
mac => { env => { GOOS => 'darwin', GOARCH => '386' }, filename => 'dau' },
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) {
@@ -30,6 +30,7 @@ foreach my $type (keys %build) {
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};
@@ -38,15 +39,5 @@ foreach my $type (keys %build) {
}
sub add_extras {
# bat file for windows
open (my $fh, ">", "release/win/dau.bat") || die $!;
print $fh 'set WEBHOOK_URL=https://yourdiscordwebhookURLhere' . "\r\n";
print $fh 'set SCREENSHOTS="C:\your\screenshot\directory\here"' ."\r\n";
print $fh 'set USERNAME="Posted by Joe Bloggs"' . "\r\n";
print $fh 'set WATCH=10' . "\r\n";
print $fh 'dau.exe --webhook %WEBHOOK_URL% --directory %SCREENSHOTS% --username %USERNAME% --watch %WATCH%' . "\r\n";
print $fh 'pause' . "\r\n";
close $fh;
# we used to have a .bat file here, but no longer needed
}

78
config/config.go Normal file
View File

@@ -0,0 +1,78 @@
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/mitchellh/go-homedir"
)
// Config for the application
var Config struct {
WebHookURL string
Path string
Watch int
Username string
NoWatermark bool
Exclude string
}
const CurrentVersion string = "0.10"
// Load the current config or initialise with defaults
func LoadOrInit() {
configPath := configPath()
daulog.SendLog(fmt.Sprintf("Trying to load config from %s", configPath), daulog.LogTypeDebug)
_, err := os.Stat(configPath)
if os.IsNotExist(err) {
daulog.SendLog("NOTE: No config file, writing out sample configuration", daulog.LogTypeInfo)
daulog.SendLog("You need to set the configuration via the web interface", daulog.LogTypeInfo)
Config.WebHookURL = ""
Config.Path = homeDir() + string(os.PathSeparator) + "screenshots"
Config.Watch = 10
SaveConfig()
} else {
LoadConfig()
}
}
func LoadConfig() {
path := configPath()
data, err := ioutil.ReadFile(path)
if err != nil {
log.Fatalf("cannot read config file %s: %s", path, err.Error())
}
err = json.Unmarshal([]byte(data), &Config)
if err != nil {
log.Fatalf("cannot decode config file %s: %s", path, err.Error())
}
}
func SaveConfig() {
daulog.SendLog("saving configuration", daulog.LogTypeInfo)
path := configPath()
jsonString, _ := json.Marshal(Config)
err := ioutil.WriteFile(path, jsonString, os.ModePerm)
if err != nil {
log.Fatalf("Cannot save config %s: %s", path, err.Error())
}
}
func homeDir() string {
dir, err := homedir.Dir()
if err != nil {
panic(err)
}
return dir
}
func configPath() string {
homeDir := homeDir()
return homeDir + string(os.PathSeparator) + ".dau.json"
}

156
data/config.html Normal file
View File

@@ -0,0 +1,156 @@
<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">&nbsp;</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>

110
data/dau.css Normal file
View File

@@ -0,0 +1,110 @@
/*
* Globals
*/
/* Links */
a,
a:focus,
a:hover {
color: #fff;
}
/* Custom default button */
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none; /* Prevent inheritance from `body` */
background-color: #fff;
border: .05rem solid #fff;
}
/*
* Base structure
*/
html,
body {
height: 100%;
background-color: #333;
}
body {
display: -ms-flexbox;
display: flex;
color: #fff;
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.DAU-container {
max-width: 42em;
}
pre {
background-color: black;
color: aliceblue;
}
/*
* Header
*/
.masthead {
margin-bottom: 2rem;
}
.masthead-brand {
margin-bottom: 0;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .nav-link + .nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
@media (min-width: 48em) {
.masthead-brand {
float: left;
}
.nav-masthead {
float: right;
}
}
/*
* Cover
*/
.cover {
padding: 0 1.5rem;
}
.cover .btn-lg {
padding: .75rem 1.25rem;
font-weight: 700;
}
/*
* Footer
*/
.mastfoot {
color: rgba(255, 255, 255, .5);
}

8
data/index.html Normal file
View File

@@ -0,0 +1,8 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Discord Auto Upload</h1>
<p class="lead">Hey look, it's DAU :-)</p>
<p class="lead">
<a href="https://github.com/tardisx/discord-auto-upload" class="btn btn-lg btn-secondary" target="_blank">Learn more</a>
</p>
</main>

53
data/logs.html Normal file
View File

@@ -0,0 +1,53 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Logs</h1>
<p class="lead">Discord-auto-upload logs</p>
<div class="container">
<div class="row">
<div class="col-sm">
<button type="button" onClick="debug=1; get_logs();" class="btn btn-primary">all logs</button>
</div>
<div class="col-sm">
<button type="button" onClick="debug=0; get_logs();" class="btn btn-primary">no debug</button>
</div>
<div class="col-sm">
<button type="button" id="scroll-button" onClick="toggle_scroll();" class="btn btn-primary">disable auto-scroll</button>
</div>
</div>
</div>
<pre id="logs" class="text-left pre-scrollable">
</pre>
</main>
<script>
var debug = 0;
var scrl = true;
$(document).ready(function() {
get_logs();
setInterval(function() { get_logs(); }, 1000);
});
function toggle_scroll() {
scrl = !scrl;
if (scrl) {
$('#scroll-button').text('disable auto-scroll');
}
else {
$('#scroll-button').text('auto-scroll');
}
}
function get_logs() {
$.ajax({ method: 'get', url: '/rest/logs', data: { debug : debug }})
.done(function(data) {
$('#logs').text(data);
console.log('scrl is ', scrl);
if (scrl) {
$('#logs').scrollTop(10000);
}
});
}
</script>

39
data/uploads.html Normal file
View File

@@ -0,0 +1,39 @@
<main role="main" class="inner DAU">
<h1 class="DAU-heading">Uploads</h1>
<p class="lead">Discord-auto-upload uploads</p>
<table class="table table-condensed table-dark">
<thead>
<tr><th>uploaded</th><th>dt</th><th>thumb</th></tr>
</thead>
<tbody id="uploads">
</tbody>
</table>
</main>
<script>
$(document).ready(function() {
get_uploads();
});
function get_uploads() {
$.ajax({ method: 'get', url: '/rest/uploads'})
.done(function(data) {
console.log(data);
$('#uploads').empty();
if (! data) { return }
data.forEach(i => {
// {uploaded: true, uploaded_at: "2021-06-08T21:59:52.855936+09:30", url: "https://cdn.discordapp.com/attachments/849615269706203171/851800197046468628/dau736004285.png", width: 640, height: 640}
console.log(i);
row = $('<tr>');
row.append($('<td>').text(i.uploaded ? 'yes' : 'no'));
row.append($('<td>').text(i.uploaded_at));
row.append($('<td>').html($('<img>', { width : i.width/10, height : i.height/10, src : i.url })));
$('#uploads').prepend(row);
});
});
}
</script>

60
data/wrapper.tmpl Normal file
View File

@@ -0,0 +1,60 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="/dau.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="DAU-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">discord-auto-upload ({{.Version}})</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link {{ if eq .Path "index.html"}} active {{ end }}" href="/">Home</a>
<a class="nav-link {{ if eq .Path "config.html"}} active {{ end }}" href="/config.html">Config</a>
<a class="nav-link {{ if eq .Path "uploads.html"}} active {{ end }}" href="/uploads.html">Uploads</a>
<a class="nav-link {{ if eq .Path "logs.html"}} active {{ end }}" href="/logs.html">Logs</a>
</nav>
</div>
</header>
{{.Body}}
<footer class="mastfoot mt-auto">
<div class="inner">
<!-- <p>DAU template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> -->
</div>
</footer>
</div>
</body>
</html>

301
dau.go
View File

@@ -1,89 +1,90 @@
package main
//go:generate go-bindata -pkg assets -o assets/static.go -prefix data/ data
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"github.com/fogleman/gg"
"github.com/pborman/getopt"
"golang.org/x/image/font/inconsolata"
)
const currentVersion = "0.6"
// "github.com/skratchdot/open-golang/open"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/tardisx/discord-auto-upload/uploads"
"github.com/tardisx/discord-auto-upload/web"
)
var lastCheck = time.Now()
var newLastCheck = time.Now()
// Config for the application
type Config struct {
webhookURL string
path string
watch int
username string
noWatermark bool
exclude string
}
func main() {
config := parseOptions()
parseOptions()
// log.Print("Opening web browser")
// open.Start("http://localhost:9090")
web.StartWebServer()
checkPath(config.path)
checkUpdates()
log.Print("Waiting for images to appear in ", config.path)
daulog.SendLog(fmt.Sprintf("Waiting for images to appear in %s", config.Config.Path), daulog.LogTypeInfo)
// wander the path, forever
for {
err := filepath.Walk(config.path,
func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err, config) })
if err != nil {
log.Fatal("could not watch path", err)
if checkPath(config.Config.Path) {
err := filepath.Walk(config.Config.Path,
func(path string, f os.FileInfo, err error) error { return checkFile(path, f, err) })
if err != nil {
log.Fatal("could not watch path", err)
}
lastCheck = newLastCheck
}
lastCheck = newLastCheck
time.Sleep(time.Duration(config.watch) * 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) {
func checkPath(path string) bool {
src, err := os.Stat(path)
if err != nil {
log.Fatal(err)
log.Printf("Problem with path '%s': %s", path, err)
return false
}
if !src.IsDir() {
log.Fatal(path, " is not a directory")
os.Exit(1)
log.Printf("Problem with path '%s': is not a directory", path)
return false
}
return true
}
func checkUpdates() {
type GithubRelease struct {
HTMLURL string
TagName string
Name string
Body string
HTMLURL string `json:"html_url"`
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
}
daulog.SendLog("checking for new version", daulog.LogTypeInfo)
client := &http.Client{Timeout: time.Second * 5}
resp, err := client.Get("https://api.github.com/repos/tardisx/discord-auto-upload/releases/latest")
if err != nil {
log.Fatal("could not check for updates:", err)
daulog.SendLog(fmt.Sprintf("WARNING: Update check failed: %v", err), daulog.LogTypeError)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
@@ -98,26 +99,20 @@ func checkUpdates() {
log.Fatal("could not parse JSON: ", err)
}
if currentVersion < latest.TagName {
fmt.Printf("You are currently on version %s, but version %s is available\n", currentVersion, latest.TagName)
if config.CurrentVersion < latest.TagName {
fmt.Printf("You are currently on version %s, but version %s is available\n", config.CurrentVersion, latest.TagName)
fmt.Println("----------- Release Info -----------")
fmt.Println(latest.Body)
fmt.Println("------------------------------------")
fmt.Println("Upgrade at https://github.com/tardisx/discord-auto-upload/releases/latest")
daulog.SendLog(fmt.Sprintf("New version available: %s - download at https://github.com/tardisx/discord-auto-upload/releases/latest", latest.TagName), daulog.LogTypeInfo)
}
}
func parseOptions() Config {
func parseOptions() {
var newConfig Config
// Declare the flags to be used
webhookFlag := getopt.StringLong("webhook", 'w', "", "discord webhook URL")
pathFlag := getopt.StringLong("directory", 'd', "", "directory to scan, optional, defaults to current directory")
watchFlag := getopt.Int16Long("watch", 's', 10, "time between scans")
usernameFlag := getopt.StringLong("username", 'u', "", "username for the bot upload")
excludeFlag := getopt.StringLong("exclude", 'x', "", "exclude files containing this string")
noWatermarkFlag := getopt.BoolLong("no-watermark", 'n', "do not put a watermark on images before uploading")
helpFlag := getopt.BoolLong("help", 'h', "help")
versionFlag := getopt.BoolLong("version", 'v', "show version")
getopt.SetParameters("")
@@ -131,36 +126,20 @@ func parseOptions() Config {
if *versionFlag {
fmt.Println("dau - https://github.com/tardisx/discord-auto-upload")
fmt.Printf("Version: %s\n", currentVersion)
fmt.Printf("Version: %s\n", config.CurrentVersion)
os.Exit(0)
}
if !getopt.IsSet("directory") {
*pathFlag = "./"
log.Println("Defaulting to current directory")
}
if !getopt.IsSet("webhook") {
log.Fatal("ERROR: You must specify a --webhook URL")
}
newConfig.path = *pathFlag
newConfig.webhookURL = *webhookFlag
newConfig.watch = int(*watchFlag)
newConfig.username = *usernameFlag
newConfig.noWatermark = *noWatermarkFlag
newConfig.exclude = *excludeFlag
return newConfig
// grab the config
config.LoadOrInit()
}
func checkFile(path string, f os.FileInfo, err error, config Config) error {
func checkFile(path string, f os.FileInfo, err error) error {
if f.ModTime().After(lastCheck) && f.Mode().IsRegular() {
if fileEligible(config, path) {
if fileEligible(path) {
// process file
processFile(config, path)
processFile(path)
}
if newLastCheck.Before(f.ModTime()) {
@@ -171,9 +150,9 @@ func checkFile(path string, f os.FileInfo, err error, config Config) error {
return nil
}
func fileEligible(config Config, file string) bool {
func fileEligible(file string) bool {
if config.exclude != "" && strings.Contains(file, config.exclude) {
if config.Config.Exclude != "" && strings.Contains(file, config.Config.Exclude) {
return false
}
@@ -185,182 +164,8 @@ func fileEligible(config Config, file string) bool {
return false
}
func processFile(config Config, file string) {
func processFile(file string) {
if !config.noWatermark {
log.Print("Copying to temp location and watermarking ", file)
file = mungeFile(file)
}
log.Print("Uploading ", file)
extraParams := map[string]string{}
if config.username != "" {
extraParams["username"] = 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.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)
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.noWatermark {
log.Print("Removing temporary file ", file)
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
log.Printf("Will retry in %d seconds (%d remaining attempts)", retryTime, retry)
// 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
daulog.SendLog("Sending to uploader", daulog.LogTypeInfo)
uploads.AddFile(file)
}

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module github.com/tardisx/discord-auto-upload
go 1.15
require (
github.com/fogleman/gg v1.3.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/pborman/getopt v1.1.0
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e
)

11
go.sum Normal file
View File

@@ -0,0 +1,11 @@
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pborman/getopt v1.1.0 h1:eJ3aFZroQqq0bWmraivjQNt6Dmm5M0h2JcDW38/Azb0=
github.com/pborman/getopt v1.1.0/go.mod h1:FxXoW1Re00sQG/+KIkuSqRL/LwQgSkv7uyac+STFsbk=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

45
log/log.go Normal file
View File

@@ -0,0 +1,45 @@
package log
import (
"time"
)
type LogEntryType string
type LogEntry struct {
Timestamp time.Time `json:"ts"`
Type LogEntryType `json:"type"`
Entry string `json:"log"`
}
const (
LogTypeInfo = "info"
LogTypeError = "error"
LogTypeDebug = "debug"
)
var LogEntries []LogEntry
var logInput chan LogEntry
func init() {
// wait for log entries
logInput = make(chan LogEntry)
go func() {
for {
aLog := <-logInput
LogEntries = append(LogEntries, aLog)
for len(LogEntries) > 100 {
LogEntries = LogEntries[1:]
}
}
}()
}
func SendLog(entry string, entryType LogEntryType) {
logInput <- LogEntry{
Timestamp: time.Now(),
Entry: entry,
Type: entryType,
}
}

249
uploads/uploads.go Normal file
View File

@@ -0,0 +1,249 @@
// The uploads pacakge encapsulates dealing with file uploads to
// discord
package uploads
import (
"bytes"
"encoding/json"
"fmt"
"image"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"github.com/fogleman/gg"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"golang.org/x/image/font/inconsolata"
)
type Upload struct {
Uploaded bool `json:"uploaded"` // has this file been uploaded to discord
UploadedAt time.Time `json:"uploaded_at"`
originalFilename string // path on the local disk
mungedFilename string // post-watermark
Url string `json:"url"` // url on the discord CDN
Width int `json:"width"`
Height int `json:"height"`
}
var Uploads []*Upload
func AddFile(file string) {
thisUpload := Upload{
Uploaded: false,
originalFilename: file,
}
Uploads = append(Uploads, &thisUpload)
ProcessUpload(&thisUpload)
}
func ProcessUpload(up *Upload) {
file := up.originalFilename
if !config.Config.NoWatermark {
daulog.SendLog("Copying to temp location and watermarking ", daulog.LogTypeInfo)
file = mungeFile(file)
up.mungedFilename = file
}
if config.Config.WebHookURL == "" {
daulog.SendLog("WebHookURL is not configured - cannot upload!", daulog.LogTypeError)
return
}
extraParams := map[string]string{}
if config.Config.Username != "" {
daulog.SendLog("Overriding username with "+config.Config.Username, daulog.LogTypeInfo)
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 {
// {"message": "Request entity too large", "code": 40005}
log.Print("Bad response from server:", resp.StatusCode)
if b, err := ioutil.ReadAll(resp.Body); err == nil {
log.Print("Body:", string(b))
daulog.SendLog(fmt.Sprintf("Bad response: %s", string(b)), daulog.LogTypeError)
}
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Print("could not deal with body: ", err)
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
}
resp.Body.Close()
var res DiscordAPIResponse
err = json.Unmarshal(resBody, &res)
// {"id": "851092588608880670", "type": 0, "content": "", "channel_id": "849615269706203171", "author": {"bot": true, "id": "849615314274484224", "username": "abcdedf", "avatar": null, "discriminator": "0000"}, "attachments": [{"id": "851092588332449812", "filename": "dau480457962.png", "size": 859505, "url": "https://cdn.discordapp.com/attachments/849615269706203171/851092588332449812/dau480457962.png", "proxy_url": "https://media.discordapp.net/attachments/849615269706203171/851092588332449812/dau480457962.png", "width": 640, "height": 640, "content_type": "image/png"}], "embeds": [], "mentions": [], "mention_roles": [], "pinned": false, "mention_everyone": false, "tts": false, "timestamp": "2021-06-06T13:38:05.660000+00:00", "edited_timestamp": null, "flags": 0, "components": [], "webhook_id": "849615314274484224"}
daulog.SendLog(fmt.Sprintf("Response: %s", string(resBody[:])), daulog.LogTypeDebug)
if err != nil {
log.Print("could not parse JSON: ", err)
fmt.Println("Response was:", string(resBody[:]))
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
}
if len(res.Attachments) < 1 {
log.Print("bad response - no attachments?")
retriesRemaining--
sleepForRetries(retriesRemaining)
continue
}
var a = res.Attachments[0]
elapsed := time.Since(start)
rate := float64(a.Size) / elapsed.Seconds() / 1024.0
daulog.SendLog(fmt.Sprintf("Uploaded to %s %dx%d", a.URL, a.Width, a.Height), daulog.LogTypeInfo)
daulog.SendLog(fmt.Sprintf("id: %d, %d bytes transferred in %.2f seconds (%.2f KiB/s)", res.ID, a.Size, elapsed.Seconds(), rate), daulog.LogTypeInfo)
up.Url = a.URL
up.Uploaded = true
up.Width = a.Width
up.Height = a.Height
up.UploadedAt = time.Now()
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 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
}
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)
}

325
web/server.go Normal file
View File

@@ -0,0 +1,325 @@
package web
import (
"encoding/json"
"fmt"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"text/template"
"github.com/tardisx/discord-auto-upload/assets"
"github.com/tardisx/discord-auto-upload/config"
daulog "github.com/tardisx/discord-auto-upload/log"
"github.com/tardisx/discord-auto-upload/uploads"
)
// DAUWebServer - stuff for the web server
type DAUWebServer struct {
ConfigChange chan int
}
type valueStringResponse struct {
Success bool `json:"success"`
Value string `json:"value"`
}
type valueBooleanResponse struct {
Success bool `json:"success"`
Value bool `json:"value"`
}
type errorResponse struct {
Success bool `json:"success"`
Error string `json:"error"`
}
func getStatic(w http.ResponseWriter, r *http.Request) {
// haha this is dumb and I should change it
re := regexp.MustCompile(`[^a-zA-Z0-9\.]`)
path := r.URL.Path[1:]
sanitized_path := re.ReplaceAll([]byte(path), []byte("_"))
if string(sanitized_path) == "" {
sanitized_path = []byte("index.html")
}
data, err := assets.Asset(string(sanitized_path))
if err != nil {
// Asset was not found.
fmt.Fprintln(w, err)
}
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 {
Body string
Path string
Version string
}
b.Body = string(data)
b.Path = string(sanitized_path)
b.Version = config.CurrentVersion
t.Execute(w, b)
return
}
// otherwise we are a static thing
w.Header().Set("Content-Type", mime.TypeByExtension(extension))
w.Write(data)
//
}
// TODO there should be locks around all these config accesses
func getSetWebhook(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
config.Config.WebHookURL = r.PostForm.Get("value")
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.WebHookURL}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
// TODO there should be locks around all these config accesses
func getSetUsername(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.Username}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
config.Config.Username = r.PostForm.Get("value")
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.Username}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetWatch(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
i, err := strconv.Atoi(r.PostForm.Get("value"))
if err != nil {
response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for watch: %v", err)}
js, _ := json.Marshal(response)
w.Write(js)
return
}
if i < 1 {
response := errorResponse{Success: false, Error: "must be > 0"}
js, _ := json.Marshal(response)
w.Write(js)
return
}
config.Config.Watch = i
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: strconv.Itoa(config.Config.Watch)}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetNoWatermark(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
v := r.PostForm.Get("value")
if v != "0" && v != "1" {
response := errorResponse{Success: false, Error: fmt.Sprintf("Bad value for nowatermark: %v", err)}
js, _ := json.Marshal(response)
w.Write(js)
return
}
if v == "0" {
config.Config.NoWatermark = false
} else {
config.Config.NoWatermark = true
}
config.SaveConfig()
postResponse := valueBooleanResponse{Success: true, Value: config.Config.NoWatermark}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetDirectory(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.Path}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
newPath := r.PostForm.Get("value")
// sanity check this path
stat, err := os.Stat(newPath)
if os.IsNotExist(err) {
// not exist
response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - does not exist", newPath)}
js, _ := json.Marshal(response)
w.Write(js)
return
} else if !stat.IsDir() {
// not a directory
response := errorResponse{Success: false, Error: fmt.Sprintf("Path: %s - is not a directory", newPath)}
js, _ := json.Marshal(response)
w.Write(js)
return
}
config.Config.Path = newPath
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.Path}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getSetExclude(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
getResponse := valueStringResponse{Success: true, Value: config.Config.Exclude}
// I can't see any way this will fail
js, _ := json.Marshal(getResponse)
w.Write(js)
} else if r.Method == "POST" {
err := r.ParseForm()
if err != nil {
log.Fatal(err)
}
config.Config.Exclude = r.PostForm.Get("value")
config.SaveConfig()
postResponse := valueStringResponse{Success: true, Value: config.Config.Exclude}
js, _ := json.Marshal(postResponse)
w.Write(js)
}
}
func getLogs(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
showDebug := false
debug, present := r.URL.Query()["debug"]
if present && len(debug[0]) > 0 && debug[0] != "0" {
showDebug = true
}
text := ""
for _, log := range daulog.LogEntries {
if !showDebug && log.Type == daulog.LogTypeDebug {
continue
}
text = text + fmt.Sprintf(
"%-6s %-19s %s\n", log.Type, log.Timestamp.Format("2006-01-02 15:04:05"), log.Entry,
)
}
// js, _ := json.Marshal(daulog.LogEntries)
w.Write([]byte(text))
}
func getUploads(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
ups := uploads.Uploads
text, _ := json.Marshal(ups)
w.Write([]byte(text))
}
func StartWebServer() {
http.HandleFunc("/", getStatic)
http.HandleFunc("/rest/config/webhook", getSetWebhook)
http.HandleFunc("/rest/config/username", getSetUsername)
http.HandleFunc("/rest/config/watch", getSetWatch)
http.HandleFunc("/rest/config/nowatermark", getSetNoWatermark)
http.HandleFunc("/rest/config/directory", getSetDirectory)
http.HandleFunc("/rest/config/exclude", getSetExclude)
http.HandleFunc("/rest/logs", getLogs)
http.HandleFunc("/rest/uploads", getUploads)
go func() {
log.Print("Starting web server on http://localhost:9090")
err := http.ListenAndServe(":9090", nil) // set listen port
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}()
}