Compare commits

...
5 Commits
9 changed files with 732 additions and 330 deletions
Binary file not shown.
Binary file not shown.
+6 -1
View File
@@ -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
+4
View File
@@ -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=
+226 -329
View File
@@ -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 {
@@ -48,8 +47,17 @@ func main() {
Repo: envOrDefault("GITEA_REPO", defaultRepo),
}
if err := platformEnsureElevated(); err != nil {
printError(err)
os.Exit(1)
}
args := os.Args[1:]
if len(args) > 0 && args[0] == "--elevated" {
args = args[1:]
}
if len(args) == 0 {
runTUI(config)
return
@@ -74,33 +82,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 +100,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 +133,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 +180,7 @@ Verwendung:
Optionen:
-h, --help Diese Hilfe anzeigen
-v, --version Version des Installers anzeigen
-y <command> Befehl ohne TUI ausführen
-y <Befehl> Befehl ohne interaktive Abfrage ausführen
Beispiele:
logofclient-installer install
@@ -193,60 +189,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 +233,7 @@ func install(config Config) error {
}
tag := release.TagName
if tag == "" {
return errors.New("Release enthält keinen Tag")
}
@@ -268,6 +242,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 +258,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 +278,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 +304,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 +317,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 +357,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 +374,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 +391,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 +419,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 +449,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 +471,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 +497,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 +518,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 +534,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 +551,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 +587,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 +615,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 +635,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 +655,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 +666,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 +688,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 +725,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 +778,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 +795,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 +842,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 +867,4 @@ func valueOrNone(value string) string {
func printError(err error) {
fmt.Printf("\nERROR: %v\n", err)
}
}
+71
View File
@@ -0,0 +1,71 @@
//go:build linux
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
func getInstallDir() (string, error) {
return "/opt/logofclient", nil
}
func platformEnsureElevated() error {
return 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
}
+186
View File
@@ -0,0 +1,186 @@
//go:build windows
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
)
var (
shell32 = syscall.NewLazyDLL("shell32.dll")
shellExecuteExW = shell32.NewProc("ShellExecuteExW")
advapi32 = syscall.NewLazyDLL("advapi32.dll")
openProcessToken = advapi32.NewProc("OpenProcessToken")
getTokenInformation = advapi32.NewProc("GetTokenInformation")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
closeHandle = kernel32.NewProc("CloseHandle")
)
const (
tokenQuery = 0x0008
tokenElevationClass = 20
errorCancelled = 1223
swShow = 1
)
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
}
type tokenElevationData struct {
TokenIsElevated uint32
}
func platformEnsureElevated() error {
// Die erhöhte Instanz wurde bereits gestartet.
// Sie darf sich nicht noch einmal selbst per runas starten.
if hasElevatedFlag() {
return nil
}
if isWindowsElevated() {
return nil
}
return restartElevated()
}
func hasElevatedFlag() bool {
for _, arg := range os.Args[1:] {
if arg == "--elevated" {
return true
}
}
return false
}
func restartElevated() 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
}
// Die erhöhte Instanz bekommt nur das interne Flag.
parameters, err := syscall.UTF16PtrFromString("--elevated")
if err != nil {
return err
}
info := shellExecuteInfo{
cbSize: uint32(unsafe.Sizeof(shellExecuteInfo{})),
lpVerb: verb,
lpFile: file,
lpParameters: parameters,
nShow: swShow,
}
ret, _, callErr := shellExecuteExW.Call(
uintptr(unsafe.Pointer(&info)),
)
if ret == 0 {
if errno, ok := callErr.(syscall.Errno); ok &&
errno == errorCancelled {
fmt.Println("Administratorrechte wurden nicht erteilt.")
return errors.New("Administratorrechte erforderlich")
}
return fmt.Errorf(
"Administratorrechte konnten nicht angefordert werden: %w",
callErr,
)
}
// WICHTIG:
// Die ursprüngliche Instanz beendet sich sofort.
// Nur die erhöhte Instanz führt danach main() weiter aus.
os.Exit(0)
return nil
}
func isWindowsElevated() bool {
var token uintptr
// GetCurrentProcess() = -1
ret, _, _ := openProcessToken.Call(
uintptr(^uint(0)),
tokenQuery,
uintptr(unsafe.Pointer(&token)),
)
if ret == 0 || token == 0 {
return false
}
defer closeHandle.Call(token)
var elevation tokenElevationData
var returned uint32
ret, _, _ = getTokenInformation.Call(
token,
tokenElevationClass,
uintptr(unsafe.Pointer(&elevation)),
uintptr(unsafe.Sizeof(elevation)),
uintptr(unsafe.Pointer(&returned)),
)
if ret == 0 {
return false
}
return elevation.TokenIsElevated != 0
}
func platformRunPrivileged(config Config, action string) error {
// Die komplette Windows-Anwendung läuft bereits erhöht.
return runAction(config, action)
}
func getInstallDir() (string, error) {
programFiles := os.Getenv("ProgramFiles")
if programFiles == "" {
return "", errors.New("ProgramFiles ist nicht gesetzt")
}
return filepath.Join(programFiles, "logofclient"), nil
}
+104
View File
@@ -0,0 +1,104 @@
//go:build linux
package main
import (
"fmt"
"os"
"path/filepath"
)
func platformInstallIntegration(installDir string) error {
binaryPath := filepath.Join(
installDir,
"Logofclient",
)
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=Logofclient
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
}
+135
View File
@@ -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,
"Logofclient.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,
"Logofclient.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", "Logofclient"); 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
}