diff --git a/contact-scrape-test/Dockerfile b/contact-scrape-test/Dockerfile deleted file mode 100644 index f8c9dec..0000000 --- a/contact-scrape-test/Dockerfile +++ /dev/null @@ -1,55 +0,0 @@ -# Build stage -FROM golang:1.21-alpine AS builder - -# Install git and ca-certificates (needed for fetching dependencies) -RUN apk add --no-cache git ca-certificates - -# Set the working directory -WORKDIR /build - -# Copy go mod files -COPY go.mod go.sum ./ - -# Download dependencies -RUN go mod download - -# Copy source code -COPY contact-scrape.go ./ - -# Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o contact-scrape . - -# Runtime stage -FROM alpine:latest - -# Install ca-certificates for HTTPS requests -RUN apk --no-cache add ca-certificates tzdata - -# Create non-root user -RUN addgroup -g 1001 -S appgroup && \ - adduser -u 1001 -S appuser -G appgroup - -# Set working directory -WORKDIR /app - -# Copy binary from builder stage -COPY --from=builder /build/contact-scrape . - -# Copy HTML file -COPY index.html . - -# Create data directory -RUN mkdir -p data && chown -R appuser:appgroup /app - -# Switch to non-root user -USER appuser - -# Expose port -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1 - -# Run the application -CMD ["./contact-scrape"] \ No newline at end of file diff --git a/contact-scrape-test/Makefile b/contact-scrape-test/Makefile deleted file mode 100644 index 3370087..0000000 --- a/contact-scrape-test/Makefile +++ /dev/null @@ -1,141 +0,0 @@ -# Contact Scrape Makefile - -.PHONY: build run clean install dev test help - -# Variables -BINARY_NAME=contact-scrape -BUILD_DIR=build -PORT=80 - -help: ## Show this help message - @echo "Available commands:" - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) - -##@ Development -dev: ## Run the application in development mode - @echo "Starting development server..." - @go run . - -run: build ## Build and run the application - @echo "Starting $(BINARY_NAME)..." - @./$(BUILD_DIR)/$(BINARY_NAME) - -test: ## Run tests - @echo "Running tests..." - @go test -v ./... - -##@ Build -build: ## Build the application - @echo "Building $(BINARY_NAME)..." - @mkdir -p $(BUILD_DIR) - @go build -o $(BUILD_DIR)/$(BINARY_NAME) . - @echo "Binary built: $(BUILD_DIR)/$(BINARY_NAME)" - -build-linux: ## Build for Linux (useful for deployment) - @echo "Building $(BINARY_NAME) for Linux..." - @mkdir -p $(BUILD_DIR) - @GOOS=linux GOARCH=amd64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-linux . - @echo "Linux binary built: $(BUILD_DIR)/$(BINARY_NAME)-linux" - -##@ Dependencies -deps: ## Download dependencies - @echo "Downloading dependencies..." - @go mod download - @go mod tidy - -##@ Deployment -install: build ## Install the service (requires sudo) - @echo "Installing contact-scrape service..." - @sudo mkdir -p /opt/contact-scrape - @sudo cp $(BUILD_DIR)/$(BINARY_NAME) /opt/contact-scrape/ - @sudo cp index.html /opt/contact-scrape/ - @sudo mkdir -p /opt/contact-scrape/data - @sudo chown -R www-data:www-data /opt/contact-scrape - @sudo cp contact-scrape.service /etc/systemd/system/ - @sudo systemctl daemon-reload - @sudo systemctl enable contact-scrape - @echo "Service installed. Start with: sudo systemctl start contact-scrape" - -uninstall: ## Uninstall the service (requires sudo) - @echo "Uninstalling contact-scrape service..." - @sudo systemctl stop contact-scrape 2>/dev/null || true - @sudo systemctl disable contact-scrape 2>/dev/null || true - @sudo rm -f /etc/systemd/system/contact-scrape.service - @sudo systemctl daemon-reload - @sudo rm -rf /opt/contact-scrape - @echo "Service uninstalled." - -status: ## Check service status - @sudo systemctl status contact-scrape - -start: ## Start the service - @sudo systemctl start contact-scrape - -stop: ## Stop the service - @sudo systemctl stop contact-scrape - -restart: ## Restart the service - @sudo systemctl restart contact-scrape - -logs: ## View service logs - @sudo journalctl -u contact-scrape -f - -##@ File Management -upload-xlsx: ## Upload contacts.xlsx to server (set SERVER variable) - @if [ -z "$(SERVER)" ]; then echo "Usage: make upload-xlsx SERVER=user@hostname"; exit 1; fi - @echo "Uploading contacts.xlsx to $(SERVER)..." - @scp contacts.xlsx $(SERVER):/opt/contact-scrape/ - @ssh $(SERVER) "sudo chown www-data:www-data /opt/contact-scrape/contacts.xlsx" - @ssh $(SERVER) "sudo systemctl restart contact-scrape" - @echo "File uploaded and service restarted." - -##@ Monitoring -monitor: ## Monitor the service with file watching - @echo "Monitoring contacts.xlsx for changes..." - @while true; do \ - inotifywait -e modify contacts.xlsx 2>/dev/null && \ - echo "File changed, reloading..." && \ - curl -X POST http://localhost:$(PORT)/reload; \ - sleep 1; \ - done - -##@ Utilities -clean: ## Clean build artifacts - @echo "Cleaning build artifacts..." - @rm -rf $(BUILD_DIR) - @rm -f data/contacts.json - -setup-dirs: ## Create necessary directories - @mkdir -p data - @mkdir -p $(BUILD_DIR) - -check-file: ## Check if contacts.xlsx exists and show info - @if [ -f "contacts.xlsx" ]; then \ - echo "✓ contacts.xlsx found"; \ - ls -lh contacts.xlsx; \ - else \ - echo "✗ contacts.xlsx not found"; \ - echo "Please place your Excel file in the current directory"; \ - fi - -##@ Docker (Optional) -docker-build: ## Build Docker image - @echo "Building Docker image..." - @docker build -t contact-scrape . - -docker-run: ## Run in Docker container - @echo "Running Docker container..." - @docker run -p $(PORT):$(PORT) -v $(PWD)/contacts.xlsx:/app/contacts.xlsx -v $(PWD)/data:/app/data contact-scrape - -##@ Information -info: ## Show application information - @echo "Contact Scrape Application" - @echo "=========================" - @echo "Port: $(PORT)" - @echo "Binary: $(BINARY_NAME)" - @echo "Build dir: $(BUILD_DIR)" - @echo "" - @echo "Endpoints:" - @echo " http://localhost:$(PORT)/ - Web interface" - @echo " http://localhost:$(PORT)/contacts - JSON API" - @echo " http://localhost:$(PORT)/reload - Reload data (POST)" \ No newline at end of file diff --git a/contact-scrape-test/contact-scrape.go b/contact-scrape-test/contact-scrape.go deleted file mode 100644 index 945d0cc..0000000 --- a/contact-scrape-test/contact-scrape.go +++ /dev/null @@ -1,403 +0,0 @@ -package main - -import ( - "crypto/md5" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "regexp" - "strings" - "time" - - "github.com/xuri/excelize/v2" -) - -type Contact struct { - Name string `json:"name"` - Position string `json:"position"` - Phone string `json:"phone,omitempty"` - ServicePhone string `json:"service_phone,omitempty"` - Table int `json:"table"` // 1 for first table, 2 for second table -} - -type ContactData struct { - Contacts []Contact `json:"contacts"` - LastUpdated time.Time `json:"last_updated"` - FileHash string `json:"file_hash"` -} - -var ( - currentData *ContactData - dataFile = "data/contacts.json" - xlsxFile = "contacts.xlsx" -) - -func main() { - // Create data directory if it doesn't exist - if err := os.MkdirAll("data", 0755); err != nil { - log.Printf("Warning: Could not create data directory: %v", err) - } - - // Load existing data or parse from Excel - loadData() - - // Set up HTTP handlers - http.HandleFunc("/", serveIndex) - http.HandleFunc("/contacts", serveContacts) - http.HandleFunc("/reload", reloadData) - - // Start server - port := os.Getenv("PORT") - if port == "" { - port = "80" - } - - log.Printf("Server starting on port %s", port) - log.Printf("Access the application at: http://localhost:%s", port) - log.Fatal(http.ListenAndServe(":"+port, nil)) -} - -func loadData() { - // Check if Excel file exists - if _, err := os.Stat(xlsxFile); os.IsNotExist(err) { - log.Printf("Excel file %s not found, using empty data", xlsxFile) - currentData = &ContactData{ - Contacts: []Contact{}, - LastUpdated: time.Now(), - FileHash: "", - } - return - } - - // Calculate current file hash - currentHash, err := calculateFileHash(xlsxFile) - if err != nil { - log.Printf("Error calculating file hash: %v", err) - return - } - - // Check if cached data exists and is up to date - if cachedData, err := loadCachedData(); err == nil { - if cachedData.FileHash == currentHash { - log.Println("Using cached data (file unchanged)") - currentData = cachedData - return - } - } - - // Parse Excel file - log.Println("Parsing Excel file...") - contacts, err := parseExcelFile(xlsxFile) - if err != nil { - log.Printf("Error parsing Excel file: %v", err) - // Use empty data if parsing fails - currentData = &ContactData{ - Contacts: []Contact{}, - LastUpdated: time.Now(), - FileHash: currentHash, - } - return - } - - currentData = &ContactData{ - Contacts: contacts, - LastUpdated: time.Now(), - FileHash: currentHash, - } - - // Save to cache - if err := saveCachedData(currentData); err != nil { - log.Printf("Warning: Could not save cached data: %v", err) - } - - log.Printf("Loaded %d contacts from Excel file", len(contacts)) -} - -func calculateFileHash(filename string) (string, error) { - file, err := os.Open(filename) - if err != nil { - return "", err - } - defer file.Close() - - hash := md5.New() - if _, err := io.Copy(hash, file); err != nil { - return "", err - } - - return fmt.Sprintf("%x", hash.Sum(nil)), nil -} - -func loadCachedData() (*ContactData, error) { - file, err := os.Open(dataFile) - if err != nil { - return nil, err - } - defer file.Close() - - var data ContactData - decoder := json.NewDecoder(file) - if err := decoder.Decode(&data); err != nil { - return nil, err - } - - return &data, nil -} - -func saveCachedData(data *ContactData) error { - file, err := os.Create(dataFile) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - return encoder.Encode(data) -} - -func parseExcelFile(filename string) ([]Contact, error) { - f, err := excelize.OpenFile(filename) - if err != nil { - return nil, fmt.Errorf("failed to open Excel file: %v", err) - } - defer f.Close() - - // Get the first sheet name - sheets := f.GetSheetList() - if len(sheets) == 0 { - return nil, fmt.Errorf("no sheets found in Excel file") - } - - sheetName := sheets[0] - var contacts []Contact - - // Parse first table (A-D columns) - contacts = append(contacts, parseTable(f, sheetName, "A", "D", 1)...) - - // Parse second table (F-H columns) - contacts = append(contacts, parseTable(f, sheetName, "F", "H", 2)...) - - return contacts, nil -} - -func parseTable(f *excelize.File, sheetName, startCol, endCol string, tableNum int) []Contact { - var contacts []Contact - var currentContact *Contact - - // Get all rows in the sheet - rows, err := f.GetRows(sheetName) - if err != nil { - log.Printf("Error getting rows: %v", err) - return contacts - } - - // Skip header rows (first 3 rows based on your description) - startRow := 3 - if len(rows) <= startRow { - return contacts - } - - // Column indices - var nameCol, positionCol, phoneCol, servicePhoneCol int - if tableNum == 1 { - nameCol, positionCol, phoneCol, servicePhoneCol = 0, 1, 2, 3 // A, B, C, D - } else { - nameCol, positionCol, phoneCol = 5, 6, 7 // F, G, H - } - - for i := startRow; i < len(rows); i++ { - row := rows[i] - - // Skip if row is too short - if len(row) <= nameCol { - continue - } - - // Check for "Aktualizace" - end of data - if len(row) > nameCol && strings.Contains(strings.ToLower(row[nameCol]), "aktualizace") { - break - } - - // Check for special formatting rows (like "*02(xx)") - if len(row) > positionCol && strings.Contains(row[positionCol], "*") { - continue - } - - name := strings.TrimSpace(row[nameCol]) - position := "" - phone := "" - servicePhone := "" - - if len(row) > positionCol { - position = strings.TrimSpace(row[positionCol]) - } - if len(row) > phoneCol { - phone = strings.TrimSpace(row[phoneCol]) - } - if tableNum == 1 && len(row) > servicePhoneCol { - servicePhone = strings.TrimSpace(row[servicePhoneCol]) - } - - // Clean phone numbers - phone = cleanPhoneNumber(phone) - servicePhone = cleanPhoneNumber(servicePhone) - - // If we have a name, start a new contact - if name != "" && !strings.Contains(name, "(") { - currentContact = &Contact{ - Name: name, - Position: position, - Phone: phone, - ServicePhone: servicePhone, - Table: tableNum, - } - contacts = append(contacts, *currentContact) - } else if currentContact != nil { - // This is additional data for the current contact - newContact := *currentContact - if position != "" { - newContact.Position = position - } - if phone != "" { - newContact.Phone = phone - } - if servicePhone != "" { - newContact.ServicePhone = servicePhone - } - contacts = append(contacts, newContact) - } - } - - return contacts -} - -func cleanPhoneNumber(phone string) string { - if phone == "" { - return "" - } - - // Remove extra whitespace - phone = strings.TrimSpace(phone) - - // Remove common formatting characters - re := regexp.MustCompile(`[^\d+\-\s()]`) - phone = re.ReplaceAllString(phone, "") - - // If it's just a short number (internal extension), keep as is - if len(phone) <= 3 { - return phone - } - - // If it looks like a Czech number without country code, add it - if regexp.MustCompile(`^[67]\d{8}$`).MatchString(strings.ReplaceAll(phone, " ", "")) { - return "+420 " + phone - } - - return phone -} - -func serveIndex(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - - // Check if index.html exists - if _, err := os.Stat("index.html"); os.IsNotExist(err) { - // Serve embedded HTML if file doesn't exist - w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprint(w, getEmbeddedHTML()) - return - } - - http.ServeFile(w, r, "index.html") -} - -func serveContacts(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - - if currentData == nil { - http.Error(w, `{"error": "No data available"}`, http.StatusInternalServerError) - return - } - - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - encoder.Encode(currentData) -} - -func reloadData(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - log.Println("Manual reload requested") - loadData() - - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status": "reloaded", "contacts_count": %d}`, len(currentData.Contacts)) -} - -func getEmbeddedHTML() string { - return ` - - - - - Kontakty - - - - -
-
-

