initial commit

This commit is contained in:
Daniel Sommer 2022-06-24 15:31:48 +02:00
commit efcb1aeda3
12 changed files with 555 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__debug_bin

17
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "go-scan",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"env": {
"GO_SCAN_SCANNER": "utsushi:esci:usb:/sys/devices/pci0000:00/0000:00:14.0/usb1/1-5/1-5:1.0",
"GO_SCAN_OUTPUTDIRECTORY": "/tmp",
"GO_SCAN_ARGUMENTS": "--jpeg-quality 100 --mode Color --scan-area ISO/A4/Portrait --resolution 600"
}
}
]
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"go.delveConfig": {
"debugAdapter": "dlv-dap",
}
}

20
LICENSE.md Normal file
View file

@ -0,0 +1,20 @@
# MIT License
**Copyright (c) 2022 Daniel Sommer \<daniel.sommer@velvettear.de\>**
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# go-scan
a simple http server serving a page to execute scans
## environment variables
- `GO_SCAN_ADDRESS`: listen address of the http server
- `GO_SCAN_PORT`: port of the http server
- `GO_SCAN_SCANNER`: name of the scanner to use
- `GO_SCAN_OUTPUTDIRECTORY`: path to the output directory for scans
- `GO_SCAN_ARGUMENTS`: extra arguments to pass to the scanimage command

43
config/config.go Normal file
View file

@ -0,0 +1,43 @@
package config
import (
"os"
"strings"
"velvettear/go-scan/util/environment"
)
// exported functions
func New() Config {
config := Config{
ServerConfig: serverConfig{
Address: environment.New("GO_SCAN_ADDRESS", "0.0.0.0").Value(),
Port: environment.New("GO_SCAN_PORT", "9000").Value(),
},
ScannerConfig: scannerConfig{
Scanner: environment.New("GO_SCAN_SCANNER", "").Value(),
OutputDirectory: environment.New("GO_SCAN_OUTPUTDIRECTORY", os.TempDir()).Value(),
Arguments: environment.New("GO_SCAN_ARGUMENTS", "").Value(),
},
}
if !strings.HasSuffix(config.ScannerConfig.OutputDirectory, "/") {
config.ScannerConfig.OutputDirectory = config.ScannerConfig.OutputDirectory + "/"
}
return config
}
// structs
type Config struct {
ServerConfig serverConfig
ScannerConfig scannerConfig
}
type serverConfig struct {
Port string
Address string
}
type scannerConfig struct {
Scanner string
OutputDirectory string
Arguments string
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module velvettear/go-scan
go 1.18

165
index.html Normal file
View file

@ -0,0 +1,165 @@
<html>
<head>
<title>go-scan</title>
<style>
#content {
position: absolute;
top: 33%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
height: 50%;
width: 50%;
}
#text {
opacity: 0;
transition: opacity 0.5s;
}
#result {
margin-top: 5%;
font-weight: 600;
font-size: 1.5em;
transition: opacity 0.5s;
}
#button {
background-color: #6e6e6e;
margin: auto;
width: fit-content;
height: fit-content;
border: none;
border-radius: 100%;
transition: background-color 0.5s;
}
#button:hover {
background-color: #4e4e4e;
}
#button-img {
min-width: 96px;
min-height: 96px;
width: 25vh;
height: 25vh;
}
#input {
margin-top: 5%;
transition: opacity 0.5s;
}
#input-label {
font-weight: 600;
font-size: 1.5rem;
margin-right: 5%;
}
body {
color: #e1e1e1;
background-color: #1e1e1e;
margin: 0;
padding: 0;
text-align: center;
}
h1 {
font-weight: 900;
font-size: 5rem;
}
</style>
</head>
<body>
<div id="content">
<div id="heading">
<h1>go-scan</h1>
</div>
<div id="button" onclick="startScan()">
<img id="button-img" src="https://cdn2.iconfinder.com/data/icons/usability-test/100/jan_yulck-26-512.png">
</div>
<div id="input">
<span id="input-label">filename:</span>
<input id="input-filename" type="text">
</div>
<div id="result">
<span id="text"></span>
</div>
</div>
</body>
<script>
function startScan() {
let button = document.getElementById("button");
let buttonDefaultColor = button.style.backgroundColor;
button.style.backgroundColor = "#ffee00";
let buttonImg = document.getElementById("button-img");
let result = document.getElementById("result");
result.style.visibility = "hidden";
let text = document.getElementById("text");
let input = document.getElementById("input");
input.style.opacity = "0"
// start rotate animation
let rotate = true;
let rotationDegree = 0;
let rotationInterval = setInterval(() => {
rotationDegree = rotateElement(buttonImg, rotationInterval, rotationDegree, !rotate, "Y");
}, 5);
// call api
fetch('scan', {
method: 'post',
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
filename: document.getElementById("input-filename").value
})
}).then(response => {
return response.json();
}).then(response => {
let buttonColor = "#008800";
result.style.visibility = "visible";
text.innerHTML = "scan finished<br>" + response.Message;
if (response.State !== "ok") {
text.style.color = "#ff0000";
buttonColor = "#880000";
text.innerHTML = "an error occured<br>" + text.innerHTML;
}
text.style.opacity = "100";
button.style.backgroundColor = buttonColor;
rotate = false;
setTimeout(() => {
text.style.opacity = "0";
result.style.visibility = "hidden";
button.style.backgroundColor = buttonDefaultColor;
input.style.opacity = "100";
}, 5000)
});
}
function rotateElement(element, interval, degree, stop, direction) {
if (element === undefined || interval === undefined) {
return;
}
if (degree === undefined) {
degree = 0;
}
if (stop === true && degree % 360 === 0) {
clearInterval(interval);
return;
}
degree++;
let value = "rotate";
if (direction !== undefined && (direction.toLowerCase() === "x" || direction.toLowerCase() === "y")) {
value += direction;
}
element.style.transform = value + "(" + degree + "deg)";
return degree;
}
</script>
</html>

