11package main
22
33import (
4- "flag"
54 "fmt"
65 "os"
76 "os/exec"
87 "path/filepath"
9- "strconv"
108 "strings"
119 "time"
1210)
1311
1412const (
15- mountPoint = "/mnt/hroot"
16- snapshotPrefix = "@ pre-update-"
13+ mountPoint = "/mnt/hroot"
14+ snapshotPrefix = "- pre-update-" // teraz używamy tej stałej
1715 updateSnapshot = "@update"
1816 btrfsDevice = "/dev/sda1" // TODO: Detect or configure the Btrfs device
1917 rootSubvolume = "@"
@@ -34,11 +32,11 @@ func main() {
3432 case "switch" :
3533 switchCmd ()
3634 case "rollback" :
37- rollbackCmd (flag .Args ()[ 1 :])
35+ rollbackCmd (os .Args [ 2 :])
3836 case "install" :
39- installCmd (flag .Args ()[ 1 :])
37+ installCmd (os .Args [ 2 :])
4038 case "remove" :
41- removeCmd (flag .Args ()[ 1 :])
39+ removeCmd (os .Args [ 2 :])
4240 case "clean" :
4341 cleanCmd ()
4442 case "status" :
@@ -54,14 +52,14 @@ func usage() {
5452Usage: hroot <command> [args]
5553
5654Commands:
57- snapshot Create a read-only snapshot of the current root
58- update Create and update a new snapshot offline
59- switch Switch to the updated snapshot
60- rollback <name> Rollback to a specific snapshot by name (e.g., @pre-update-20251130-2013)
61- install <pkg> Install a package in the current root (non-atomic, for simplicity )
62- remove <pkg> Remove a package from the current root (non-atomic)
63- clean Clean up temporary files and unused snapshots
64- status List available snapshots` )
55+ snapshot Create a read-only snapshot of the current root
56+ update Create and update a new snapshot offline
57+ switch Switch to the updated snapshot (@update → default)
58+ rollback <name> Rollback to a specific snapshot (e.g. @pre-update-20251130-2013)
59+ install <pkg>... Install package(s) in the current root (non-atomic)
60+ remove <pkg>... Remove package(s) from the current root (non-atomic)
61+ clean Clean apt cache (snapshots must be deleted manually)
62+ status List available snapshots` )
6563}
6664
6765func runCommand (name string , args ... string ) error {
@@ -72,12 +70,32 @@ func runCommand(name string, args ...string) error {
7270}
7371
7472func getSnapshotName () string {
75- return rootSubvolume + "pre-update-" + time .Now ().Format ("20060102-1504" )
73+ return rootSubvolume + snapshotPrefix + time .Now ().Format ("20060102-1504" )
74+ }
75+
76+ // Pobiera Subvolume ID dla podanej względnej ścieżki (np. @update, @pre-update-...)
77+ func getSubvolumeID (subvol string ) (string , error ) {
78+ path := "/" + subvol // /@update, /@pre-update-20251130-2013 itd.
79+ output , err := exec .Command ("btrfs" , "subvolume" , "show" , path ).CombinedOutput ()
80+ if err != nil {
81+ return "" , fmt .Errorf ("btrfs subvolume show %s failed: %v\n %s" , path , err , output )
82+ }
83+
84+ for _ , line := range strings .Split (string (output ), "\n " ) {
85+ line = strings .TrimSpace (line )
86+ if strings .HasPrefix (line , "Subvolume ID:" ) {
87+ parts := strings .Fields (line )
88+ if len (parts ) >= 3 {
89+ return parts [2 ], nil
90+ }
91+ }
92+ }
93+ return "" , fmt .Errorf ("Subvolume ID not found for %s" , path )
7694}
7795
7896func snapshotCmd () {
7997 snapshotName := getSnapshotName ()
80- fmt .Printf ("Creating snapshot: %s\n " , snapshotName )
98+ fmt .Printf ("Creating read-only snapshot: %s\n " , snapshotName )
8199 if err := runCommand ("btrfs" , "subvolume" , "snapshot" , "-r" , "/" , snapshotName ); err != nil {
82100 fmt .Fprintf (os .Stderr , "Error creating snapshot: %v\n " , err )
83101 os .Exit (1 )
@@ -86,22 +104,22 @@ func snapshotCmd() {
86104}
87105
88106func updateCmd () {
89- // Step 1: Create writable snapshot for update
107+ // Krok 1: Tworzymy writable snapshot do aktualizacji
90108 fmt .Println ("Creating update snapshot:" , updateSnapshot )
91109 if err := runCommand ("btrfs" , "subvolume" , "snapshot" , "/" , updateSnapshot ); err != nil {
92110 fmt .Fprintf (os .Stderr , "Error creating update snapshot: %v\n " , err )
93111 os .Exit (1 )
94112 }
95113
96- // Step 2: Mount the snapshot
114+ // Krok 2: Montujemy snapshot
97115 os .MkdirAll (mountPoint , 0755 )
98116 if err := runCommand ("mount" , btrfsDevice , mountPoint , "-o" , "subvol=" + updateSnapshot ); err != nil {
99117 fmt .Fprintf (os .Stderr , "Error mounting update snapshot: %v\n " , err )
100118 os .Exit (1 )
101119 }
102- defer runCommand ("umount" , mountPoint ) // Clean up on exit
120+ defer runCommand ("umount" , mountPoint )
103121
104- // Bind mount necessary filesystems
122+ // Bind mount niezbędnych systemów plików
105123 bindMounts := []string {"/proc" , "/sys" , "/dev" , "/run" }
106124 for _ , m := range bindMounts {
107125 target := filepath .Join (mountPoint , m [1 :])
@@ -113,115 +131,113 @@ func updateCmd() {
113131 defer runCommand ("umount" , target )
114132 }
115133
116- // Step 3: Chroot and perform update
134+ // Krok 3: Chroot + aktualizacja
117135 fmt .Println ("Performing system update in chroot..." )
118- chrootCmd := []string {"chroot" , mountPoint , "apt" , "update" }
119- if err := runCommand (chrootCmd [0 ], chrootCmd [1 :]... ); err != nil {
136+ if err := runCommand ("chroot" , mountPoint , "apt" , "update" ); err != nil {
120137 fmt .Fprintf (os .Stderr , "Error running apt update: %v\n " , err )
121138 os .Exit (1 )
122139 }
123- chrootCmd = []string {"chroot" , mountPoint , "apt" , "upgrade" , "-y" }
124- if err := runCommand (chrootCmd [0 ], chrootCmd [1 :]... ); err != nil {
140+ if err := runCommand ("chroot" , mountPoint , "apt" , "upgrade" , "-y" ); err != nil {
125141 fmt .Fprintf (os .Stderr , "Error running apt upgrade: %v\n " , err )
126142 os .Exit (1 )
127143 }
128144
129- fmt .Println ("Update completed in snapshot." )
145+ fmt .Println ("Update completed successfully in snapshot:" , updateSnapshot )
146+ fmt .Println ("Run 'hroot switch' and reboot to apply." )
130147}
131148
132149func switchCmd () {
133- // Get ID of update snapshot
134- out , err := exec .Command ("btrfs" , "subvolume" , "find-new" , "/" , "9999999" ).CombinedOutput () // Hack to get current ID, but better to list
150+ id , err := getSubvolumeID (updateSnapshot )
135151 if err != nil {
136- fmt .Fprintf (os .Stderr , "Error finding subvolume ID: %v\n " , err )
152+ fmt .Fprintf (os .Stderr , "Cannot get subvolume ID for %s : %v\n " , updateSnapshot , err )
137153 os .Exit (1 )
138154 }
139- // Parse ID from output - this is simplistic, assume we know path
140- fmt .Println ( "Switching default subvolume to" , updateSnapshot )
141- if err := runCommand ("btrfs" , "subvolume" , "set-default" , updateSnapshot ); err != nil {
155+
156+ fmt .Printf ( "Setting default subvolume to %s (ID: %s) \n " , updateSnapshot , id )
157+ if err := runCommand ("btrfs" , "subvolume" , "set-default" , id , "/" ); err != nil {
142158 fmt .Fprintf (os .Stderr , "Error setting default subvolume: %v\n " , err )
143159 os .Exit (1 )
144160 }
145- fmt .Println ("Default subvolume switched . Reboot to apply ." )
161+ fmt .Println ("Default subvolume changed . Reboot required ." )
146162}
147163
148164func rollbackCmd (args []string ) {
149165 if len (args ) == 0 {
150- fmt .Fprintf (os .Stderr , "Rollback requires snapshot name\n " )
166+ fmt .Fprintf (os .Stderr , "Usage: hroot rollback <snapshot- name> \n " )
151167 os .Exit (1 )
152168 }
169+
153170 snapshotName := args [0 ]
154- fmt .Printf ("Rolling back to %s\n " , snapshotName )
155- if err := runCommand ("btrfs" , "subvolume" , "set-default" , snapshotName ); err != nil {
171+ id , err := getSubvolumeID (snapshotName )
172+ if err != nil {
173+ fmt .Fprintf (os .Stderr , "Cannot get subvolume ID for %s: %v\n " , snapshotName , err )
174+ os .Exit (1 )
175+ }
176+
177+ fmt .Printf ("Rolling back to %s (ID: %s)\n " , snapshotName , id )
178+ if err := runCommand ("btrfs" , "subvolume" , "set-default" , id , "/" ); err != nil {
156179 fmt .Fprintf (os .Stderr , "Error setting default subvolume: %v\n " , err )
157180 os .Exit (1 )
158181 }
159- fmt .Println ("Rollback set . Reboot to apply ." )
182+ fmt .Println ("Rollback successful . Reboot required ." )
160183}
161184
162- func installCmd (packages []string ) {
163- if len (packages ) == 0 {
164- fmt .Fprintf (os .Stderr , "Install requires package names \n " )
185+ func installCmd (pkgs []string ) {
186+ if len (pkgs ) == 0 {
187+ fmt .Fprintf (os .Stderr , "Usage: hroot install <package>... \n " )
165188 os .Exit (1 )
166189 }
167- fmt .Printf ("Installing packages: %v\n " , packages )
168- args := append ([]string {"install" , "-y" }, packages ... )
190+ fmt .Printf ("Installing packages (live system) : %v\n " , pkgs )
191+ args := append ([]string {"install" , "-y" }, pkgs ... )
169192 if err := runCommand ("apt" , args ... ); err != nil {
170193 fmt .Fprintf (os .Stderr , "Error installing packages: %v\n " , err )
171194 os .Exit (1 )
172195 }
173196}
174197
175- func removeCmd (packages []string ) {
176- if len (packages ) == 0 {
177- fmt .Fprintf (os .Stderr , "Remove requires package names \n " )
198+ func removeCmd (pkgs []string ) {
199+ if len (pkgs ) == 0 {
200+ fmt .Fprintf (os .Stderr , "Usage: hroot remove <package>... \n " )
178201 os .Exit (1 )
179202 }
180- fmt .Printf ("Removing packages: %v\n " , packages )
181- args := append ([]string {"remove" , "-y" }, packages ... )
203+ fmt .Printf ("Removing packages (live system) : %v\n " , pkgs )
204+ args := append ([]string {"remove" , "-y" }, pkgs ... )
182205 if err := runCommand ("apt" , args ... ); err != nil {
183206 fmt .Fprintf (os .Stderr , "Error removing packages: %v\n " , err )
184207 os .Exit (1 )
185208 }
186209}
187210
188211func cleanCmd () {
189- fmt .Println ("Cleaning temporary files ..." )
212+ fmt .Println ("Cleaning apt cache ..." )
190213 if err := runCommand ("apt" , "clean" ); err != nil {
191214 fmt .Fprintf (os .Stderr , "Error cleaning apt cache: %v\n " , err )
192215 }
193- // Optionally delete old snapshots - manual for now
194- fmt .Println ("List snapshots with 'hroot status' and delete manually if needed." )
216+ fmt .Println ("Done. Delete old snapshots manually with 'btrfs subvolume delete /<name>'" )
195217}
196218
197219func statusCmd () {
198- out , err := exec .Command ("btrfs" , "subvolume" , "list" , "-p" , "/" ).CombinedOutput ()
220+ output , err := exec .Command ("btrfs" , "subvolume" , "list" , "-p" , "/" ).CombinedOutput ()
199221 if err != nil {
200222 fmt .Fprintf (os .Stderr , "Error listing subvolumes: %v\n " , err )
201223 os .Exit (1 )
202224 }
203- lines := strings . Split ( string ( out ), " \n " )
225+
204226 fmt .Println ("Available snapshots:" )
227+ lines := strings .Split (string (output ), "\n " )
205228 for _ , line := range lines {
206- if strings .Contains (line , rootSubvolume ) {
229+ if strings .Contains (line , rootSubvolume ) || strings . Contains ( line , updateSnapshot ) {
207230 fields := strings .Fields (line )
208- if len (fields ) > 8 {
231+ if len (fields ) >= 9 {
209232 id := fields [1 ]
210- path := fields [8 ]
211- if strings .HasPrefix (path , rootSubvolume ) {
212- fmt .Printf ("ID: %s, Path: %s\n " , id , path )
213- }
233+ path := fields [len (fields )- 1 ]
234+ fmt .Printf (" ID %-6s → %s\n " , id , path )
214235 }
215236 }
216237 }
217238
218- // Get default
219- defOut , defErr := exec .Command ("btrfs" , "subvolume" , "get-default" , "/" ).CombinedOutput ()
220- if defErr == nil {
221- defFields := strings .Fields (string (defOut ))
222- if len (defFields ) > 1 {
223- defID := defFields [1 ]
224- fmt .Printf ("Current default ID: %s\n " , defID )
225- }
239+ defOutput , err := exec .Command ("btrfs" , "subvolume" , "get-default" , "/" ).CombinedOutput ()
240+ if err == nil {
241+ fmt .Printf ("\n Current default: %s" , defOutput )
226242 }
227243}
0 commit comments