From 1dad3c86fe463ff3f635095d71a1737caa2c16f0 Mon Sep 17 00:00:00 2001 From: Hadi Chokr Date: Sat, 12 Jul 2025 17:24:44 +0200 Subject: [PATCH] Add unexport and export. Signed-off-by: Hadi Chokr --- src/cmd/export.go | 143 ++++++++++++++++++++++++++++++++++++++++++++ src/cmd/unexport.go | 121 +++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 src/cmd/export.go create mode 100644 src/cmd/unexport.go diff --git a/src/cmd/export.go b/src/cmd/export.go new file mode 100644 index 0000000..c2c3a64 --- /dev/null +++ b/src/cmd/export.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var ( + exportBin string + exportApp string + exportContainer string +) + +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Export binaries or applications from a toolbox container", + RunE: runExport, +} + +func init() { + exportCmd.Flags().StringVar(&exportBin, "bin", "", "Path or name of binary to export") + exportCmd.Flags().StringVar(&exportApp, "app", "", "Path or name of application to export") + exportCmd.Flags().StringVar(&exportContainer, "container", "", "Name of the toolbox container") + rootCmd.AddCommand(exportCmd) +} + +func runExport(cmd *cobra.Command, args []string) error { + if exportBin == "" && exportApp == "" { + return errors.New("must specify either --bin or --app") + } + if exportContainer == "" { + return errors.New("must specify --container") + } + + if exportBin != "" { + return exportBinary(exportBin, exportContainer) + } else if exportApp != "" { + return exportApplication(exportApp, exportContainer) + } + return nil +} + +func exportBinary(binName, containerName string) error { + // Find the binary's full path inside the container + checkCmd := fmt.Sprintf("toolbox run -c %s which %s", containerName, binName) + out, err := exec.Command("sh", "-c", checkCmd).Output() + if err != nil || strings.TrimSpace(string(out)) == "" { + return fmt.Errorf("binary %s not found in container %s", binName, containerName) + } + binPath := strings.TrimSpace(string(out)) + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + exportedBinPath := filepath.Join(homeDir, ".local", "bin", binName) + + script := fmt.Sprintf(`#!/bin/sh + # toolbox_binary + # name: %s + exec toolbox run -c %s %s "$@" + `, containerName, containerName, binPath) + + if err := os.WriteFile(exportedBinPath, []byte(script), 0755); err != nil { + return fmt.Errorf("failed to create wrapper: %v", err) + } + + fmt.Printf("Successfully exported %s from container %s to %s\n", binName, containerName, exportedBinPath) + return nil +} + +func exportApplication(appName, containerName string) error { + // Find the desktop file inside the container + findCmd := fmt.Sprintf("toolbox run -c %s sh -c 'find /usr/share/applications -name \"*%s*.desktop\" | head -1'", containerName, appName) + out, err := exec.Command("sh", "-c", findCmd).Output() + if err != nil || strings.TrimSpace(string(out)) == "" { + return fmt.Errorf("application %s not found in container %s", appName, containerName) + } + desktopFile := strings.TrimSpace(string(out)) + + // Read the desktop file content + catCmd := fmt.Sprintf("toolbox run -c %s cat %s", containerName, desktopFile) + content, err := exec.Command("sh", "-c", catCmd).Output() + if err != nil { + return fmt.Errorf("failed to read desktop file: %v", err) + } + lines := strings.Split(string(content), "\n") + var newLines []string + hasNameTranslations := false + + for _, line := range lines { + if strings.HasPrefix(line, "Exec=") { + execCmd := line[5:] + line = fmt.Sprintf("Exec=toolbox run -c %s %s", containerName, execCmd) + } else if strings.HasPrefix(line, "Name=") { + line = fmt.Sprintf("Name=%s (on %s)", line[5:], containerName) + } else if strings.HasPrefix(line, "Name[") { + hasNameTranslations = true + } else if strings.HasPrefix(line, "GenericName=") { + line = fmt.Sprintf("GenericName=%s (on %s)", line[12:], containerName) + } else if strings.HasPrefix(line, "TryExec=") || line == "DBusActivatable=true" { + continue + } + newLines = append(newLines, line) + } + + if hasNameTranslations { + for i, line := range newLines { + if strings.HasPrefix(line, "Name[") { + lang := line[5:strings.Index(line, "]")] + value := line[strings.Index(line, "=")+1:] + newLines[i] = fmt.Sprintf("Name[%s]=%s (on %s)", lang, value, containerName) + } + } + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + appsPath := filepath.Join(homeDir, ".local", "share", "applications") + exportedPath := filepath.Join(appsPath, filepath.Base(desktopFile)) + exportedPath = strings.TrimSuffix(exportedPath, ".desktop") + "-" + containerName + ".desktop" + + if err := os.MkdirAll(appsPath, 0755); err != nil { + return fmt.Errorf("failed to create applications directory: %v", err) + } + if err := os.WriteFile(exportedPath, []byte(strings.Join(newLines, "\n")), 0644); err != nil { + return fmt.Errorf("failed to create desktop file: %v", err) + } + + // Update desktop database + exec.Command("update-desktop-database", appsPath).Run() + + fmt.Printf("Successfully exported %s from container %s to %s\n", appName, containerName, exportedPath) + return nil +} diff --git a/src/cmd/unexport.go b/src/cmd/unexport.go new file mode 100644 index 0000000..00ccc71 --- /dev/null +++ b/src/cmd/unexport.go @@ -0,0 +1,121 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var ( + unexportContainer string + unexportBin string + unexportApp string + unexportAll bool +) + +var unexportCmd = &cobra.Command{ + Use: "unexport", + Short: "Remove exported binaries and applications for a specific toolbox container", + RunE: runUnexport, +} + +func init() { + unexportCmd.Flags().StringVar(&unexportContainer, "container", "", "Name of the toolbox container") + unexportCmd.Flags().StringVar(&unexportBin, "bin", "", "Name of the exported binary to remove") + unexportCmd.Flags().StringVar(&unexportApp, "app", "", "Name of the exported application to remove") + unexportCmd.Flags().BoolVar(&unexportAll, "all", false, "Remove all exported binaries and applications for the container") + rootCmd.AddCommand(unexportCmd) +} + +func runUnexport(cmd *cobra.Command, args []string) error { + if unexportContainer == "" { + return errors.New("must specify --container") + } + + if !unexportAll && unexportBin == "" && unexportApp == "" { + return errors.New("must specify --bin, --app, or --all") + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + binDir := filepath.Join(homeDir, ".local", "bin") + appsDir := filepath.Join(homeDir, ".local", "share", "applications") + + removedBins := []string{} + removedApps := []string{} + + if unexportBin != "" { + path := filepath.Join(binDir, unexportBin) + if fileContainsContainer(path, unexportContainer) { + if err := os.Remove(path); err == nil { + removedBins = append(removedBins, path) + } + } + } + + if unexportApp != "" { + // Remove .desktop file that matches app name and container + matches, _ := filepath.Glob(filepath.Join(appsDir, fmt.Sprintf("*%s-%s.desktop", unexportApp, unexportContainer))) + for _, path := range matches { + if err := os.Remove(path); err == nil { + removedApps = append(removedApps, path) + } + } + } + + if unexportAll { + // Remove all binaries for this container in .local/bin + binFiles, _ := os.ReadDir(binDir) + for _, f := range binFiles { + if f.IsDir() { + continue + } + path := filepath.Join(binDir, f.Name()) + if fileContainsContainer(path, unexportContainer) { + if err := os.Remove(path); err == nil { + removedBins = append(removedBins, path) + } + } + } + + // Remove all .desktop files for this container in .local/share/applications + appFiles, _ := os.ReadDir(appsDir) + for _, f := range appFiles { + name := f.Name() + if strings.HasSuffix(name, "-"+unexportContainer+".desktop") { + path := filepath.Join(appsDir, name) + if err := os.Remove(path); err == nil { + removedApps = append(removedApps, path) + } + } + } + } + + fmt.Printf("Removed binaries:\n") + for _, b := range removedBins { + fmt.Printf(" %s\n", b) + } + fmt.Printf("Removed desktop files:\n") + for _, a := range removedApps { + fmt.Printf(" %s\n", a) + } + if len(removedBins) == 0 && len(removedApps) == 0 { + fmt.Println("No exported binaries or desktop files found to remove for container", unexportContainer) + } + return nil +} + +// fileContainsContainer returns true if the file exists and has a toolbox_binary comment with name: +func fileContainsContainer(path, container string) bool { + content, err := os.ReadFile(path) + if err != nil { + return false + } + return strings.Contains(string(content), "# toolbox_binary") && strings.Contains(string(content), fmt.Sprintf("name: %s", container)) +}