1package handlers
2
3import (
4 "context"
5 "fmt"
6 "os/exec"
7 "path/filepath"
8
9 "github.com/bnema/archup/internal/application/dto"
10 "github.com/bnema/archup/internal/config"
11 "github.com/bnema/archup/internal/domain/ports"
12)
13
14// BootstrapHandler handles downloading/cloning install files
15type BootstrapHandler struct {
16 fs ports.FileSystem
17 httpClient ports.HTTPClient
18 logger ports.Logger
19 repoURL string
20 rawURL string
21 branch string
22}
23
24// NewBootstrapHandler creates a new bootstrap handler
25func NewBootstrapHandler(fs ports.FileSystem, httpClient ports.HTTPClient, logger ports.Logger, repoURL, rawURL, branch string) *BootstrapHandler {
26 return &BootstrapHandler{
27 fs: fs,
28 httpClient: httpClient,
29 logger: logger,
30 repoURL: repoURL,
31 rawURL: rawURL,
32 branch: branch,
33 }
34}
35
36// Handle prepares install files by cloning repo or downloading
37func (h *BootstrapHandler) Handle(ctx context.Context) (*dto.BootstrapResult, error) {
38 h.logger.Info("Starting bootstrap")
39
40 result := &dto.BootstrapResult{
41 Success: false,
42 InstallDir: config.DefaultInstallDir,
43 ErrorDetail: "",
44 }
45
46 // Create install directory
47 if err := h.fs.MkdirAll(config.DefaultInstallDir, 0755); err != nil {
48 result.ErrorDetail = fmt.Sprintf("Failed to create install directory: %v", err)
49 return result, err
50 }
51
52 // Try to clone repo first
53 if err := h.cloneRepo(ctx); err == nil {
54 if err := h.copyRepoFiles(); err != nil {
55 h.logger.Warn("Failed to copy repo files", "error", err)
56 result.ErrorDetail = fmt.Sprintf("Failed to copy repo files: %v", err)
57 return result, err
58 }
59 h.logger.Info("Bootstrap complete via repo clone")
60 result.Success = true
61 result.Method = "clone"
62 return result, nil
63 } else {
64 h.logger.Warn("Repo clone failed, falling back to downloads", "error", err)
65 }
66
67 // Fallback: download files
68 if err := h.downloadFiles(ctx); err != nil {
69 result.ErrorDetail = fmt.Sprintf("Failed to download files: %v", err)
70 return result, err
71 }
72
73 h.logger.Info("Bootstrap complete via downloads")
74 result.Success = true
75 result.Method = "download"
76 return result, nil
77}
78
79// cloneRepo clones the archup repository at the configured git ref.
80func (h *BootstrapHandler) cloneRepo(ctx context.Context) error {
81 repoDir := config.DefaultInstallRepoDir
82
83 // Check if already cloned
84 if _, err := h.fs.Stat(filepath.Join(repoDir, ".git")); err == nil {
85 h.logger.Info("Repository already cloned")
86 return nil
87 }
88
89 branch := h.branch
90 if branch == "" {
91 branch = "main"
92 }
93
94 h.logger.Info("Cloning repository", "url", h.repoURL, "branch", branch)
95
96 cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", branch, h.repoURL, repoDir)
97 if output, err := cmd.CombinedOutput(); err != nil {
98 return fmt.Errorf("git clone failed: %w (output: %s)", err, string(output))
99 }
100
101 return nil
102}
103
104// copyRepoFiles copies install files from cloned repo to install directory
105func (h *BootstrapHandler) copyRepoFiles() error {
106 repoDir := config.DefaultInstallRepoDir
107 installDir := config.DefaultInstallDir
108
109 // Files to copy from repo
110 files := []struct {
111 src string
112 dest string
113 }{
114 {"install/base.packages", "base.packages"},
115 {"install/extra.packages", "extra.packages"},
116 {"install/configs/limine.conf.template", "configs/limine.conf.template"},
117 {"install/configs/chaotic-aur.conf", "configs/chaotic-aur.conf"},
118 {"install/configs/shell/shell", "configs/shell/shell"},
119 {"install/configs/shell/starship.toml", "configs/shell/starship.toml"},
120 {"install/configs/shell/init", "configs/shell/init"},
121 {"install/configs/shell/aliases", "configs/shell/aliases"},
122 {"install/configs/shell/envs", "configs/shell/envs"},
123 {"install/configs/shell/rc", "configs/shell/rc"},
124 {"install/configs/shell/bashrc", "configs/shell/bashrc"},
125 }
126
127 for _, f := range files {
128 srcPath := filepath.Join(repoDir, f.src)
129 destPath := filepath.Join(installDir, f.dest)
130
131 // Create destination directory
132 if err := h.fs.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
133 return fmt.Errorf("failed to create directory for %s: %w", f.dest, err)
134 }
135
136 // Read and copy file
137 content, err := h.fs.ReadFile(srcPath)
138 if err != nil {
139 h.logger.Warn("File not found in repo", "file", f.src)
140 continue // Skip missing files
141 }
142
143 if err := h.fs.WriteFile(destPath, content, 0644); err != nil {
144 return fmt.Errorf("failed to write %s: %w", f.dest, err)
145 }
146
147 h.logger.Info("Copied file", "file", f.dest)
148 }
149
150 return nil
151}
152
153// downloadFiles downloads install files from GitHub
154func (h *BootstrapHandler) downloadFiles(_ context.Context) error {
155 files := []struct {
156 urlPath string
157 destPath string
158 }{
159 {"install/base.packages", "base.packages"},
160 {"install/extra.packages", "extra.packages"},
161 {"install/configs/limine.conf.template", "configs/limine.conf.template"},
162 }
163
164 for _, f := range files {
165 url := fmt.Sprintf("%s/%s", h.rawURL, f.urlPath)
166 destPath := filepath.Join(config.DefaultInstallDir, f.destPath)
167
168 // Create destination directory
169 if err := h.fs.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
170 return fmt.Errorf("failed to create directory for %s: %w", f.destPath, err)
171 }
172
173 h.logger.Info("Downloading file", "url", url)
174
175 resp, err := h.httpClient.Get(url)
176 if err != nil {
177 return fmt.Errorf("failed to download %s: %w", f.urlPath, err)
178 }
179
180 if resp.StatusCode() < 200 || resp.StatusCode() > 299 {
181 if closeErr := resp.Close(); closeErr != nil {
182 h.logger.Warn("Failed to close HTTP response", "error", closeErr)
183 }
184 return fmt.Errorf("unexpected status %d from %s", resp.StatusCode(), url)
185 }
186
187 if err := h.fs.WriteFile(destPath, resp.Body(), 0644); err != nil {
188 if closeErr := resp.Close(); closeErr != nil {
189 h.logger.Warn("Failed to close HTTP response", "error", closeErr)
190 }
191 return fmt.Errorf("failed to write %s: %w", f.destPath, err)
192 }
193
194 if closeErr := resp.Close(); closeErr != nil {
195 h.logger.Warn("Failed to close HTTP response", "error", closeErr)
196 }
197 }
198
199 return nil
200}