📞 Firemní telefonní seznam

-

Poppe + Potthoff kontakty

-
-
- -
-
-
- -
- -
-
- - -
-
- -
-
-

Načítání kontaktů...

-
- - -
-
- - - -` -} diff --git a/contact-scrape-test/contact-scrape.service b/contact-scrape-test/contact-scrape.service deleted file mode 100644 index 954aa48..0000000 --- a/contact-scrape-test/contact-scrape.service +++ /dev/null @@ -1,29 +0,0 @@ -[Unit] -Description=Contact Scrape Service -After=network.target - -[Service] -Type=simple -User=www-data -WorkingDirectory=/opt/contact-scrape -ExecStart=/opt/contact-scrape/contact-scrape -Restart=always -RestartSec=5 -Environment=PORT=80 - -# Security settings -NoNewPrivileges=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=true -ReadWritePaths=/opt/contact-scrape/data -CapabilityBoundingSet=CAP_NET_BIND_SERVICE -AmbientCapabilities=CAP_NET_BIND_SERVICE - -# Logging -StandardOutput=journal -StandardError=journal -SyslogIdentifier=contact-scrape - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/contact-scrape-test/contacts.xlsx b/contact-scrape-test/contacts.xlsx deleted file mode 100644 index 07b950b..0000000 Binary files a/contact-scrape-test/contacts.xlsx and /dev/null differ diff --git a/contact-scrape-test/index.html b/contact-scrape-test/index.html deleted file mode 100644 index 722b6df..0000000 --- a/contact-scrape-test/index.html +++ /dev/null @@ -1,376 +0,0 @@ - - - - - - Kontakty - - - - - -
-
-

📞 Firemní telefonní seznam

-

Poppe + Potthoff kontakty

-
-
- -
-
- -
-
-

📞 Kontakty

-

Firemní telefonní seznam

-
- -
- - -
-
- - -
-
- - -
- - - -
- - -
-
-

Načítání kontaktů...

-
- - - - - - -
-
- - - - - - \ No newline at end of file