diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e9eecf5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + diff --git a/README.md b/README.md index e69de29..0c3a604 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..c9220bb --- /dev/null +++ b/backend/Dockerfile @@ -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"] \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..a9d74e6 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,3 @@ +module github.com/scanner/backend + +go 1.20 \ No newline at end of file diff --git a/backend/handlers/scan_handlers.go b/backend/handlers/scan_handlers.go new file mode 100644 index 0000000..4b1f0f7 --- /dev/null +++ b/backend/handlers/scan_handlers.go @@ -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) +} diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..da28454 --- /dev/null +++ b/backend/main.go @@ -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)) +} diff --git a/backend/middleware/cors.go b/backend/middleware/cors.go new file mode 100644 index 0000000..7a0b49b --- /dev/null +++ b/backend/middleware/cors.go @@ -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) + } +} diff --git a/backend/models/document.go b/backend/models/document.go new file mode 100644 index 0000000..e8aaf14 --- /dev/null +++ b/backend/models/document.go @@ -0,0 +1,7 @@ +package models + +// Document represents a scanned document +type Document struct { + Filename string `json:"filename"` + URL string `json:"url"` +} diff --git a/backend/models/scanner_state.go b/backend/models/scanner_state.go new file mode 100644 index 0000000..18c7d61 --- /dev/null +++ b/backend/models/scanner_state.go @@ -0,0 +1,10 @@ +package models + +import ( + "sync" +) + +type ScannerState struct { + IsScanning bool `json:"isScanning"` + Mu sync.Mutex `json:"-"` +} diff --git a/backend/services/sane.go b/backend/services/sane.go new file mode 100644 index 0000000..5cd56a3 --- /dev/null +++ b/backend/services/sane.go @@ -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 +} diff --git a/backend/services/scanner_service.go b/backend/services/scanner_service.go new file mode 100644 index 0000000..0cd5ece --- /dev/null +++ b/backend/services/scanner_service.go @@ -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 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2101893 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..4252404 --- /dev/null +++ b/frontend/Dockerfile @@ -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;"] \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..bd29bc0 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,188 @@ + + + + + + Document Scanner + + + +
+

Document Scanner

+ +
Scanner is idle
+ +
+ + +
+ +

Documents

+
+

No documents scanned yet.

+
+
+ + + + \ No newline at end of file diff --git a/frontend/site.conf b/frontend/site.conf new file mode 100644 index 0000000..b87df84 --- /dev/null +++ b/frontend/site.conf @@ -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; + } +}