diff --git a/go.mod b/go.mod index d619014..4620f4b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module git.mypapercloud.de/fierke/logofclient-installer go 1.26.8 -require github.com/ulikunitz/xz v0.5.17 +require ( + github.com/go-ole/go-ole v1.3.0 + github.com/ulikunitz/xz v0.5.17 +) + +require golang.org/x/sys v0.1.0 // indirect diff --git a/go.sum b/go.sum index cd0bbdf..8879a97 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4= github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index c95cd87..100d053 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "io" "net/http" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -18,11 +17,11 @@ import ( ) const ( - appName = "logofclient" - appVersion = "1.0.0" - defaultGitea = "https://git.mypapercloud.de" - defaultOwner = "fierke" - defaultRepo = "logofclient" + appName = "logofclient" + appVersion = "1.0.0" + defaultGitea = "https://git.mypapercloud.de" + defaultOwner = "fierke" + defaultRepo = "logofclient" ) type Config struct { @@ -74,33 +73,15 @@ func main() { os.Exit(1) } - case "-v", "--version": - fmt.Printf("%s %s\n", appName, appVersion) - - case "-h", "--help", "help": - printHelp() - case "-y": - if len(args) < 2 { + if len(args) != 2 { printHelp() os.Exit(1) } switch args[1] { - case "install": - if err := runPrivileged(config, "install"); err != nil { - printError(err) - os.Exit(1) - } - - case "update": - if err := runPrivileged(config, "update"); err != nil { - printError(err) - os.Exit(1) - } - - case "uninstall": - if err := runPrivileged(config, "uninstall"); err != nil { + case "install", "update", "uninstall": + if err := runPrivileged(config, args[1]); err != nil { printError(err) os.Exit(1) } @@ -110,6 +91,12 @@ func main() { os.Exit(1) } + case "-v", "--version": + fmt.Printf("%s %s\n", appName, appVersion) + + case "-h", "--help", "help": + printHelp() + default: fmt.Printf("Unbekannter Befehl: %s\n\n", args[0]) printHelp() @@ -137,14 +124,14 @@ func runTUI(config Config) { fmt.Print("Auswahl: ") var choice string + if _, err := fmt.Scanln(&choice); err != nil { - fmt.Println() return } var err error - switch choice { + switch strings.TrimSpace(choice) { case "1": err = runPrivileged(config, "install") @@ -184,7 +171,7 @@ Verwendung: Optionen: -h, --help Diese Hilfe anzeigen -v, --version Version des Installers anzeigen - -y Befehl ohne TUI ausführen + -y Befehl ohne interaktive Abfrage ausführen Beispiele: logofclient-installer install @@ -193,60 +180,37 @@ Beispiele: logofclient-installer -y install`) } -// ------------------------------------------------------------ -// Privileges -// ------------------------------------------------------------ - -func runPrivileged(config Config, action string) error { - // Windows: - // Die EXE wird später über ein Manifest mit - // requestedExecutionLevel="requireAdministrator" - // gestartet. Hier ist daher keine Elevation nötig. - if runtime.GOOS != "linux" { - return runAction(config, action) - } - - // Linux: bereits root - if os.Geteuid() == 0 { - return runAction(config, action) - } - - // Linux: mit sudo neu starten - fmt.Println() - fmt.Println("Für diese Aktion werden Administratorrechte benötigt.") - fmt.Println("Starte Installer mit sudo...") - fmt.Println() - - exe, err := os.Executable() - if err != nil { - return fmt.Errorf("Installer-Pfad konnte nicht ermittelt werden: %w", err) - } - - cmd := exec.Command("sudo", exe, action) - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - return cmd.Run() -} - func runAction(config Config, action string) error { switch action { case "install": return install(config) - case "update": return update(config) - case "uninstall": return uninstall(config) - default: return fmt.Errorf("unbekannte Aktion: %s", action) } } +// ------------------------------------------------------------ +// Privilegien +// ------------------------------------------------------------ + +// runPrivileged wird von den Plattformdateien implementiert. +// +// Linux: +// - fragt nach Bestätigung +// - startet sich bei Bedarf über sudo neu +// +// Windows: +// - die EXE wird über das Manifest mit +// requireAdministrator gestartet +// - hier ist keine weitere Elevation nötig +func runPrivileged(config Config, action string) error { + return platformRunPrivileged(config, action) +} + // ------------------------------------------------------------ // Installation // ------------------------------------------------------------ @@ -260,6 +224,7 @@ func install(config Config) error { } tag := release.TagName + if tag == "" { return errors.New("Release enthält keinen Tag") } @@ -268,6 +233,13 @@ func install(config Config) error { assetName := getAssetName(tag) + if assetName == "" { + return fmt.Errorf( + "kein Paket für Betriebssystem %s definiert", + runtime.GOOS, + ) + } + asset, err := findAsset(release, assetName) if err != nil { return err @@ -277,7 +249,10 @@ func install(config Config) error { tempDir, err := os.MkdirTemp("", "logofclient-installer-*") if err != nil { - return fmt.Errorf("temporäres Verzeichnis konnte nicht erstellt werden: %w", err) + return fmt.Errorf( + "temporäres Verzeichnis konnte nicht erstellt werden: %w", + err, + ) } defer os.RemoveAll(tempDir) @@ -294,16 +269,20 @@ func install(config Config) error { fmt.Printf("Installing to: %s\n", installDir) - // Alte Installation entfernen if err := removeIfExists(installDir); err != nil { - return fmt.Errorf("alte Installation konnte nicht entfernt werden: %w", err) + return fmt.Errorf( + "alte Installation konnte nicht entfernt werden: %w", + err, + ) } if err := os.MkdirAll(installDir, 0755); err != nil { - return fmt.Errorf("Installationsverzeichnis konnte nicht erstellt werden: %w", err) + return fmt.Errorf( + "Installationsverzeichnis konnte nicht erstellt werden: %w", + err, + ) } - // Archiv entpacken switch runtime.GOOS { case "linux": if err := extractTarXz(archivePath, installDir); err != nil { @@ -316,10 +295,12 @@ func install(config Config) error { } default: - return fmt.Errorf("nicht unterstütztes Betriebssystem: %s", runtime.GOOS) + return fmt.Errorf( + "nicht unterstütztes Betriebssystem: %s", + runtime.GOOS, + ) } - // installierte Version speichern versionFile := filepath.Join(installDir, ".version") if err := os.WriteFile( @@ -327,24 +308,22 @@ func install(config Config) error { []byte(tag+"\n"), 0644, ); err != nil { - return fmt.Errorf("Version konnte nicht gespeichert werden: %w", err) + return fmt.Errorf( + "Version konnte nicht gespeichert werden: %w", + err, + ) } - // Plattformintegration - switch runtime.GOOS { - case "linux": - if err := installLinuxIntegration(installDir); err != nil { - return err - } - - case "windows": - if err := installWindowsIntegration(installDir); err != nil { - return err - } + if err := platformInstallIntegration(installDir); err != nil { + return err } fmt.Println() - fmt.Printf("%s %s wurde erfolgreich installiert.\n", appName, tag) + fmt.Printf( + "%s %s wurde erfolgreich installiert.\n", + appName, + tag, + ) return nil } @@ -369,7 +348,10 @@ func update(config Config) error { return install(config) } - return fmt.Errorf("installierte Version konnte nicht gelesen werden: %w", err) + return fmt.Errorf( + "installierte Version konnte nicht gelesen werden: %w", + err, + ) } currentVersion := strings.TrimSpace(string(currentVersionData)) @@ -383,8 +365,15 @@ func update(config Config) error { latestVersion := release.TagName - fmt.Printf("Installierte Version: %s\n", valueOrNone(currentVersion)) - fmt.Printf("Neueste Version: %s\n", valueOrNone(latestVersion)) + fmt.Printf( + "Installierte Version: %s\n", + valueOrNone(currentVersion), + ) + + fmt.Printf( + "Neueste Version: %s\n", + valueOrNone(latestVersion), + ) if currentVersion == latestVersion { fmt.Println() @@ -393,18 +382,21 @@ func update(config Config) error { } fmt.Println() - fmt.Printf("Update von %s auf %s...\n", currentVersion, latestVersion) + fmt.Printf( + "Update von %s auf %s...\n", + currentVersion, + latestVersion, + ) - // install() entfernt die alte Version selbst. return install(config) } // ------------------------------------------------------------ -// Uninstall +// Deinstallation // ------------------------------------------------------------ func uninstall(config Config) error { - _ = config // aktuell nicht benötigt + _ = config installDir, err := getInstallDir() if err != nil { @@ -418,20 +410,15 @@ func uninstall(config Config) error { fmt.Printf("Deinstalliere %s...\n", appName) - switch runtime.GOOS { - case "linux": - if err := removeLinuxIntegration(); err != nil { - return err - } - - case "windows": - if err := removeWindowsIntegration(); err != nil { - return err - } + if err := platformRemoveIntegration(); err != nil { + return err } if err := removeIfExists(installDir); err != nil { - return fmt.Errorf("Installation konnte nicht entfernt werden: %w", err) + return fmt.Errorf( + "Installation konnte nicht entfernt werden: %w", + err, + ) } fmt.Println("Logof Client wurde erfolgreich deinstalliert.") @@ -453,12 +440,17 @@ func getLatestRelease(config Config) (*Release, error) { resp, err := http.Get(url) if err != nil { - return nil, fmt.Errorf("Gitea Release konnte nicht abgerufen werden: %w", err) + return nil, fmt.Errorf( + "Gitea Release konnte nicht abgerufen werden: %w", + err, + ) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + body, _ := io.ReadAll( + io.LimitReader(resp.Body, 4096), + ) return nil, fmt.Errorf( "Gitea API antwortete mit HTTP %d: %s", @@ -470,7 +462,10 @@ func getLatestRelease(config Config) (*Release, error) { var release Release if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { - return nil, fmt.Errorf("Gitea-Antwort konnte nicht gelesen werden: %w", err) + return nil, fmt.Errorf( + "Gitea-Antwort konnte nicht gelesen werden: %w", + err, + ) } return &release, nil @@ -493,10 +488,18 @@ func findAsset(release *Release, name string) (*Asset, error) { func getAssetName(tag string) string { switch runtime.GOOS { case "linux": - return fmt.Sprintf("%s-%s-linux.tar.xz", appName, tag) + return fmt.Sprintf( + "%s-%s-linux.tar.xz", + appName, + tag, + ) case "windows": - return fmt.Sprintf("%s-%s-win.zip", appName, tag) + return fmt.Sprintf( + "%s-%s-win.zip", + appName, + tag, + ) default: return "" @@ -506,7 +509,10 @@ func getAssetName(tag string) string { func download(url string, destination string) error { resp, err := http.Get(url) if err != nil { - return fmt.Errorf("Download fehlgeschlagen: %w", err) + return fmt.Errorf( + "Download fehlgeschlagen: %w", + err, + ) } defer resp.Body.Close() @@ -519,84 +525,16 @@ func download(url string, destination string) error { file, err := os.Create(destination) if err != nil { - return fmt.Errorf("Download-Datei konnte nicht erstellt werden: %w", err) + return fmt.Errorf( + "Download-Datei konnte nicht erstellt werden: %w", + err, + ) } defer file.Close() if _, err := io.Copy(file, resp.Body); err != nil { - return fmt.Errorf("Download fehlgeschlagen: %w", err) - } - - return nil -} - -// ------------------------------------------------------------ -// Linux -// ------------------------------------------------------------ - -func getInstallDir() (string, error) { - switch runtime.GOOS { - case "linux": - return "/opt/logofclient", nil - - case "windows": - programFiles := os.Getenv("ProgramFiles") - - if programFiles == "" { - return "", errors.New("ProgramFiles ist nicht gesetzt") - } - - return filepath.Join(programFiles, "logofclient"), nil - - default: - return "", fmt.Errorf("nicht unterstütztes Betriebssystem: %s", runtime.GOOS) - } -} - -func installLinuxIntegration(installDir string) error { - binaryPath := filepath.Join(installDir, "Logof Client") - linkPath := "/usr/local/bin/logofclient" - - fmt.Println("Creating command symlink...") - - if err := removeIfExists(linkPath); err != nil { - return fmt.Errorf("alter Symlink konnte nicht entfernt werden: %w", err) - } - - if err := os.Symlink(binaryPath, linkPath); err != nil { - return fmt.Errorf("Symlink konnte nicht erstellt werden: %w", err) - } - - desktopDir := "/usr/share/applications" - desktopFile := filepath.Join(desktopDir, "logofclient.desktop") - - if err := os.MkdirAll(desktopDir, 0755); err != nil { - return fmt.Errorf("Desktop-Verzeichnis konnte nicht erstellt werden: %w", err) - } - - iconPath := filepath.Join( - installDir, - "assets", - "icon.png", - ) - - desktopEntry := fmt.Sprintf(`[Desktop Entry] -Name=Logof Client -Comment=Logof Client -Exec=%s -Terminal=false -Type=Application -Icon=%s -Categories=Office; -`, linkPath, iconPath) - - if err := os.WriteFile( - desktopFile, - []byte(desktopEntry), - 0644, - ); err != nil { return fmt.Errorf( - "Desktop-Datei konnte nicht geschrieben werden: %w", + "Download fehlgeschlagen: %w", err, ) } @@ -604,135 +542,30 @@ Categories=Office; return nil } -func removeLinuxIntegration() error { - linkPath := "/usr/local/bin/logofclient" - desktopFile := "/usr/share/applications/logofclient.desktop" - - if err := removeIfExists(linkPath); err != nil { - return fmt.Errorf("Symlink konnte nicht entfernt werden: %w", err) - } - - if err := removeIfExists(desktopFile); err != nil { - return fmt.Errorf("Desktop-Datei konnte nicht entfernt werden: %w", err) - } - - return nil -} // ------------------------------------------------------------ -// Windows +// Archive // ------------------------------------------------------------ -func installWindowsIntegration(installDir string) error { - startMenu := filepath.Join( - os.Getenv("ProgramData"), - "Microsoft", - "Windows", - "Start Menu", - "Programs", - ) - - if err := os.MkdirAll(startMenu, 0755); err != nil { - return fmt.Errorf( - "Startmenü-Verzeichnis konnte nicht erstellt werden: %w", - err, - ) - } - - shortcut := filepath.Join( - startMenu, - "Logof Client.lnk", - ) - - target := filepath.Join( - installDir, - "Logof Client.exe", - ) - - icon := filepath.Join( - installDir, - "assets", - "icon.ico", - ) - - // PowerShell wird hier nur intern zur Erstellung der Windows- - // Shell-Verknüpfung verwendet. Der Benutzer muss kein - // PowerShell-Skript ausführen. - script := ` -$ws = New-Object -ComObject WScript.Shell -$sc = $ws.CreateShortcut($args[0]) -$sc.TargetPath = $args[1] -$sc.WorkingDirectory = $args[2] -$sc.Description = "Logof Client" -if (Test-Path $args[3]) { - $sc.IconLocation = $args[3] -} -$sc.Save() -` - - cmd := exec.Command( - "powershell.exe", - "-NoProfile", - "-NonInteractive", - "-Command", - script, - shortcut, - target, - installDir, - icon, - ) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - return fmt.Errorf( - "Startmenü-Verknüpfung konnte nicht erstellt werden: %w", - err, - ) - } - - return nil -} - -func removeWindowsIntegration() error { - startMenu := filepath.Join( - os.Getenv("ProgramData"), - "Microsoft", - "Windows", - "Start Menu", - "Programs", - ) - - shortcut := filepath.Join( - startMenu, - "Logof Client.lnk", - ) - - if err := removeIfExists(shortcut); err != nil { - return fmt.Errorf( - "Startmenü-Verknüpfung konnte nicht entfernt werden: %w", - err, - ) - } - - return nil -} - -// ------------------------------------------------------------ -// Archive extraction -// ------------------------------------------------------------ - -func extractTarXz(archivePath string, destination string) error { +func extractTarXz( + archivePath string, + destination string, +) error { file, err := os.Open(archivePath) if err != nil { - return fmt.Errorf("Archiv konnte nicht geöffnet werden: %w", err) + return fmt.Errorf( + "Archiv konnte nicht geöffnet werden: %w", + err, + ) } defer file.Close() xzReader, err := xz.NewReader(file) if err != nil { - return fmt.Errorf("XZ-Archiv konnte nicht gelesen werden: %w", err) + return fmt.Errorf( + "XZ-Archiv konnte nicht gelesen werden: %w", + err, + ) } tarReader := tar.NewReader(xzReader) @@ -745,17 +578,26 @@ func extractTarXz(archivePath string, destination string) error { } if err != nil { - return fmt.Errorf("TAR-Archiv konnte nicht gelesen werden: %w", err) + return fmt.Errorf( + "TAR-Archiv konnte nicht gelesen werden: %w", + err, + ) } - target, err := safeJoin(destination, header.Name) + target, err := safeJoin( + destination, + header.Name, + ) if err != nil { return err } switch header.Typeflag { case tar.TypeDir: - if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { + if err := os.MkdirAll( + target, + os.FileMode(header.Mode), + ); err != nil { return fmt.Errorf( "Verzeichnis %s konnte nicht erstellt werden: %w", target, @@ -764,7 +606,10 @@ func extractTarXz(archivePath string, destination string) error { } case tar.TypeReg: - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + if err := os.MkdirAll( + filepath.Dir(target), + 0755, + ); err != nil { return err } @@ -781,7 +626,11 @@ func extractTarXz(archivePath string, destination string) error { ) } - _, copyErr := io.Copy(out, tarReader) + _, copyErr := io.Copy( + out, + tarReader, + ) + closeErr := out.Close() if copyErr != nil { @@ -797,7 +646,10 @@ func extractTarXz(archivePath string, destination string) error { } case tar.TypeSymlink: - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + if err := os.MkdirAll( + filepath.Dir(target), + 0755, + ); err != nil { return err } @@ -805,9 +657,10 @@ func extractTarXz(archivePath string, destination string) error { return err } - linkTarget := header.Linkname - - if err := os.Symlink(linkTarget, target); err != nil { + if err := os.Symlink( + header.Linkname, + target, + ); err != nil { return fmt.Errorf( "Symlink %s konnte nicht erstellt werden: %w", target, @@ -826,21 +679,33 @@ func extractTarXz(archivePath string, destination string) error { return nil } -func extractZip(archivePath string, destination string) error { +func extractZip( + archivePath string, + destination string, +) error { reader, err := zip.OpenReader(archivePath) if err != nil { - return fmt.Errorf("ZIP-Archiv konnte nicht geöffnet werden: %w", err) + return fmt.Errorf( + "ZIP-Archiv konnte nicht geöffnet werden: %w", + err, + ) } defer reader.Close() for _, file := range reader.File { - target, err := safeJoin(destination, file.Name) + target, err := safeJoin( + destination, + file.Name, + ) if err != nil { return err } if file.FileInfo().IsDir() { - if err := os.MkdirAll(target, 0755); err != nil { + if err := os.MkdirAll( + target, + 0755, + ); err != nil { return fmt.Errorf( "Verzeichnis %s konnte nicht erstellt werden: %w", target, @@ -851,7 +716,10 @@ func extractZip(archivePath string, destination string) error { continue } - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + if err := os.MkdirAll( + filepath.Dir(target), + 0755, + ); err != nil { return err } @@ -901,10 +769,13 @@ func extractZip(archivePath string, destination string) error { } // ------------------------------------------------------------ -// Helpers +// Hilfsfunktionen // ------------------------------------------------------------ -func safeJoin(base string, name string) (string, error) { +func safeJoin( + base string, + name string, +) (string, error) { if filepath.IsAbs(name) { return "", fmt.Errorf( "unsicherer Archivpfad: %s", @@ -915,22 +786,34 @@ func safeJoin(base string, name string) (string, error) { cleanName := filepath.Clean(name) if cleanName == ".." || - strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) { + strings.HasPrefix( + cleanName, + ".."+string(os.PathSeparator), + ) { return "", fmt.Errorf( "unsicherer Archivpfad: %s", name, ) } - target := filepath.Join(base, cleanName) + target := filepath.Join( + base, + cleanName, + ) - rel, err := filepath.Rel(base, target) + rel, err := filepath.Rel( + base, + target, + ) if err != nil { return "", err } if rel == ".." || - strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + strings.HasPrefix( + rel, + ".."+string(os.PathSeparator), + ) { return "", fmt.Errorf( "unsicherer Archivpfad: %s", name, @@ -950,8 +833,13 @@ func removeIfExists(path string) error { return nil } -func envOrDefault(name string, fallback string) string { - value := strings.TrimSpace(os.Getenv(name)) +func envOrDefault( + name string, + fallback string, +) string { + value := strings.TrimSpace( + os.Getenv(name), + ) if value == "" { return fallback @@ -970,4 +858,4 @@ func valueOrNone(value string) string { func printError(err error) { fmt.Printf("\nERROR: %v\n", err) -} \ No newline at end of file +} diff --git a/platform_linux.go b/platform_linux.go new file mode 100644 index 0000000..f416c3b --- /dev/null +++ b/platform_linux.go @@ -0,0 +1,67 @@ +//go:build linux + +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func getInstallDir() (string, error) { + return "/opt/logofclient", nil +} + +func platformRunPrivileged(config Config, action string) error { + // Bereits als root gestartet. + if os.Geteuid() == 0 { + return runAction(config, action) + } + + fmt.Println() + fmt.Println("Für diese Aktion werden Administratorrechte benötigt.") + fmt.Print("Jetzt Administratorrechte anfordern? [J/n]: ") + + var answer string + + if _, err := fmt.Scanln(&answer); err != nil { + // Ein leeres Enter kann bei Scanln als Fehler erscheinen. + // Das behandeln wir als Zustimmung. + answer = "" + } + + answer = strings.ToLower(strings.TrimSpace(answer)) + + if answer != "" && answer != "j" && answer != "ja" { + fmt.Println("Abgebrochen.") + return nil + } + + exe, err := os.Executable() + if err != nil { + return fmt.Errorf( + "Pfad des Installers konnte nicht ermittelt werden: %w", + err, + ) + } + + cmd := exec.Command( + "sudo", + exe, + action, + ) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf( + "Ausführung mit Administratorrechten fehlgeschlagen: %w", + err, + ) + } + + return nil +} \ No newline at end of file diff --git a/platform_windows.go b/platform_windows.go new file mode 100644 index 0000000..5a9ed77 --- /dev/null +++ b/platform_windows.go @@ -0,0 +1,130 @@ +//go:build windows + +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + shell32 = syscall.NewLazyDLL("shell32.dll") + shellExecuteExW = shell32.NewProc("ShellExecuteExW") +) + +const ( + seeMaskNocloseprocess = 0x00000040 +) + +type shellExecuteInfo struct { + cbSize uint32 + fMask uint32 + hwnd uintptr + lpVerb *uint16 + lpFile *uint16 + lpParameters *uint16 + lpDirectory *uint16 + nShow int32 + hInstApp uintptr + lpIDList uintptr + lpClass *uint16 + hkeyClass uintptr + dwHotKey uint32 + hIcon uintptr + hProcess uintptr +} + +func getInstallDir() (string, error) { + programFiles := os.Getenv("ProgramFiles") + + if programFiles == "" { + return "", errors.New("ProgramFiles ist nicht gesetzt") + } + + return filepath.Join(programFiles, "logofclient"), nil +} + +func platformRunPrivileged(config Config, action string) error { + if isWindowsAdmin() { + return runAction(config, action) + } + + return runElevated(action) +} + +func isWindowsAdmin() bool { + // Ein einfacher und robuster Test: + // Versuch, in das Windows-Systemverzeichnis zu schreiben. + // + // Da wir dort natürlich nichts verändern wollen, verwenden wir + // stattdessen "net session". Der Befehl funktioniert nur mit + // Administratorrechten. + + cmd := exec.Command("net", "session") + cmd.Stdout = nil + cmd.Stderr = nil + + return cmd.Run() == nil +} + +func runElevated(action string) error { + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("Pfad des Installers konnte nicht ermittelt werden: %w", err) + } + + verb, err := syscall.UTF16PtrFromString("runas") + if err != nil { + return err + } + + file, err := syscall.UTF16PtrFromString(exe) + if err != nil { + return err + } + + parameters, err := syscall.UTF16PtrFromString(action) + if err != nil { + return err + } + + info := shellExecuteInfo{ + cbSize: uint32(unsafe.Sizeof(shellExecuteInfo{})), + fMask: seeMaskNocloseprocess, + lpVerb: verb, + lpFile: file, + lpParameters: parameters, + nShow: 1, // SW_SHOWNORMAL + } + + ret, _, callErr := shellExecuteExW.Call(uintptr(unsafe.Pointer(&info))) + + if ret == 0 { + // ERROR_CANCELLED = 1223: + // Benutzer hat den UAC-Dialog abgebrochen. + if errno, ok := callErr.(syscall.Errno); ok && errno == 1223 { + fmt.Println("Administratorrechte wurden nicht erteilt.") + return nil + } + + return fmt.Errorf("Administratorrechte konnten nicht angefordert werden: %w", callErr) + } + + // Auf die erhöhte Instanz warten. + if info.hProcess != 0 { + handle := syscall.Handle(info.hProcess) + defer syscall.CloseHandle(handle) + + _, err := syscall.WaitForSingleObject(handle, syscall.INFINITE) + if err != nil { + return fmt.Errorf("auf erhöhte Instanz konnte nicht gewartet werden: %w", err) + } + } + + return nil +} \ No newline at end of file diff --git a/shortcut_linux.go b/shortcut_linux.go new file mode 100644 index 0000000..0cb4d14 --- /dev/null +++ b/shortcut_linux.go @@ -0,0 +1,104 @@ +//go:build linux + +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +func platformInstallIntegration(installDir string) error { + binaryPath := filepath.Join( + installDir, + "Logof Client", + ) + + linkPath := "/usr/local/bin/logofclient" + + fmt.Println("Creating command symlink...") + + if err := removeIfExists(linkPath); err != nil { + return fmt.Errorf( + "alter Symlink konnte nicht entfernt werden: %w", + err, + ) + } + + if err := os.Symlink(binaryPath, linkPath); err != nil { + return fmt.Errorf( + "Symlink konnte nicht erstellt werden: %w", + err, + ) + } + + desktopDir := "/usr/share/applications" + desktopFile := filepath.Join( + desktopDir, + "logofclient.desktop", + ) + + if err := os.MkdirAll( + desktopDir, + 0755, + ); err != nil { + return fmt.Errorf( + "Desktop-Verzeichnis konnte nicht erstellt werden: %w", + err, + ) + } + + iconPath := filepath.Join( + installDir, + "assets", + "icon.png", + ) + + desktopEntry := fmt.Sprintf(`[Desktop Entry] +Name=Logof Client +Comment=Logof Client +Exec=%s +Terminal=false +Type=Application +Icon=%s +Categories=Office; +`, linkPath, iconPath) + + if err := os.WriteFile( + desktopFile, + []byte(desktopEntry), + 0644, + ); err != nil { + return fmt.Errorf( + "Desktop-Datei konnte nicht geschrieben werden: %w", + err, + ) + } + + return nil +} + +func platformRemoveIntegration() error { + linkPath := "/usr/local/bin/logofclient" + + desktopFile := filepath.Join( + "/usr/share/applications", + "logofclient.desktop", + ) + + if err := removeIfExists(linkPath); err != nil { + return fmt.Errorf( + "Symlink konnte nicht entfernt werden: %w", + err, + ) + } + + if err := removeIfExists(desktopFile); err != nil { + return fmt.Errorf( + "Desktop-Datei konnte nicht entfernt werden: %w", + err, + ) + } + + return nil +} \ No newline at end of file diff --git a/shortcut_windows.go b/shortcut_windows.go new file mode 100644 index 0000000..cdfa7a6 --- /dev/null +++ b/shortcut_windows.go @@ -0,0 +1,135 @@ +//go:build windows + +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole/oleutil" +) + +func platformInstallIntegration(installDir string) error { + targetPath := filepath.Join(installDir, "Logof Client.exe") + workingDir := installDir + iconPath := filepath.Join(installDir, "assets", "icon.ico") + + startMenuDir := filepath.Join( + os.Getenv("ProgramData"), + "Microsoft", + "Windows", + "Start Menu", + "Programs", + ) + + shortcutPath := filepath.Join( + startMenuDir, + "Logof Client.lnk", + ) + + if err := os.MkdirAll(startMenuDir, 0755); err != nil { + return fmt.Errorf("Startmenü-Verzeichnis konnte nicht erstellt werden: %w", err) + } + + fmt.Println("Creating Start Menu shortcut...") + + if err := platformCreateShortcut( + shortcutPath, + targetPath, + workingDir, + iconPath, + ); err != nil { + return err + } + + return nil +} + +func platformRemoveIntegration() error { + startMenuDir := filepath.Join( + os.Getenv("ProgramData"), + "Microsoft", + "Windows", + "Start Menu", + "Programs", + ) + + shortcutPath := filepath.Join( + startMenuDir, + "Logof Client.lnk", + ) + + if err := os.Remove(shortcutPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("Startmenü-Verknüpfung konnte nicht entfernt werden: %w", err) + } + + return nil +} + +func platformCreateShortcut( + shortcutPath string, + targetPath string, + workingDir string, + iconPath string, +) error { + // COM für den aktuellen Thread initialisieren. + if err := ole.CoInitialize(0); err != nil { + return fmt.Errorf("COM konnte nicht initialisiert werden: %w", err) + } + defer ole.CoUninitialize() + + // Windows Scripting Host. + unknown, err := oleutil.CreateObject("WScript.Shell") + if err != nil { + return fmt.Errorf("WScript.Shell konnte nicht erstellt werden: %w", err) + } + defer unknown.Release() + + shell, err := unknown.QueryInterface(ole.IID_IDispatch) + if err != nil { + return fmt.Errorf("WScript.Shell IDispatch konnte nicht abgerufen werden: %w", err) + } + defer shell.Release() + + // WScript.Shell.CreateShortcut(...) + result, err := oleutil.CallMethod(shell, "CreateShortcut", shortcutPath) + if err != nil { + return fmt.Errorf("Shortcut-Objekt konnte nicht erstellt werden: %w", err) + } + defer result.Clear() + + shortcut := result.ToIDispatch() + if shortcut == nil { + return fmt.Errorf("ungültiges Shortcut-Objekt erhalten") + } + defer shortcut.Release() + + // Zielprogramm. + if _, err := oleutil.PutProperty(shortcut, "TargetPath", targetPath); err != nil { + return fmt.Errorf("TargetPath konnte nicht gesetzt werden: %w", err) + } + + // Arbeitsverzeichnis. + if _, err := oleutil.PutProperty(shortcut, "WorkingDirectory", workingDir); err != nil { + return fmt.Errorf("WorkingDirectory konnte nicht gesetzt werden: %w", err) + } + + // Beschreibung. + if _, err := oleutil.PutProperty(shortcut, "Description", "Logof Client"); err != nil { + return fmt.Errorf("Description konnte nicht gesetzt werden: %w", err) + } + + // Icon. + if _, err := oleutil.PutProperty(shortcut, "IconLocation", iconPath); err != nil { + return fmt.Errorf("IconLocation konnte nicht gesetzt werden: %w", err) + } + + // Verknüpfung tatsächlich auf die Festplatte schreiben. + if _, err := oleutil.CallMethod(shortcut, "Save"); err != nil { + return fmt.Errorf("Shortcut konnte nicht gespeichert werden: %w", err) + } + + return nil +} \ No newline at end of file