diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..49a307f --- /dev/null +++ b/build.bat @@ -0,0 +1,12 @@ +@echo off +echo Building FolderOpener executable... + +REM Create build directory if it doesn't exist +if not exist "build" mkdir build + +REM Build for Windows as a GUI application (no console window) +go build -ldflags="-H windowsgui" -o build/FolderOpener.exe main.go + +echo Done! Executable is in the build folder. +echo The application will run silently in the background. +pause diff --git a/build/FolderOpener.exe b/build/FolderOpener.exe new file mode 100644 index 0000000..bcc565f Binary files /dev/null and b/build/FolderOpener.exe differ diff --git a/go.mod b/go.mod index c6d2f62..8602d5a 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,3 @@ -module ppve +module folderopener -go 1.24.2 - -require ( - github.com/bytedance/sonic v1.13.2 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/cors v1.7.5 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.15.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +go 1.20 diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..6ccb8ab --- /dev/null +++ b/install.bat @@ -0,0 +1,29 @@ +@echo off +echo Installing FolderOpener... + +REM Create destination directory +set INSTALL_DIR=%APPDATA%\FolderOpener +if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%" + +REM Copy executable +copy /Y "build\FolderOpener.exe" "%INSTALL_DIR%" + +REM Create startup shortcut +set STARTUP_DIR=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup +echo Set oWS = WScript.CreateObject("WScript.Shell") > "%TEMP%\CreateShortcut.vbs" +echo sLinkFile = "%STARTUP_DIR%\FolderOpener.lnk" >> "%TEMP%\CreateShortcut.vbs" +echo Set oLink = oWS.CreateShortcut(sLinkFile) >> "%TEMP%\CreateShortcut.vbs" +echo oLink.TargetPath = "%INSTALL_DIR%\FolderOpener.exe" >> "%TEMP%\CreateShortcut.vbs" +echo oLink.WorkingDirectory = "%INSTALL_DIR%" >> "%TEMP%\CreateShortcut.vbs" +echo oLink.Description = "FolderOpener - Windows Folder Opener Service" >> "%TEMP%\CreateShortcut.vbs" +echo oLink.Save >> "%TEMP%\CreateShortcut.vbs" +cscript /nologo "%TEMP%\CreateShortcut.vbs" +del "%TEMP%\CreateShortcut.vbs" + +echo Installation complete! FolderOpener will start automatically when you log in. +echo Starting FolderOpener now... + +REM Start the application +start "" "%INSTALL_DIR%\FolderOpener.exe" + +pause diff --git a/main.go b/main.go index 4337618..3598d41 100644 --- a/main.go +++ b/main.go @@ -1,177 +1,104 @@ package main import ( - "crypto/tls" - "encoding/json" "fmt" - "io" "log" "net/http" "os" "os/exec" + "path/filepath" "strings" "time" - - "gopkg.in/gomail.v2" ) -type TripEntry struct { - Name string `json:"name"` - Vehicle string `json:"vehicle"` - Destination string `json:"destination"` - DateStart string `json:"date_start"` - TimeStart string `json:"time_start"` - DateEnd string `json:"date_end"` - TimeEnd string `json:"time_end"` - Purpose string `json:"purpose"` - KmStart int `json:"km_start"` - KmEnd int `json:"km_end"` - Coordinates *GeoCoords `json:"coordinates,omitempty"` -} +const ( + AppName = "FolderOpener" + AppVersion = "1.0.0" + ServerPort = "8765" +) -type GeoCoords struct { - Lat string `json:"lat"` - Lng string `json:"lng"` -} +var ( + logFile *os.File + logger *log.Logger +) func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) + // Set up logging + setupLogging() - http.HandleFunc("/submit", enableCORS(handleSubmit)) - http.HandleFunc("/health", enableCORS(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"status":"ok"}`)) - })) + // Print startup message + fmt.Printf("%s v%s starting up\n", AppName, AppVersion) - http.HandleFunc("/", enableCORS(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "index.html") - })) + // Start the HTTP server + startServer() +} - http.HandleFunc("/evidence-aut", enableCORS(func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "evidence-aut.html") - })) +func setupLogging() { + // Create logs directory if it doesn't exist + logsDir := filepath.Join(os.Getenv("APPDATA"), AppName, "logs") + os.MkdirAll(logsDir, 0755) - // Add folder opener endpoint - http.HandleFunc("/open", enableCORS(openFolderHandler)) + // Create log file with timestamp + timestamp := time.Now().Format("2006-01-02") + logFilePath := filepath.Join(logsDir, fmt.Sprintf("%s.log", timestamp)) - port := os.Getenv("PORT") - if port == "" { - port = "8080" + var err error + logFile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatalf("Failed to open log file: %v", err) } - log.Printf("Server běží na portu %s (včetně otevírání složek Windows)", port) - err := http.ListenAndServe(":"+port, nil) + // Set up logger + logger = log.New(logFile, "", log.LstdFlags) + logger.Printf("%s v%s starting up", AppName, AppVersion) +} + +func startServer() { + // Set up HTTP server + http.HandleFunc("/open", openFolderHandler) + + // Start server on the specified port + serverAddr := fmt.Sprintf(":%s", ServerPort) + logger.Printf("Folder opener server running on http://localhost%s", serverAddr) + + // Log to console as well + fmt.Printf("Folder opener server running on http://localhost%s\n", serverAddr) + + // Start the server + err := http.ListenAndServe(serverAddr, nil) if err != nil { - log.Fatalf("Chyba při spuštění serveru: %v", err) + logger.Fatalf("Server error: %v", err) } } -func enableCORS(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - if next != nil { - next(w, r) - } - } -} - -func handleSubmit(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - if r.Method != http.MethodPost { - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - w.WriteHeader(http.StatusMethodNotAllowed) - w.Write([]byte(`{"error":"Only POST method is allowed"}`)) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("Chyba při čtení těla požadavku: %v", err) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"Failed to read request body"}`)) - return - } - defer r.Body.Close() - - log.Printf("Přijatá data: %s", string(body)) - - var entry TripEntry - err = json.Unmarshal(body, &entry) - if err != nil { - log.Printf("Chyba při parsování JSON: %v", err) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf(`{"error":"Failed to parse JSON: %v"}`, err))) - return - } - - if entry.Name == "" || entry.Destination == "" || entry.DateStart == "" || entry.DateEnd == "" || entry.Purpose == "" { - log.Printf("Chybějící povinná pole: %+v", entry) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"Missing required fields"}`)) - return - } - - if entry.KmEnd < entry.KmStart { - log.Printf("Neplatný stav tachometru: %d -> %d", entry.KmStart, entry.KmEnd) - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error":"End kilometers must be greater than or equal to start kilometers"}`)) - return - } - - // Formátování dat do českého formátu - czechMonths := []string{ - "ledna", "února", "března", "dubna", "května", "června", - "července", "srpna", "září", "října", "listopadu", "prosince", - } - - // Zpracování začátku cesty - parsedDateStart, err := time.Parse("2006-01-02", entry.DateStart) - if err != nil { - log.Printf("Chyba při parsování data začátku: %v", err) - } - - // Zpracování konce cesty - parsedDateEnd, err := time.Parse("2006-01-02", entry.DateEnd) - if err != nil { - log.Printf("Chyba při parsování data konce: %v", err) - } - - err = sendEmail(entry, parsedDateStart, parsedDateEnd, czechMonths) - if err != nil { - log.Printf("Chyba při odesílání emailu: %v", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf(`{"error":"Failed to send email: %v"}`, err))) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"message":"Záznam byl úspěšně uložen a email odeslán"}`)) -} - -// Handler pro otevírání Windows složek přímo z prohlížeče func openFolderHandler(w http.ResponseWriter, r *http.Request) { + // Set CORS headers to allow requests from any origin + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + // Handle preflight OPTIONS request + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + // Only allow GET requests + if r.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + // Get the folder path from the query parameter folderPath := r.URL.Query().Get("path") if folderPath == "" { http.Error(w, "Missing path parameter", http.StatusBadRequest) return } - + // Log the request - log.Printf("Otevírání složky: %s", folderPath) - + logger.Printf("Opening folder: %s", folderPath) + // Properly handle backslashes in Windows paths // First, replace any forward slashes with backslashes folderPath = strings.ReplaceAll(folderPath, "/", "\\") @@ -182,330 +109,53 @@ func openFolderHandler(w http.ResponseWriter, r *http.Request) { } // Log the cleaned path - log.Printf("Upravená cesta: %s", folderPath) - - // Open the folder in Windows Explorer + logger.Printf("Cleaned path: %s", folderPath) + + // Try multiple methods to open the folder + successful := false + + // Method 1: Direct explorer.exe call cmd := exec.Command("explorer.exe", folderPath) err := cmd.Start() + if err == nil { + logger.Printf("Opened folder using direct method") + successful = true + } else { + logger.Printf("Error opening folder with direct method: %v", err) + } - if err != nil { - // If there was an error, try opening the parent directory - log.Printf("Chyba při otevírání složky: %v, zkouším jinou metodu", err) - - // Try using /root,path format which sometimes works better + // Method 2: Using /root parameter if Method 1 failed + if !successful { cmd = exec.Command("explorer.exe", "/root," + folderPath) err = cmd.Start() - - if err != nil { - log.Printf("Chyba při otevírání složky: %v", err) - http.Error(w, fmt.Sprintf("Error opening folder: %v", err), http.StatusInternalServerError) - return + if err == nil { + logger.Printf("Opened folder using /root parameter") + successful = true + } else { + logger.Printf("Error opening folder with /root parameter: %v", err) } } - - // Return success response + + // Method 3: Try with shell execute + if !successful { + cmd = exec.Command("cmd.exe", "/c", "start", "", folderPath) + err = cmd.Start() + if err == nil { + logger.Printf("Opened folder using shell execute") + successful = true + } else { + logger.Printf("Error opening folder with shell execute: %v", err) + } + } + + // Return appropriate response w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": fmt.Sprintf("Opening folder: %s", folderPath)}) -} - -func sendEmail(entry TripEntry, parsedDateStart, parsedDateEnd time.Time, czechMonths []string) error { - smtpHost := "mail.pp-kunovice.cz" - smtpPort := 465 - sender := "sluzebnicek@pp-kunovice.cz" - password := "7g}qznB5bj" - recipient := "sluzebnicek@pp-kunovice.cz" - - m := gomail.NewMessage() - m.SetHeader("From", sender) - m.SetHeader("To", recipient) - m.SetHeader("Subject", "Nový záznam o jízdě služebním autem") - - var htmlContent strings.Builder - - htmlContent.WriteString(` - - - - - - Záznam o jízdě služebním autem - - - -
-
-

