inital commit

This commit is contained in:
Tim Lappe 2025-03-23 17:33:43 +01:00
parent f50224d8bb
commit 52a2ae9a31
15 changed files with 676 additions and 0 deletions

7
Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,62 @@
# Document Scanner Application
A simple document scanning application with Go backend and HTML/JavaScript frontend.
## Project Structure
```
scanner/
├── backend/ # Go backend
│ └── main.go # Main Go server file
└── frontend/ # Frontend
└── index.html # Main HTML file
```
## Getting Started
### Prerequisites
- Go 1.16+ installed
### Backend Setup
1. Navigate to the backend directory:
```
cd backend
```
2. Run the Go server:
```
go run main.go
```
The server will start on port 8080.
### Frontend Setup
1. Simply open the `frontend/index.html` file in a web browser.
## API Endpoints
The backend provides the following endpoints:
- `POST /scan/start`: Starts a new scanning session
- `POST /scan/abort`: Aborts the current scanning session
- `GET /documents`: Returns documents from the current session
## Usage
1. Open the frontend in your browser
2. Click "Start Scanning" to begin scanning documents
3. The documents will appear in the grid below as they are scanned
4. Click "Abort Scanning" to stop the process at any time
5. Click "Refresh Documents" to manually refresh the document list
## Notes
This is a simplified simulation of document scanning. In a real-world application, you would need to:
1. Integrate with actual scanning hardware
2. Implement proper error handling
3. Add authentication and security
4. Add persistent storage for documents

