Compare commits
21
Commits
618b2b9483
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
373359116c | ||
|
|
1cb2296480 | ||
|
|
0ee10a64c1 | ||
|
|
88ce254037 | ||
|
|
231ce0767d | ||
|
|
061cf03277 | ||
|
|
4836e2a842 | ||
|
|
86ea3fc0f7 | ||
|
|
47155e4ca5 | ||
|
|
b7ab9594c9 | ||
|
|
e7038574b0 | ||
|
|
b0796e7bce | ||
|
|
2294ea140d | ||
|
|
58cd91e637 | ||
|
|
de623efc43 | ||
|
|
a1af41db6b | ||
|
|
e22ace00d4 | ||
|
|
425a02f539 | ||
|
|
d34ec2d974 | ||
|
|
266eb7fb75 | ||
|
|
b240b07f3a |
@@ -1,3 +1,21 @@
|
|||||||
# logofclient-launcher
|
# logofclient-launcher
|
||||||
|
|
||||||
Installs/Updates logofclient on your system.
|
Installs/Updates logofclient on your system.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
See [https://mypapercloud.de/logof/wiki/installation](https://mypapercloud.de/logof/wiki/installation)
|
||||||
|
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
See [https://mypapercloud.de/logof/wiki/installation](https://mypapercloud.de/logof/wiki/installation/#windows)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Troubleshooting</summary>
|
||||||
|
|
||||||
|
Install linux.
|
||||||
|
|
||||||
|
</details>
|
||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
module git.mypapercloud.de/fierke/logofclient-installer
|
||||||
|
|
||||||
|
go 1.26.8
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +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=
|
||||||
@@ -0,0 +1,870 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "-y":
|
||||||
|
if len(args) != 2 {
|
||||||
|
printHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[1] {
|
||||||
|
case "install", "update", "uninstall":
|
||||||
|
if err := runPrivileged(config, args[1]); err != nil {
|
||||||
|
printError(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
printHelp()
|
||||||
|
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()
|
||||||
|
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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch strings.TrimSpace(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 <Befehl> Befehl ohne interaktive Abfrage ausführen
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
logofclient-installer install
|
||||||
|
logofclient-installer update
|
||||||
|
logofclient-installer uninstall
|
||||||
|
logofclient-installer -y install`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if assetName == "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"kein Paket für Betriebssystem %s definiert",
|
||||||
|
runtime.GOOS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := platformInstallIntegration(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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return install(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Deinstallation
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
func uninstall(config Config) error {
|
||||||
|
_ = config
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if err := platformRemoveIntegration(); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Archive
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Symlink(
|
||||||
|
header.Linkname,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Hilfsfunktionen
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Write-Host "Welcome to logofclient!"
|
||||||
|
Write-Host "(c)2026 - MyPapertown, MyPaperCloud, Elias Fierke"
|
||||||
|
Write-Host "Version 1.0"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Note: Windows (Un)Installer has been implemented with AI and is untested. Please report to fierke@mypapertown.de"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
$GiteaUrl = if ($env:GITEA_URL) { $env:GITEA_URL } else { "https://git.mypapercloud.de" }
|
||||||
|
$Owner = if ($env:OWNER) { $env:OWNER } else { "fierke" }
|
||||||
|
$Repo = if ($env:REPO) { $env:REPO } else { "logofclient" }
|
||||||
|
|
||||||
|
$InstallDir = Join-Path $env:ProgramFiles "logofclient"
|
||||||
|
$VersionFile = Join-Path $InstallDir ".version"
|
||||||
|
|
||||||
|
$StartMenuDir = Join-Path `
|
||||||
|
$env:ProgramData `
|
||||||
|
"Microsoft\Windows\Start Menu\Programs"
|
||||||
|
|
||||||
|
$ShortcutPath = Join-Path `
|
||||||
|
$StartMenuDir `
|
||||||
|
"logofclient.lnk"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Administrator privileges
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
function Test-IsAdministrator {
|
||||||
|
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$Principal = New-Object Security.Principal.WindowsPrincipal($Identity)
|
||||||
|
|
||||||
|
return $Principal.IsInRole(
|
||||||
|
[Security.Principal.WindowsBuiltInRole]::Administrator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Ensure-Administrator {
|
||||||
|
if (Test-IsAdministrator) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Administrator privileges are required."
|
||||||
|
Write-Host "Requesting elevation..."
|
||||||
|
|
||||||
|
# Reconstruct the original arguments
|
||||||
|
$ArgumentList = @(
|
||||||
|
"-NoProfile"
|
||||||
|
"-ExecutionPolicy"
|
||||||
|
"Bypass"
|
||||||
|
"-File"
|
||||||
|
"`"$PSCommandPath`""
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($Arg in $args) {
|
||||||
|
$ArgumentList += "`"$Arg`""
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Process `
|
||||||
|
-FilePath "powershell.exe" `
|
||||||
|
-ArgumentList $ArgumentList `
|
||||||
|
-Verb RunAs
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Help
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
function Show-Help {
|
||||||
|
Write-Host "-- HELP --"
|
||||||
|
Write-Host " -v | Print the current application version"
|
||||||
|
Write-Host " -h | Show this help text"
|
||||||
|
Write-Host " -y | Answer all questions with yes"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "-- COMMANDS --"
|
||||||
|
Write-Host " install | install logofclient to your Windows-system"
|
||||||
|
Write-Host " update | update logofclient to the newest version"
|
||||||
|
Write-Host " uninstall | remove logofclient from your system"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Gitea
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
function Get-LatestRelease {
|
||||||
|
$Api = "$($GiteaUrl.TrimEnd('/'))/api/v1"
|
||||||
|
|
||||||
|
Write-Host "Fetching version..."
|
||||||
|
|
||||||
|
return Invoke-RestMethod `
|
||||||
|
-Uri "$Api/repos/$Owner/$Repo/releases/latest" `
|
||||||
|
-Method Get
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Install
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
function Install {
|
||||||
|
Ensure-Administrator
|
||||||
|
|
||||||
|
$Release = Get-LatestRelease
|
||||||
|
$Tag = $Release.tag_name
|
||||||
|
|
||||||
|
Write-Host "Installing logofclient $Tag..."
|
||||||
|
|
||||||
|
$AssetName = "logofclient-$Tag-win.zip"
|
||||||
|
|
||||||
|
$Asset = $Release.assets |
|
||||||
|
Where-Object { $_.name -eq $AssetName } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $Asset) {
|
||||||
|
throw "Could not find release asset '$AssetName'."
|
||||||
|
}
|
||||||
|
|
||||||
|
$AssetUrl = $Asset.browser_download_url
|
||||||
|
|
||||||
|
Write-Host "Downloading package: $AssetUrl"
|
||||||
|
|
||||||
|
$TempDir = Join-Path `
|
||||||
|
$env:TEMP `
|
||||||
|
"logofclient-install"
|
||||||
|
|
||||||
|
$Archive = Join-Path `
|
||||||
|
$TempDir `
|
||||||
|
$AssetName
|
||||||
|
|
||||||
|
if (Test-Path $TempDir) {
|
||||||
|
Remove-Item `
|
||||||
|
$TempDir `
|
||||||
|
-Recurse `
|
||||||
|
-Force
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item `
|
||||||
|
-ItemType Directory `
|
||||||
|
-Path $TempDir | Out-Null
|
||||||
|
|
||||||
|
Invoke-WebRequest `
|
||||||
|
-Uri $AssetUrl `
|
||||||
|
-OutFile $Archive
|
||||||
|
|
||||||
|
Write-Host "Installing to: $InstallDir"
|
||||||
|
|
||||||
|
# Remove previous installation
|
||||||
|
if (Test-Path $InstallDir) {
|
||||||
|
Remove-Item `
|
||||||
|
$InstallDir `
|
||||||
|
-Recurse `
|
||||||
|
-Force
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item `
|
||||||
|
-ItemType Directory `
|
||||||
|
-Path $InstallDir | Out-Null
|
||||||
|
|
||||||
|
# Extract ZIP
|
||||||
|
Expand-Archive `
|
||||||
|
-Path $Archive `
|
||||||
|
-DestinationPath $InstallDir `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
# Save installed version
|
||||||
|
Set-Content `
|
||||||
|
-Path $VersionFile `
|
||||||
|
-Value $Tag `
|
||||||
|
-Encoding UTF8
|
||||||
|
|
||||||
|
# Find executable
|
||||||
|
$Executable = Join-Path `
|
||||||
|
$InstallDir `
|
||||||
|
"Logof Client.exe"
|
||||||
|
|
||||||
|
if (-not (Test-Path $Executable)) {
|
||||||
|
throw "Executable '$Executable' was not found after installation."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Start Menu shortcut
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
Write-Host "Creating Start Menu shortcut..."
|
||||||
|
|
||||||
|
New-Item `
|
||||||
|
-ItemType Directory `
|
||||||
|
-Path $StartMenuDir `
|
||||||
|
-Force | Out-Null
|
||||||
|
|
||||||
|
$Shell = New-Object -ComObject WScript.Shell
|
||||||
|
|
||||||
|
$Shortcut = $Shell.CreateShortcut($ShortcutPath)
|
||||||
|
|
||||||
|
$Shortcut.TargetPath = $Executable
|
||||||
|
$Shortcut.WorkingDirectory = $InstallDir
|
||||||
|
$Shortcut.Description = "logofclient"
|
||||||
|
|
||||||
|
$IconPath = Join-Path `
|
||||||
|
$InstallDir `
|
||||||
|
"assets\icon.ico"
|
||||||
|
|
||||||
|
if (Test-Path $IconPath) {
|
||||||
|
$Shortcut.IconLocation = $IconPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$Shortcut.Save()
|
||||||
|
|
||||||
|
# Clean up downloaded archive
|
||||||
|
Remove-Item `
|
||||||
|
$TempDir `
|
||||||
|
-Recurse `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Installed logofclient $Tag successfully."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Uninstall
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
function Uninstall {
|
||||||
|
Ensure-Administrator
|
||||||
|
|
||||||
|
Write-Host "Uninstalling logofclient..."
|
||||||
|
|
||||||
|
if (Test-Path $InstallDir) {
|
||||||
|
Remove-Item `
|
||||||
|
$InstallDir `
|
||||||
|
-Recurse `
|
||||||
|
-Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $ShortcutPath) {
|
||||||
|
Remove-Item `
|
||||||
|
$ShortcutPath `
|
||||||
|
-Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "logofclient has been uninstalled."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Update
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
function Update {
|
||||||
|
Ensure-Administrator
|
||||||
|
|
||||||
|
Write-Host "Checking for updates..."
|
||||||
|
|
||||||
|
$InstalledTag = ""
|
||||||
|
|
||||||
|
if (Test-Path $VersionFile) {
|
||||||
|
$InstalledTag = (
|
||||||
|
Get-Content `
|
||||||
|
$VersionFile `
|
||||||
|
-Raw
|
||||||
|
).Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
$Release = Get-LatestRelease
|
||||||
|
$LatestTag = $Release.tag_name
|
||||||
|
|
||||||
|
Write-Host "Installed version: $InstalledTag"
|
||||||
|
Write-Host "Latest version: $LatestTag"
|
||||||
|
|
||||||
|
if ($InstalledTag -eq $LatestTag) {
|
||||||
|
Write-Host "Already up to date."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Updating logofclient: $InstalledTag -> $LatestTag"
|
||||||
|
|
||||||
|
if ($InstalledTag) {
|
||||||
|
Uninstall
|
||||||
|
}
|
||||||
|
|
||||||
|
Install
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
$AlwaysYes = $false
|
||||||
|
$Command = $null
|
||||||
|
|
||||||
|
foreach ($Arg in $args) {
|
||||||
|
switch ($Arg) {
|
||||||
|
|
||||||
|
"-y" {
|
||||||
|
$AlwaysYes = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
"-h" {
|
||||||
|
Show-Help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"--help" {
|
||||||
|
Show-Help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"-v" {
|
||||||
|
Write-Host "Version 1.0"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"install" {
|
||||||
|
$Command = "install"
|
||||||
|
}
|
||||||
|
|
||||||
|
"update" {
|
||||||
|
$Command = "update"
|
||||||
|
}
|
||||||
|
|
||||||
|
"uninstall" {
|
||||||
|
$Command = "uninstall"
|
||||||
|
}
|
||||||
|
|
||||||
|
default {
|
||||||
|
if ($Arg.StartsWith("-")) {
|
||||||
|
Write-Error "Unknown option: $Arg"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Error "Unknown command: $Arg"
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (-not $Command) {
|
||||||
|
Show-Help
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Execute command
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
switch ($Command) {
|
||||||
|
|
||||||
|
"install" {
|
||||||
|
Install
|
||||||
|
}
|
||||||
|
|
||||||
|
"update" {
|
||||||
|
Update
|
||||||
|
}
|
||||||
|
|
||||||
|
"uninstall" {
|
||||||
|
Uninstall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user