initial commit
This commit is contained in:
commit
efcb1aeda3
12 changed files with 555 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
__debug_bin
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal 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
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"go.delveConfig": {
|
||||
"debugAdapter": "dlv-dap",
|
||||
}
|
||||
}
|
20
LICENSE.md
Normal file
20
LICENSE.md
Normal 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
12
README.md
Normal 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
43
config/config.go
Normal 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
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module velvettear/go-scan
|
||||
|
||||
go 1.18
|
165
index.html
Normal file
165
index.html
Normal 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
63
log/log.go
Normal 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
129
main.go
Normal 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(¶meters)
|
||||
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
64
util/date/date.go
Normal 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
|
||||
}
|
33
util/environment/environment.go
Normal file
33
util/environment/environment.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue