ported installer to go
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
module git.mypapercloud.de/fierke/logofclient-installer
|
||||||
|
|
||||||
|
go 1.26.8
|
||||||
|
|
||||||
|
require github.com/ulikunitz/xz v0.5.17
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=
|
||||||
|
github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||||
@@ -0,0 +1,973 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ulikunitz/xz"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
appName = "logofclient"
|
||||||
|
appVersion = "1.0.0"
|
||||||
|
defaultGitea = "https://git.mypapercloud.de"
|
||||||
|
defaultOwner = "fierke"
|
||||||
|
defaultRepo = "logofclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
GiteaURL string
|
||||||
|
Owner string
|
||||||
|
Repo string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Assets []Asset `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Asset struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
BrowserDownloadURL string `json:"browser_download_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := Config{
|
||||||
|
GiteaURL: envOrDefault("GITEA_URL", defaultGitea),
|
||||||
|
Owner: envOrDefault("GITEA_OWNER", defaultOwner),
|
||||||
|
Repo: envOrDefault("GITEA_REPO", defaultRepo),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
runTUI(config)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
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 {
|
||||||
|
printError(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "-v", "--version":
|
||||||
|
fmt.Printf("%s %s\n", appName, appVersion)
|
||||||
|
|
||||||
|
case "-h", "--help", "help":
|
||||||
|
printHelp()
|
||||||
|
|
||||||
|
case "-y":
|
||||||
|
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 {
|
||||||
|
printError(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
printHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("Unbekannter Befehl: %s\n\n", args[0])
|
||||||
|
printHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// TUI
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func runTUI(config Config) {
|
||||||
|
for {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("========================================")
|
||||||
|
fmt.Println(" Logof Client Installer")
|
||||||
|
fmt.Println("========================================")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" 1) Logof Client installieren")
|
||||||
|
fmt.Println(" 2) Nach Updates suchen")
|
||||||
|
fmt.Println(" 3) Logof Client deinstallieren")
|
||||||
|
fmt.Println(" 4) Beenden")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Print("Auswahl: ")
|
||||||
|
|
||||||
|
var choice string
|
||||||
|
if _, err := fmt.Scanln(&choice); err != nil {
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch choice {
|
||||||
|
case "1":
|
||||||
|
err = runPrivileged(config, "install")
|
||||||
|
|
||||||
|
case "2":
|
||||||
|
err = runPrivileged(config, "update")
|
||||||
|
|
||||||
|
case "3":
|
||||||
|
err = runPrivileged(config, "uninstall")
|
||||||
|
|
||||||
|
case "4":
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println("Ungültige Auswahl.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
printError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Print("Drücken Sie Enter, um fortzufahren...")
|
||||||
|
fmt.Scanln()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Println(`Logof Client Installer
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
logofclient-installer
|
||||||
|
logofclient-installer install
|
||||||
|
logofclient-installer update
|
||||||
|
logofclient-installer uninstall
|
||||||
|
|
||||||
|
Optionen:
|
||||||
|
-h, --help Diese Hilfe anzeigen
|
||||||
|
-v, --version Version des Installers anzeigen
|
||||||
|
-y <command> Befehl ohne TUI ausführen
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
logofclient-installer install
|
||||||
|
logofclient-installer update
|
||||||
|
logofclient-installer uninstall
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Installation
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func install(config Config) error {
|
||||||
|
fmt.Println("Fetching latest version...")
|
||||||
|
|
||||||
|
release, err := getLatestRelease(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := release.TagName
|
||||||
|
if tag == "" {
|
||||||
|
return errors.New("Release enthält keinen Tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Installing %s %s...\n", appName, tag)
|
||||||
|
|
||||||
|
assetName := getAssetName(tag)
|
||||||
|
|
||||||
|
asset, err := findAsset(release, assetName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloading package: %s\n", asset.Name)
|
||||||
|
|
||||||
|
tempDir, err := os.MkdirTemp("", "logofclient-installer-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("temporäres Verzeichnis konnte nicht erstellt werden: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
archivePath := filepath.Join(tempDir, asset.Name)
|
||||||
|
|
||||||
|
if err := download(asset.BrowserDownloadURL, archivePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installDir, err := getInstallDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("Installationsverzeichnis konnte nicht erstellt werden: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archiv entpacken
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
if err := extractTarXz(archivePath, installDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "windows":
|
||||||
|
if err := extractZip(archivePath, installDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("nicht unterstütztes Betriebssystem: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installierte Version speichern
|
||||||
|
versionFile := filepath.Join(installDir, ".version")
|
||||||
|
|
||||||
|
if err := os.WriteFile(
|
||||||
|
versionFile,
|
||||||
|
[]byte(tag+"\n"),
|
||||||
|
0644,
|
||||||
|
); err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s %s wurde erfolgreich installiert.\n", appName, tag)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Update
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func update(config Config) error {
|
||||||
|
installDir, err := getInstallDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
versionFile := filepath.Join(installDir, ".version")
|
||||||
|
|
||||||
|
currentVersionData, err := os.ReadFile(versionFile)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("Logof Client ist nicht installiert.")
|
||||||
|
fmt.Println("Installiere aktuelle Version...")
|
||||||
|
return install(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("installierte Version konnte nicht gelesen werden: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVersion := strings.TrimSpace(string(currentVersionData))
|
||||||
|
|
||||||
|
fmt.Println("Fetching latest version...")
|
||||||
|
|
||||||
|
release, err := getLatestRelease(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVersion := release.TagName
|
||||||
|
|
||||||
|
fmt.Printf("Installierte Version: %s\n", valueOrNone(currentVersion))
|
||||||
|
fmt.Printf("Neueste Version: %s\n", valueOrNone(latestVersion))
|
||||||
|
|
||||||
|
if currentVersion == latestVersion {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Logof Client ist bereits aktuell.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Update von %s auf %s...\n", currentVersion, latestVersion)
|
||||||
|
|
||||||
|
// install() entfernt die alte Version selbst.
|
||||||
|
return install(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Uninstall
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func uninstall(config Config) error {
|
||||||
|
_ = config // aktuell nicht benötigt
|
||||||
|
|
||||||
|
installDir, err := getInstallDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(installDir); os.IsNotExist(err) {
|
||||||
|
fmt.Println("Logof Client ist nicht installiert.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := removeIfExists(installDir); err != nil {
|
||||||
|
return fmt.Errorf("Installation konnte nicht entfernt werden: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Logof Client wurde erfolgreich deinstalliert.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Gitea
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func getLatestRelease(config Config) (*Release, error) {
|
||||||
|
url := fmt.Sprintf(
|
||||||
|
"%s/api/v1/repos/%s/%s/releases/latest",
|
||||||
|
strings.TrimRight(config.GiteaURL, "/"),
|
||||||
|
config.Owner,
|
||||||
|
config.Repo,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
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))
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Gitea API antwortete mit HTTP %d: %s",
|
||||||
|
resp.StatusCode,
|
||||||
|
strings.TrimSpace(string(body)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 &release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAsset(release *Release, name string) (*Asset, error) {
|
||||||
|
for _, asset := range release.Assets {
|
||||||
|
if asset.Name == name {
|
||||||
|
return &asset, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Release %s enthält nicht das erwartete Paket: %s",
|
||||||
|
release.TagName,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAssetName(tag string) string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
return fmt.Sprintf("%s-%s-linux.tar.xz", appName, tag)
|
||||||
|
|
||||||
|
case "windows":
|
||||||
|
return fmt.Sprintf("%s-%s-win.zip", appName, tag)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func download(url string, destination string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Download fehlgeschlagen: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Download fehlgeschlagen: HTTP %d",
|
||||||
|
resp.StatusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(destination)
|
||||||
|
if err != nil {
|
||||||
|
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",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
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 {
|
||||||
|
file, err := os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(xzReader)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("TAR-Archiv konnte nicht gelesen werden: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Verzeichnis %s konnte nicht erstellt werden: %w",
|
||||||
|
target,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case tar.TypeReg:
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.OpenFile(
|
||||||
|
target,
|
||||||
|
os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
|
||||||
|
os.FileMode(header.Mode),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Datei %s konnte nicht erstellt werden: %w",
|
||||||
|
target,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, copyErr := io.Copy(out, tarReader)
|
||||||
|
closeErr := out.Close()
|
||||||
|
|
||||||
|
if copyErr != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Datei %s konnte nicht entpackt werden: %w",
|
||||||
|
target,
|
||||||
|
copyErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
case tar.TypeSymlink:
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := removeIfExists(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
linkTarget := header.Linkname
|
||||||
|
|
||||||
|
if err := os.Symlink(linkTarget, target); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Symlink %s konnte nicht erstellt werden: %w",
|
||||||
|
target,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(
|
||||||
|
"nicht unterstützter TAR-Eintrag: %s",
|
||||||
|
header.Name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
target, err := safeJoin(destination, file.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
if err := os.MkdirAll(target, 0755); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Verzeichnis %s konnte nicht erstellt werden: %w",
|
||||||
|
target,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"ZIP-Datei %s konnte nicht geöffnet werden: %w",
|
||||||
|
file.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.OpenFile(
|
||||||
|
target,
|
||||||
|
os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
in.Close()
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Datei %s konnte nicht erstellt werden: %w",
|
||||||
|
target,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, copyErr := io.Copy(out, in)
|
||||||
|
|
||||||
|
in.Close()
|
||||||
|
closeErr := out.Close()
|
||||||
|
|
||||||
|
if copyErr != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Datei %s konnte nicht entpackt werden: %w",
|
||||||
|
target,
|
||||||
|
copyErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func safeJoin(base string, name string) (string, error) {
|
||||||
|
if filepath.IsAbs(name) {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"unsicherer Archivpfad: %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanName := filepath.Clean(name)
|
||||||
|
|
||||||
|
if cleanName == ".." ||
|
||||||
|
strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"unsicherer Archivpfad: %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := filepath.Join(base, cleanName)
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(base, target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rel == ".." ||
|
||||||
|
strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"unsicherer Archivpfad: %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeIfExists(path string) error {
|
||||||
|
err := os.RemoveAll(path)
|
||||||
|
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOrDefault(name string, fallback string) string {
|
||||||
|
value := strings.TrimSpace(os.Getenv(name))
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueOrNone(value string) string {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return "<unbekannt>"
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func printError(err error) {
|
||||||
|
fmt.Printf("\nERROR: %v\n", err)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user