17
backend/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM golang:1.22-bookworm
# Install scanimage (part of sane-utils)
RUN apt-get update && apt-get install -y sane-utils iputils-ping && apt-get clean && rm -rf /var/lib/apt/lists/*
# Add non-root user
RUN useradd -m backend
USER backend
WORKDIR /home/backend
COPY --chown=backend:backend . .
RUN go build -o main
CMD ["./main"]

3
backend/go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/scanner/backend
go 1.20

View File

@ -0,0 +1,88 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/scanner/backend/middleware"
"github.com/scanner/backend/services"
)
// ScanHandler manages scan-related HTTP endpoints
type ScanHandler struct {
scannerService *services.ScannerService
}
// NewScanHandler creates a new scan handler
func NewScanHandler(scannerService *services.ScannerService) *ScanHandler {
return &ScanHandler{
scannerService: scannerService,
}
}
// GetScanStatus handles requests to get the current scan status
func (h *ScanHandler) GetScanStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
middleware.ApplyCors(w, r)
status := h.scannerService.IsScanning()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"isScanning": status})
}
// StartScan handles requests to start a new scan
func (h *ScanHandler) StartScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
middleware.ApplyCors(w, r)
success, _ := h.scannerService.StartScan()
if !success {
http.Error(w, "Scan already in progress", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// AbortScan handles requests to abort an ongoing scan
func (h *ScanHandler) AbortScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
middleware.ApplyCors(w, r)
success, _ := h.scannerService.AbortScan()
if !success {
http.Error(w, "No scan in progress", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
// GetDocuments handles requests to retrieve all scanned documents
func (h *ScanHandler) GetDocuments(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
middleware.ApplyCors(w, r)
documents := h.scannerService.GetDocuments()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(documents)
}

35
backend/main.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"log"
"net/http"
"github.com/scanner/backend/handlers"
"github.com/scanner/backend/middleware"
"github.com/scanner/backend/services"
)
func main() {
// Initialize services
scannerService := services.NewScannerService()
// Initialize handlers
scanHandler := handlers.NewScanHandler(scannerService)
// Set up routes with middleware
mux := http.NewServeMux()
// Register API routes
mux.HandleFunc("/scan/start", scanHandler.StartScan)
mux.HandleFunc("/scan/abort", scanHandler.AbortScan)
mux.HandleFunc("/scan/status", scanHandler.GetScanStatus)
mux.HandleFunc("/documents", scanHandler.GetDocuments)
fileServer := http.FileServer(http.Dir("/home/backend/var/documents"))
mux.Handle("/files/", http.StripPrefix("/files", fileServer))
handler := middleware.CorsMiddleware(mux)
log.Println("Server starting on port 80...")
log.Fatal(http.ListenAndServe(":80", handler))
}

View File

@ -0,0 +1,32 @@
package middleware
import (
"net/http"
)
// CorsMiddleware adds CORS headers to responses
func CorsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// ApplyCors adds CORS headers to a response
func ApplyCors(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
}
}

View File

@ -0,0 +1,7 @@
package models
// Document represents a scanned document
type Document struct {
Filename string `json:"filename"`
URL string `json:"url"`
}

View File

@ -0,0 +1,10 @@
package models
import (
"sync"
)
type ScannerState struct {
IsScanning bool `json:"isScanning"`
Mu sync.Mutex `json:"-"`
}

51
backend/services/sane.go Normal file
View File

@ -0,0 +1,51 @@
package services
import (
"bytes"
"fmt"
"os/exec"
"strings"
)
func scanImage(args ...string) (string, error) {
cmd := exec.Command("scanimage", args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
var stdout bytes.Buffer
cmd.Stdout = &stdout
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("error scanning: %v", stderr.String())
}
return stdout.String(), nil
}
func status() (string, error) {
result, err := scanImage("--status")
if err != nil {
return "", fmt.Errorf("error getting status: %v", err)
}
return result, nil
}
func listDevices() ([]string, error) {
result, err := scanImage("--list-devices")
if err != nil {
return nil, fmt.Errorf("error listing devices: %v", err)
}
return strings.Split(result, "\n"), nil
}
func startScan(outputFile string) (string, error) {
result, err := scanImage("-d", "escl:https://printer:443", "--format", "png", "--output-file", outputFile)
if err != nil {
return "", fmt.Errorf("error scanning: %v", err)
}
return result, nil
}

View File

@ -0,0 +1,119 @@
package services
import (
"fmt"
"os"
"strings"
"time"
"github.com/scanner/backend/models"
)
// ScannerService handles scanner operations
type ScannerService struct {
state models.ScannerState
}
// NewScannerService creates a new scanner service
func NewScannerService() *ScannerService {
return &ScannerService{
state: models.ScannerState{
IsScanning: false,
},
}
}
// GetScanStatus returns the current scan status
func (s *ScannerService) IsScanning() bool {
s.state.Mu.Lock()
state := s.state.IsScanning
s.state.Mu.Unlock()
return state
}
// StartScan begins a new scanning process
func (s *ScannerService) StartScan() (bool, error) {
s.state.Mu.Lock()
defer s.state.Mu.Unlock()
if s.state.IsScanning {
return false, nil
}
s.state.IsScanning = true
go s.scan()
return true, nil
}
// AbortScan stops an in-progress scan
func (s *ScannerService) AbortScan() (bool, error) {
s.state.Mu.Lock()
defer s.state.Mu.Unlock()
if !s.state.IsScanning {
return false, nil
}
s.state.IsScanning = false
return true, nil
}
// GetDocuments returns all scanned documents
func (s *ScannerService) GetDocuments() []models.Document {
const outputDir = "/home/backend/var/documents"
files, err := os.ReadDir(outputDir)
if err != nil {
return []models.Document{}
}
documents := []models.Document{}
for _, file := range files {
if file.IsDir() {
continue
}
filename := file.Name()
if !strings.HasPrefix(filename, "scan_") || !strings.HasSuffix(filename, ".jpg") {
continue
}
documents = append(documents, models.Document{
Filename: filename,
URL: fmt.Sprintf("/api/files/%s", filename),
})
}
return documents
}
func (s *ScannerService) scan() {
s.state.Mu.Lock()
isScanning := s.state.IsScanning
s.state.Mu.Unlock()
if !isScanning {
return
}
outputDir := "/home/backend/var/documents"
timestamp := time.Now().Format("20060102150405")
outputFile := fmt.Sprintf("%s/scan_%s.jpg", outputDir, timestamp)
_, err := startScan(outputFile)
s.state.Mu.Lock()
defer s.state.Mu.Unlock()
if !s.state.IsScanning {
return
}
if err != nil {
fmt.Printf("%v\n", err)
}
s.state.IsScanning = false
}

28
docker-compose.yml Normal file
View File

@ -0,0 +1,28 @@
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8089:80"
extra_hosts:
- "printer:192.168.178.36"
networks:
- printer_network
cap_add:
- NET_ADMIN
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "8090:80"
networks:
- printer_network
networks:
printer_network:
driver: bridge
#scanimage --source "escl:https://printer:443" --format=png --no-ssl-verification

7
frontend/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM nginx:latest
COPY ./public /usr/share/nginx/html
COPY site.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]

188
frontend/public/index.html Normal file
View File

@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Scanner</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.controls {
display: flex;
gap: 10px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #4285f4;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #3367d6;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.document-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.document-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.document-card img {
width: 100%;
height: 150px;
object-fit: cover;
}
.document-card .info {
padding: 10px;
}
.status {
margin-bottom: 10px;
padding: 10px;
border-radius: 4px;
}
.status.scanning {
background-color: #e6f7ff;
border: 1px solid #91d5ff;
}
.status.idle {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
}
</style>
</head>
<body>
<div class="container">
<h1>Document Scanner</h1>
<div id="status" class="status idle">Scanner is idle</div>
<div class="controls">
<button id="startBtn">Start Scanning</button>
<button id="abortBtn" disabled>Abort Scanning</button>
</div>
<h2>Documents</h2>
<div id="documents" class="document-grid">
<p>No documents scanned yet.</p>
</div>
</div>
<script>
const API_URL = `/api`;
const startBtn = document.getElementById('startBtn');
const abortBtn = document.getElementById('abortBtn');
const statusEl = document.getElementById('status');
const documentsEl = document.getElementById('documents');
function setScannerState(scanning) {
startBtn.disabled = scanning;
abortBtn.disabled = !scanning;
if (scanning) {
statusEl.textContent = 'Scanner is running...';
statusEl.className = 'status scanning';
} else {
statusEl.textContent = 'Scanner is idle';
statusEl.className = 'status idle';
}
}
async function startScanning() {
startBtn.disabled = true;
try {
const response = await fetch(`${API_URL}/scan/start`, {
method: 'POST'
});
} catch (error) {
console.error('Error starting scan:', error);
alert('Failed to connect to scanner service.');
startBtn.disabled = false;
}
}
async function abortScanning() {
abortBtn.disabled = true;
try {
const response = await fetch(`${API_URL}/scan/abort`, {
method: 'POST'
});
} catch (error) {
console.error('Error aborting scan:', error);
alert('Failed to connect to scanner service.');
abortBtn.disabled = false;
}
}
function pollDocuments() {
fetch(`${API_URL}/documents`)
.then(response => response.json())
.then(documents => {
if (documents.length === 0) {
documentsEl.innerHTML = '<p>No documents scanned yet.</p>';
setTimeout(() => {
pollDocuments();
}, 2000);
return;
}
documentsEl.innerHTML = documents.map(doc => `
<div class="document-card">
<img src="${doc.url}" alt="${doc.filename}">
<div class="info">
<h3>${doc.filename}</h3>
</div>
</div>
`).join('');
setTimeout(() => {
pollDocuments();
}, 2000);
})
.catch(error => {
console.error('Error fetching documents:', error);
documentsEl.innerHTML = '<p>Error loading documents.</p>';
});
}
function pollStatus() {
fetch(`${API_URL}/scan/status`)
.then(response => response.json())
.then(data => {
setScannerState(data.isScanning);
setTimeout(() => {
pollStatus();
}, 1000);
});
}
startBtn.addEventListener('click', startScanning);
abortBtn.addEventListener('click', abortScanning);
pollStatus();
pollDocuments();
</script>
</body>
</html>

22
frontend/site.conf Normal file
View File

@ -0,0 +1,22 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:80/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}