test release
This commit is contained in:
commit
a15ff9ba5f
100
cmd/extractor/main.go
Normal file
100
cmd/extractor/main.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"kisekinopureya.com.tr/updater/internal/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sha256sum(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
fmt.Printf("Usage: %s <archive.tar.xz> <archive.tar.xz.sig>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveFile := os.Args[1]
|
||||||
|
sig := os.Args[2]
|
||||||
|
|
||||||
|
// Step 1: verify GPG signature
|
||||||
|
cmd := exec.Command("/usr/bin/gpgv", "--keyring", "/etc/updates-public.gpg", sig, archiveFile)
|
||||||
|
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "GPG verification failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("GPG signature verified.")
|
||||||
|
|
||||||
|
// Step 2: open archiveFile
|
||||||
|
f, err := os.Open(archiveFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Step 2: extract tar.xz using system tar
|
||||||
|
cmdTar := exec.Command("tar", "-xJf", archiveFile)
|
||||||
|
cmdTar.Stdout = os.Stdout
|
||||||
|
cmdTar.Stderr = os.Stderr
|
||||||
|
if err := cmdTar.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metaFile := "metadata.json"
|
||||||
|
f, err = os.Open(metaFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var meta archive.Output
|
||||||
|
if err := json.NewDecoder(f).Decode(&meta); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: check sha256
|
||||||
|
ok := true
|
||||||
|
for _, file := range meta.Files {
|
||||||
|
sum, err := sha256sum(file.Name)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to hash %s: %v\n", file.Name, err)
|
||||||
|
ok = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sum != file.Sha256 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Checksum mismatch for %s!\nExpected: %s\nGot: %s\n",
|
||||||
|
file.Name, file.Sha256, sum)
|
||||||
|
ok = false
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s checksum OK\n", file.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("All files verified successfully.")
|
||||||
|
}
|
||||||
40
cmd/os-branch-select/main.go
Normal file
40
cmd/os-branch-select/main.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"kisekinopureya.com.tr/updater/internal/logger"
|
||||||
|
"kisekinopureya.com.tr/updater/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger.InitializeLogger("/tmp/os-select-branchlog")
|
||||||
|
|
||||||
|
log.Printf("%+v", os.Args)
|
||||||
|
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Println("Usage: steamos-select-branch <-stable|unstable|testing>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
branchPath := version.BranchPath
|
||||||
|
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "-c":
|
||||||
|
branch := version.GetCurrentBranch()
|
||||||
|
fmt.Printf("%s\n", branch)
|
||||||
|
os.Exit(0)
|
||||||
|
case "-l":
|
||||||
|
fmt.Printf("%s\n%s\n%s\n", "stable", "unstable", "testing")
|
||||||
|
os.Exit(0)
|
||||||
|
case "stable", "unstable", "testing":
|
||||||
|
err := os.WriteFile(branchPath, []byte(os.Args[1]), 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
cmd/packer/main.go
Normal file
25
cmd/packer/main.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"kisekinopureya.com.tr/updater/internal/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 5 {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: %s <rootfs.img> <dm-verity.img> <kernel.img> <output.tar.xz>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootfs := os.Args[1]
|
||||||
|
verity := os.Args[2]
|
||||||
|
kernel := os.Args[3]
|
||||||
|
output := os.Args[4]
|
||||||
|
|
||||||
|
if err := archive.Pack(rootfs, verity, kernel, output); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "packer error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
165
cmd/patcher/main.go
Normal file
165
cmd/patcher/main.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"kisekinopureya.com.tr/updater/internal/archive"
|
||||||
|
"kisekinopureya.com.tr/updater/internal/logger"
|
||||||
|
mountfilesystem "kisekinopureya.com.tr/updater/internal/mountFilesystem"
|
||||||
|
"kisekinopureya.com.tr/updater/internal/plymouth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BlockDevice struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
PartType string `json:"parttype"`
|
||||||
|
Children []BlockDevice `json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LSBLK struct {
|
||||||
|
BlockDevices []BlockDevice `json:"blockdevices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type partitionScheme struct {
|
||||||
|
EfiDevice string
|
||||||
|
RootfsDevice string
|
||||||
|
DmVerityDevice string
|
||||||
|
//VarDevice string
|
||||||
|
}
|
||||||
|
|
||||||
|
var efiUUID string = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
|
||||||
|
var rootfsUUID string = "4f68bce3-e8cd-4db1-96e7-fbcaf984b709"
|
||||||
|
var dmverityUUID string = "2c7357ed-ebd2-46d9-aec1-23d437ec2bf5"
|
||||||
|
|
||||||
|
//var varUUID string = "4d21b016-b534-45c2-a9fb-5c16e091fd2d"
|
||||||
|
|
||||||
|
func findByPartType(devices []BlockDevice, target string) string {
|
||||||
|
for _, dev := range devices {
|
||||||
|
if dev.PartType != "" && dev.PartType == target {
|
||||||
|
return dev.Name
|
||||||
|
}
|
||||||
|
if len(dev.Children) > 0 {
|
||||||
|
if found := findByPartType(dev.Children, target); found != "" {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func populatePartitionScheme() partitionScheme {
|
||||||
|
|
||||||
|
var generatedScheme partitionScheme = partitionScheme{}
|
||||||
|
cmd := exec.Command("lsblk", "-J", "-o", "NAME,TYPE,PARTTYPE")
|
||||||
|
blockDeviceTypes, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lsblk LSBLK
|
||||||
|
if err := json.Unmarshal(blockDeviceTypes, &lsblk); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
generatedScheme.EfiDevice = findByPartType(lsblk.BlockDevices, efiUUID)
|
||||||
|
generatedScheme.RootfsDevice = findByPartType(lsblk.BlockDevices, rootfsUUID)
|
||||||
|
generatedScheme.DmVerityDevice = findByPartType(lsblk.BlockDevices, dmverityUUID)
|
||||||
|
|
||||||
|
return generatedScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchImage(imageFile string, partition string, name string) {
|
||||||
|
cmd := exec.Command("dd", "if="+imageFile, "of="+"/dev/"+partition, "bs=4M")
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
plymouth.ShowPlymouthMessage("Patching " + name + " partition")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyFile(source, destDir string) (string, error) {
|
||||||
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("mkdir failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("open src failed: %w", err)
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
dstPath := filepath.Join(destDir, filepath.Base(source))
|
||||||
|
|
||||||
|
out, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create dst failed: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return "", fmt.Errorf("copy failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := out.Sync(); err != nil {
|
||||||
|
return "", fmt.Errorf("sync failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dstPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger.InitializeLogger("/mnt/var/log/patcher")
|
||||||
|
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Printf("Usage: %s <metadata>\n", os.Args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(os.Args[1])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var meta archive.Output
|
||||||
|
if err := json.NewDecoder(f).Decode(&meta); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPartitionScheme := populatePartitionScheme()
|
||||||
|
|
||||||
|
fileCount := len(meta.Files)
|
||||||
|
|
||||||
|
// before 20 and after 80 will be filled by initramfs
|
||||||
|
leftProgress := 60/fileCount + 20
|
||||||
|
|
||||||
|
for _, f := range meta.Files {
|
||||||
|
plymouth.ShowPlymouthProgress(leftProgress)
|
||||||
|
leftProgress += leftProgress
|
||||||
|
|
||||||
|
imageFile := f.Name
|
||||||
|
imageType := f.Type
|
||||||
|
|
||||||
|
switch imageType {
|
||||||
|
case "rootfs":
|
||||||
|
patchImage(imageFile, currentPartitionScheme.RootfsDevice, imageType)
|
||||||
|
case "dm-verity":
|
||||||
|
patchImage(imageFile, currentPartitionScheme.DmVerityDevice, imageType)
|
||||||
|
case "kernel":
|
||||||
|
mountPath := "/updates-tmp/efi"
|
||||||
|
kernelDir := "/EFI/Linux"
|
||||||
|
mountfilesystem.MountFileSystem("/dev/"+currentPartitionScheme.EfiDevice, mountPath, "vfat", 0, "")
|
||||||
|
CopyFile(imageFile, mountPath+kernelDir)
|
||||||
|
mountfilesystem.UnmountFileSystem(mountPath)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
cmd/steamos-update/main.go
Normal file
125
cmd/steamos-update/main.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kisekinopureya.com.tr/updater/internal/downloader"
|
||||||
|
"kisekinopureya.com.tr/updater/internal/logger"
|
||||||
|
"kisekinopureya.com.tr/updater/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
The Steam client is known to call this program with the following parameter combinations:
|
||||||
|
steamos-update --supports-duplicate-detection -- should do nothing
|
||||||
|
steamos-update --enable-duplicate-detection check -- should check for update
|
||||||
|
steamos-update check -- should check for update
|
||||||
|
steamos-update --enable-duplicate-detection -- should perform an update
|
||||||
|
steamos-update -- should perform an update
|
||||||
|
*/
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger.InitializeLogger("/tmp/steamos-updatelog")
|
||||||
|
|
||||||
|
log.Printf("%+v", os.Args)
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
action := performUpdate
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(args) == 1 && args[0] == "--supports-duplicate-detection":
|
||||||
|
os.Exit(7)
|
||||||
|
|
||||||
|
case len(args) >= 1 && args[0] == "--enable-duplicate-detection":
|
||||||
|
if len(args) >= 2 && args[1] == "check" {
|
||||||
|
action = checkForUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
case len(args) >= 1 && args[0] == "check":
|
||||||
|
action = checkForUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
action()
|
||||||
|
|
||||||
|
os.Exit(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIndex(url string) ([]string, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch index: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("bad status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line != "" {
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("reading response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUpdate() {
|
||||||
|
branch := version.GetCurrentBranch()
|
||||||
|
currentVersion := version.DetermineCurrentVersion()
|
||||||
|
currentVersionInt, err := strconv.ParseInt(currentVersion, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
currentVersionInt = 0 //
|
||||||
|
}
|
||||||
|
|
||||||
|
indexFile, err := fetchIndex("http://10.26.1.3/updates/index")
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
for _, s := range indexFile {
|
||||||
|
if strings.HasPrefix(s, string(branch)) && !strings.HasSuffix(s, ".sig") {
|
||||||
|
parts := strings.Split(s, "/")
|
||||||
|
base := parts[len(parts)-1]
|
||||||
|
|
||||||
|
// Remove extension
|
||||||
|
base = strings.TrimSuffix(base, ".tar.xz")
|
||||||
|
|
||||||
|
// Split by dash
|
||||||
|
tokens := strings.Split(base, "-")
|
||||||
|
versionFromFile := tokens[len(tokens)-1]
|
||||||
|
versionFromFileInt, err := strconv.ParseInt(versionFromFile, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if currentVersionInt < versionFromFileInt {
|
||||||
|
fmt.Printf("Update available: %s\n", base)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func performUpdate() {
|
||||||
|
branch := version.GetCurrentBranch()
|
||||||
|
err := downloader.DownloadFile("http://10.26.1.3/updates/"+branch+"/latest.tar.xz.sig", "/var/cache/updates/update.tar.xz.sig", true)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
err = downloader.DownloadFile("http://10.26.1.3/updates/"+branch+"/latest.tar.xz", "/var/cache/updates/update.tar.xz", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module kisekinopureya.com.tr/updater
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.36.0
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
207
internal/archive/packer.go
Normal file
207
internal/archive/packer.go
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"kisekinopureya.com.tr/updater/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Output metadata structure
|
||||||
|
type Output struct {
|
||||||
|
Metadata struct {
|
||||||
|
Date int64 `json:"date"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
Files []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Sha256 string `json:"sha256"`
|
||||||
|
} `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileInfo struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Path string
|
||||||
|
Sha256 string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack creates a tar.xz archive with metadata.json and the given images
|
||||||
|
func Pack(rootfsPath, verityPath, kernelPath, outPath string) error {
|
||||||
|
paths := []struct {
|
||||||
|
Path string
|
||||||
|
Type string
|
||||||
|
}{
|
||||||
|
{rootfsPath, "rootfs"},
|
||||||
|
{verityPath, "dm-verity"},
|
||||||
|
{kernelPath, "kernel"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var infos []fileInfo
|
||||||
|
for _, p := range paths {
|
||||||
|
log.Printf("Hashing %s...", p.Path)
|
||||||
|
sum, err := sha256File(p.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("hashing %s: %w", p.Path, err)
|
||||||
|
}
|
||||||
|
infos = append(infos, fileInfo{
|
||||||
|
Name: filepath.Base(p.Path),
|
||||||
|
Type: p.Type,
|
||||||
|
Path: p.Path,
|
||||||
|
Sha256: sum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := exec.Command("blkid", rootfsPath).Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionFromImage, err := version.Parse(string(output))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
var out Output
|
||||||
|
out.Metadata.Date = time.Now().Unix()
|
||||||
|
// NOTE: version should be filled by caller if needed
|
||||||
|
out.Metadata.Version = versionFromImage
|
||||||
|
for _, fi := range infos {
|
||||||
|
out.Files = append(out.Files, struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Sha256 string `json:"sha256"`
|
||||||
|
}{
|
||||||
|
Name: fi.Name,
|
||||||
|
Type: fi.Type,
|
||||||
|
Sha256: fi.Sha256,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
metaBytes, err := json.MarshalIndent(out, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Metadata: %+v\n", out)
|
||||||
|
|
||||||
|
if err := writeTarXz(outPath, metaBytes, infos); err != nil {
|
||||||
|
return fmt.Errorf("create tar.xz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigFile := outPath + ".sig"
|
||||||
|
cmd := exec.Command("gpg", "--output", sigFile, "--detach-sign", "--armor", outPath)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
panic(fmt.Errorf("gpg signing failed: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Created %s and signed as %s\n", outPath, sigFile)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256File(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTarXz(outPath string, metadata []byte, files []fileInfo) error {
|
||||||
|
outFile, err := os.Create(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create output: %w", err)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("xz", "-z", "-c")
|
||||||
|
cmd.Stdout = outFile
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tar.NewWriter(stdin)
|
||||||
|
|
||||||
|
// metadata.json
|
||||||
|
metaHeader := &tar.Header{
|
||||||
|
Name: "metadata.json",
|
||||||
|
Mode: 0644,
|
||||||
|
Size: int64(len(metadata)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(metaHeader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(metadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// files
|
||||||
|
for _, fi := range files {
|
||||||
|
if err := addFileToTar(tw, fi.Path, fi.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFileToTar(tw *tar.Writer, srcPath, destName string) error {
|
||||||
|
f, err := os.Open(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: destName,
|
||||||
|
Mode: int64(stat.Mode().Perm()),
|
||||||
|
Size: stat.Size(),
|
||||||
|
ModTime: stat.ModTime(),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(tw, f)
|
||||||
|
return err
|
||||||
|
}
|
||||||
81
internal/downloader/downloader.go
Normal file
81
internal/downloader/downloader.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package downloader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DownloadFile(url, filepath string, silent bool) error {
|
||||||
|
var start int64
|
||||||
|
if info, err := os.Stat(filepath); err == nil {
|
||||||
|
start = info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if start > 0 {
|
||||||
|
if _, err := out.Seek(start, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP request with Range header for resume
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if start > 0 {
|
||||||
|
req.Header.Set("Range", "bytes="+strconv.FormatInt(start, 10)+"-")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total size for progress calculation
|
||||||
|
var total int64
|
||||||
|
if resp.StatusCode == http.StatusPartialContent {
|
||||||
|
contentRange := resp.Header.Get("Content-Range") // e.g., bytes 1234-9999/10000
|
||||||
|
var sizeStr string
|
||||||
|
fmt.Sscanf(contentRange, "bytes %*d-%*d/%s", &sizeStr)
|
||||||
|
total, _ = strconv.ParseInt(sizeStr, 10, 64)
|
||||||
|
} else if resp.ContentLength > 0 {
|
||||||
|
total = start + resp.ContentLength
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
var downloaded = start
|
||||||
|
for {
|
||||||
|
n, err := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
downloaded += int64(n)
|
||||||
|
if total > 0 && !silent {
|
||||||
|
fmt.Printf("\r%.2f%%", float64(downloaded)*100/float64(total))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
23
internal/logger/logger.go
Normal file
23
internal/logger/logger.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func InitializeLogger(filePath string) error {
|
||||||
|
var err error
|
||||||
|
once.Do(func() {
|
||||||
|
logFile, e := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
34
internal/mountFilesystem/mountfilesystem.go
Normal file
34
internal/mountFilesystem/mountfilesystem.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package mountfilesystem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"kisekinopureya.com.tr/updater/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MountFileSystem(source string, target string, fsType string, flags int, data string) {
|
||||||
|
logger.InitializeLogger("/tmp/mountLog")
|
||||||
|
|
||||||
|
if err := unix.Mkdir(target, 0755); err != nil && !os.IsExist(err) {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := unix.Mount(source, target, fsType, uintptr(flags), data)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(fmt.Errorf("mount failed: %w", err))
|
||||||
|
}
|
||||||
|
fmt.Println("Mounted successfully at", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnmountFileSystem(target string) {
|
||||||
|
logger.InitializeLogger("/tmp/unmountLog")
|
||||||
|
err := unix.Unmount(target, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Unmounted successfully at", target)
|
||||||
|
|
||||||
|
}
|
||||||
24
internal/plymouth/plymouth.go
Normal file
24
internal/plymouth/plymouth.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package plymouth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ShowPlymouthMessage(message string) {
|
||||||
|
cmd := exec.Command("plymouth", "update", "--status=\""+message+"\"")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Println(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowPlymouthProgress(progress int) {
|
||||||
|
if progress < 100 && progress > 0 {
|
||||||
|
progressStr := strconv.Itoa(progress)
|
||||||
|
cmd := exec.Command("plymouth", "system-update", "--progress="+progressStr+"\"")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Progress: %s%%", progressStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
internal/version/currentVersion.go
Normal file
41
internal/version/currentVersion.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCurrentBranch() string {
|
||||||
|
branch, err := os.ReadFile(BranchPath)
|
||||||
|
if err != nil {
|
||||||
|
return "stable"
|
||||||
|
}
|
||||||
|
return string(branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetermineCurrentVersion() string {
|
||||||
|
file, err := os.Open("/etc/os-release")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var lastLine string
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lastLine = scanner.Text()
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(lastLine, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
panic("invalid format")
|
||||||
|
}
|
||||||
|
value := parts[1]
|
||||||
|
|
||||||
|
currentVersion := strings.Trim(value, "\"")
|
||||||
|
return currentVersion
|
||||||
|
}
|
||||||
18
internal/version/parse.go
Normal file
18
internal/version/parse.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var BranchPath = "/var/lib/os-branch"
|
||||||
|
|
||||||
|
// Parse extracts version from LABEL="rootfs-XXXX"
|
||||||
|
func Parse(label string) (string, error) {
|
||||||
|
re := regexp.MustCompile(`LABEL="rootfs-([0-9]+)"`)
|
||||||
|
m := re.FindStringSubmatch(label)
|
||||||
|
if len(m) < 2 {
|
||||||
|
return "", fmt.Errorf("version not found in: %s", label)
|
||||||
|
}
|
||||||
|
return m[1], nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user