Compare commits

...
21 Commits
Author SHA1 Message Date
fierke 373359116c windwows's architecture is still shitty 2026-09-27 16:30:46 +02:00
fierke 1cb2296480 windowss architecture is shitty 2026-09-27 16:30:02 +02:00
fierke 0ee10a64c1 fix: admin-version in second window on windoof (incl build) 2026-09-27 16:25:25 +02:00
fierke 88ce254037 new builds 2026-09-27 15:53:05 +02:00
fierke 231ce0767d feat: splitting platforms and maybe fixing shortcut installations as well as admin privileges 2026-09-27 15:52:55 +02:00
fierke 061cf03277 fix: windows link and it was hidden 2026-09-24 12:04:38 +00:00
fierke 4836e2a842 Update README.md 2026-09-24 12:04:06 +00:00
fierke 86ea3fc0f7 updated to new installer version (go!) 2026-09-24 12:03:52 +00:00
fierke 47155e4ca5 added current build 2026-09-24 13:55:38 +02:00
fierke b7ab9594c9 moved old scriptfiles 2026-09-24 13:53:02 +02:00
fierke e7038574b0 ported installer to go 2026-09-24 13:52:03 +02:00
fierke b0796e7bce oops, forgot to remove AI's ``` 2026-09-19 16:25:05 +00:00
fierke 2294ea140d Update README.md 2026-09-19 16:22:43 +00:00
fierke 58cd91e637 Update README.md 2026-09-19 16:20:16 +00:00
fierke de623efc43 Update README.md 2026-09-18 21:12:12 +00:00
fierke a1af41db6b Update README.md 2026-09-02 11:01:13 +00:00
fierke e22ace00d4 Update README.md 2026-09-02 11:00:53 +00:00
fierke 425a02f539 Update README.md 2026-09-02 10:58:18 +00:00
fierke d34ec2d974 chore: windows-installer (untested) 2026-09-01 18:49:44 +02:00
fierke 266eb7fb75 file: better name 2026-09-01 18:49:22 +02:00
fierke b240b07f3a file: better name 2026-09-01 18:49:03 +02:00
12 changed files with 1779 additions and 1 deletions
+18
View File
@@ -1,3 +1,21 @@
# logofclient-launcher
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>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+10
View File
@@ -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
+6
View File
@@ -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=
+870
View File
@@ -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)
}
+378
View File
@@ -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
}
}
+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
}