63
log/log.go Normal file
View file

@ -0,0 +1,63 @@
package log
import (
"fmt"
"velvettear/go-scan/util/date"
)
// exported functions
func Debug(logMessage string, logExtras ...string) {
trace(0, logMessage, logExtras...)
}
func Info(logMessage string, logExtras ...string) {
trace(1, logMessage, logExtras...)
}
func Warning(logMessage string, logExtras ...string) {
trace(2, logMessage, logExtras...)
}
func Error(logMessage string, logExtras ...string) {
trace(3, logMessage, logExtras...)
}
// unexported functions
func trace(logLevel int, logMessage string, logExtras ...string) {
if len(logMessage) == 0 {
return
}
extras := ""
for index := 0; index < len(logExtras); index++ {
tmp := logExtras[index]
if len(tmp) == 0 {
continue
}
if index > 0 {
extras += " | "
}
extras += logExtras[index]
}
if len(extras) > 0 {
logMessage = logMessage + " (" + extras + ")"
}
fmt.Println(buildLogMessage(getPrefixForLogLevel(logLevel), logMessage))
}
func getPrefixForLogLevel(loglevel int) string {
switch loglevel {
case 3:
return "error"
case 2:
return "warning"
case 1:
return "info"
default:
return "debug"
}
}
func buildLogMessage(prefix string, message string) string {
timestamp := date.New()
return timestamp.GetFormattedDate() + " [" + prefix + "]" + " > " + message
}

129
main.go Normal file
View file