Záznam o jízdě služebním autem

-
-
- `) - - // Formátování dat a časů pro zobrazení - formattedDateStart := "" - if parsedDateStart.IsZero() == false { - monthNameStart := czechMonths[parsedDateStart.Month()-1] - formattedDateStart = fmt.Sprintf("%d. %s %d", parsedDateStart.Day(), monthNameStart, parsedDateStart.Year()) + + if successful { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"status":"success","message":"Opening folder: %s"}`, folderPath) } else { - formattedDateStart = entry.DateStart + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{"status":"error","message":"Failed to open folder: %s"}`, folderPath) } - - formattedDateEnd := "" - if !parsedDateEnd.IsZero() { - monthNameEnd := czechMonths[parsedDateEnd.Month()-1] - formattedDateEnd = fmt.Sprintf("%d. %s %d", parsedDateEnd.Day(), monthNameEnd, parsedDateEnd.Year()) - } else { - formattedDateEnd = entry.DateEnd - } - - // Výpočet celkové doby jízdy - startDateTime, startErr := time.Parse("2006-01-02T15:04", fmt.Sprintf("%sT%s", entry.DateStart, entry.TimeStart)) - endDateTime, endErr := time.Parse("2006-01-02T15:04", fmt.Sprintf("%sT%s", entry.DateEnd, entry.TimeEnd)) - - totalDurationStr := "Neznámá" - if startErr == nil && endErr == nil { - diffMs := endDateTime.Sub(startDateTime) - if diffMs >= 0 { - diffDays := int(diffMs.Hours() / 24) - diffHours := int(diffMs.Hours()) % 24 - diffMinutes := int(diffMs.Minutes()) % 60 - - if diffDays > 0 { - dayWord := "dní" - if diffDays == 1 { - dayWord = "den" - } else if diffDays >= 2 && diffDays <= 4 { - dayWord = "dny" - } - totalDurationStr = fmt.Sprintf("%d %s, %d h %d min", diffDays, dayWord, diffHours, diffMinutes) - } else { - totalDurationStr = fmt.Sprintf("%d h %d min", diffHours, diffMinutes) - } - } - } - - // Vypsání informací o řidiči a vozidle - htmlContent.WriteString(`
-
Informace o řidiči a vozidle
-
-
- Řidič - ` + entry.Name + ` -
-
- Vozidlo - ` + entry.Vehicle + ` -
-
-
`) - - // Vypsání informací o trase - htmlContent.WriteString(`
-
Informace o trase
-
-
- Cíl cesty - ` + entry.Destination + ` -
-
- Účel jízdy - ` + entry.Purpose + ` -
-
-
`) - - // Vypsání informací o času - htmlContent.WriteString(`
-
Časové údaje
-
-
- Datum a čas odjezdu - ` + formattedDateStart + `, ` + entry.TimeStart + ` -
-
- Datum a čas příjezdu - ` + formattedDateEnd + `, ` + entry.TimeEnd + ` -
-
-
- Celková doba jízdy - ` + totalDurationStr + ` -
-
`) - - // Vypsání informací o kilometrech - htmlContent.WriteString(`
-
Stav tachometru
-
-
- Stav na začátku - ` + fmt.Sprintf("%d km", entry.KmStart) + ` -
-
- Stav na konci - ` + fmt.Sprintf("%d km", entry.KmEnd) + ` -
-
-
- Celkem ujeto - ` + fmt.Sprintf("%d km", entry.KmEnd-entry.KmStart) + ` -
-
`) - - if entry.Coordinates != nil { - htmlContent.WriteString(`
-
GPS Souřadnice
-
-
- Souřadnice - ` + entry.Coordinates.Lat + `, ` + entry.Coordinates.Lng + ` -
-
- - Zobrazit na mapě - -
`) - } - - htmlContent.WriteString(` -
- -
- - - `) - - m.SetBody("text/html", htmlContent.String()) - - d := gomail.NewDialer(smtpHost, smtpPort, sender, password) - d.TLSConfig = &tls.Config{InsecureSkipVerify: true} - - return d.DialAndSend(m) } diff --git a/start_server.bat b/start_server.bat new file mode 100644 index 0000000..32ac4a2 --- /dev/null +++ b/start_server.bat @@ -0,0 +1,4 @@ +@echo off +echo Starting Folder Opener Server... +cd %~dp0 +go run main.go