@ -0,0 +1,129 @@
package main
import (
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os/exec"
"strconv"
"strings"
"time"
"velvettear/go-scan/config"
"velvettear/go-scan/log"
"velvettear/go-scan/util/date"
)
//go:embed index.html
var html []byte
var configuration config.Config
func main() {
configuration = config.New()
startServer()
}
func startServer() {
serverAddress := configuration.ServerConfig.Address + ":" + configuration.ServerConfig.Port
http.HandleFunc("/", handleHTTPRequests)
log.Info("starting server '" + serverAddress + "'...")
error := http.ListenAndServe(serverAddress, nil)
if error != nil {
log.Error("an error occured starting the server", error.Error())
}
}
func handleHTTPRequests(writer http.ResponseWriter, request *http.Request) {
if strings.ToLower(request.Method) == "post" {
result := handleScan(request)
writer.Header().Set("Content-Type", "application/json")
json, _ := json.Marshal(result)
writer.Write(json)
return
}
writer.Write(html)
}
func handleScan(request *http.Request) *result {
log.Info("starting scan...")
timestamp := date.Milliseconds()
var parameters requestParameters
parseErr := json.NewDecoder(request.Body).Decode(&parameters)
if parseErr != nil {
log.Error("error parsing request parameters", parseErr.Error())
}
scanName := generateScanName(parameters.Filename)
args := []string{}
if len(configuration.ScannerConfig.Scanner) > 0 {
args = append(args, "-d")
args = append(args, configuration.ScannerConfig.Scanner)
}
args = append(args, "-o")
args = append(args, scanName)
extraArgs := strings.Split(configuration.ScannerConfig.Arguments, " ")
for index := 0; index < len(extraArgs); index++ {
args = append(args, extraArgs[index])
}
log.Debug("executing command 'scanimage'...", args...)
scanCmd := exec.Command(
"scanimage",
args...,
)
_, err := scanCmd.Output()
if err != nil {
log.Error("an error occured executing the scan", err.Error())
result := &result{
State: "error",
Message: err.Error(),
}
return result
}
result := &result{
State: "ok",
Message: scanName,
}
log.Info("finished scan after "+strconv.Itoa(date.GetTimeDifference(timestamp))+"ms", scanName)
return result
}
func generateScanName(filename string) string {
if len(filename) == 0 {
day := fmt.Sprint(time.Now().Day())
if len(day) < 2 {
day = "0" + day
}
month := fmt.Sprint(int(time.Now().Month()))
if len(month) < 2 {
month = "0" + month
}
hour := fmt.Sprint(time.Now().Hour())
if len(hour) < 2 {
hour = "0" + hour
}
minute := fmt.Sprint(time.Now().Minute())
if len(minute) < 2 {
minute = "0" + minute
}
second := fmt.Sprint(time.Now().Second())
if len(second) < 2 {
second = "0" + second
}
filename = day + month + fmt.Sprint(time.Now().Year()) + "-" + hour + minute + second
}
if !strings.HasSuffix(filename, ".png") {
filename += ".png"
}
return configuration.ScannerConfig.OutputDirectory + filename
}
// structs
type result struct {
State string
Message string
}
type requestParameters struct {
Filename string
}

64
util/date/date.go Normal file
View file

@ -0,0 +1,64 @@
package date
import (
"fmt"
"time"
)
// exported functions
func New() Date {
now := time.Now()
day := fmt.Sprint(now.Day())
if len(day) < 2 {
day = "0" + day
}
month := fmt.Sprint(int(now.Month()))
if len(month) < 2 {
month = "0" + month
}
year := fmt.Sprint(now.Year())
hour := fmt.Sprint(now.Hour())
if len(hour) < 2 {
hour = "0" + hour
}
minute := fmt.Sprint(now.Minute())
if len(minute) < 2 {
minute = "0" + minute
}
second := fmt.Sprint(now.Second())
if len(second) < 2 {
second = "0" + second
}
return Date{
Day: day,
Month: month,
Year: year,
Hour: hour,
Minute: minute,
Second: second,
GetFormattedDate: func() string {
return day + "." + month + "." + year + " " + hour + ":" + minute + ":" + second
},
}
}
func Milliseconds() int {
return int(time.Now().UnixMilli())
}
func GetTimeDifference(timestamp int) int {
return Milliseconds() - timestamp
}
// structs
type getFormattedDate func() string
type Date struct {
Day string
Month string
Year string
Hour string
Minute string
Second string
GetFormattedDate getFormattedDate
}

View file

@ -0,0 +1,33 @@
package environment
import (
"os"
)
// exported functions
func New(name string, defaultValue string) EnvironmentVariable {
environmentVariable := EnvironmentVariable{
Key: name,
Default: defaultValue,
Value: func() string {
if len(name) == 0 {
return defaultValue
}
value := os.Getenv(name)
if len(value) == 0 {
return defaultValue
}
return value
},
}
return environmentVariable
}
// structs
type getValue func() string
type EnvironmentVariable struct {
Key string
Default string
Value getValue
}