From 53a5e2e45a3386eae80fc6d17d1337a57c1c8976 Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 14 May 2026 23:16:46 +0200 Subject: [PATCH] static --- README.md | 3 +- internal/app/deploy_service.go | 38 +++++++++++ internal/app/integrations.go | 118 ++++++++------------------------- 3 files changed, 67 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 1693fac..1ced087 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,8 @@ Route shape: 1. Clone repo to `~/projects//repo` if missing. 2. Otherwise `fetch origin main` and `reset --hard origin/main`. 3. Build binary to `~/projects//bin/app`. - 4. Restart the project user service via `systemctl --user restart`. + 4. Sync static assets from `~/projects//repo/static` to `~/projects//data/static`. + 5. Restart the project user service via `systemctl --user restart`. Notes: - `WEBHOOK_BASE_URL` should be set to an externally reachable base URL so Forgejo can call webhook endpoints. diff --git a/internal/app/deploy_service.go b/internal/app/deploy_service.go index f3f331c..e69fd91 100644 --- a/internal/app/deploy_service.go +++ b/internal/app/deploy_service.go @@ -113,6 +113,9 @@ func (a *App) buildProjectBinary(p Project) error { if err := ensureOwnedByUnixUser(binaryPath, p.UnixUser, 0o755); err != nil { return err } + if err := a.syncStaticAssetsForProject(p); err != nil { + return err + } return nil } @@ -186,3 +189,38 @@ func runGoBuildForUser(unixUser, repoDir, binaryPath string) error { "go", "-C", repoDir, "build", "-buildvcs=false", "-o", binaryPath, ".", ) } + +func (a *App) syncStaticAssetsForProject(p Project) error { + projectDir, err := a.projectDirFor(p) + if err != nil { + return err + } + repoStatic := filepath.Join(projectDir, "repo", "static") + dataStatic := filepath.Join(projectDir, "data", "static") + + if _, err := os.Stat(repoStatic); err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if err := os.MkdirAll(dataStatic, 0o755); err != nil { + return err + } + if err := ensureOwnedByUnixUser(dataStatic, p.UnixUser, 0o755); err != nil { + return err + } + + // Preferred path: fast deterministic sync with deletes. + if err := runCmd("sudo", "-n", "-u", p.UnixUser, "rsync", "-a", "--delete", repoStatic+"/", dataStatic+"/"); err == nil { + return nil + } + // Fallback if rsync is unavailable: recreate target and copy. + if err := runCmd("sudo", "-n", "-u", p.UnixUser, "rm", "-rf", dataStatic); err != nil { + return err + } + if err := runCmd("sudo", "-n", "-u", p.UnixUser, "mkdir", "-p", dataStatic); err != nil { + return err + } + return runCmd("sudo", "-n", "-u", p.UnixUser, "cp", "-a", repoStatic+"/.", dataStatic+"/") +} diff --git a/internal/app/integrations.go b/internal/app/integrations.go index 8d3302a..787f124 100644 --- a/internal/app/integrations.go +++ b/internal/app/integrations.go @@ -72,70 +72,12 @@ func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error { if err != nil { return err } - indexes := findCaddyRouteIndexesByID(cfg, a.cfg.CaddyServerID, route["@id"].(string)) - if len(indexes) > 0 { - // Update first matching route in place. - if err := a.putCaddyRouteAtIndex(ctx, indexes[0], route); err != nil { - return err - } - // Remove stale duplicates from highest index to lowest. - for i := len(indexes) - 1; i >= 1; i-- { - if err := a.deleteCaddyRouteAtIndex(ctx, indexes[i]); err != nil { - return err - } - } - return nil - } - - if err := a.appendCaddyRoute(ctx, route); err != nil { - // If duplicate race happened, retry once through dedupe/update path. - if strings.Contains(err.Error(), "duplicate ID") { - cfg2, ferr := a.fetchCaddyConfig(ctx) - if ferr != nil { - return err - } - indexes2 := findCaddyRouteIndexesByID(cfg2, a.cfg.CaddyServerID, route["@id"].(string)) - if len(indexes2) == 0 { - return err - } - if uerr := a.putCaddyRouteAtIndex(ctx, indexes2[0], route); uerr != nil { - return uerr - } - for i := len(indexes2) - 1; i >= 1; i-- { - if derr := a.deleteCaddyRouteAtIndex(ctx, indexes2[i]); derr != nil { - return derr - } - } - return nil - } + newRoutes, err := mergedDedupedRoutes(cfg, a.cfg.CaddyServerID, route) + if err != nil { return err } - return nil -} - -func (a *App) appendCaddyRoute(ctx context.Context, route map[string]any) error { - body, _ := json.Marshal(route) + body, _ := json.Marshal(newRoutes) url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(body))) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("caddy request failed: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode >= 300 { - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("caddy error: status=%d body=%s", resp.StatusCode, string(respBody)) - } - return nil -} - -func (a *App) putCaddyRouteAtIndex(ctx context.Context, idx int, route map[string]any) error { - body, _ := json.Marshal(route) - url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes/%d", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID, idx) req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader(string(body))) if err != nil { return err @@ -143,66 +85,60 @@ func (a *App) putCaddyRouteAtIndex(ctx context.Context, idx int, route map[strin req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { - return fmt.Errorf("caddy update request failed: %w", err) + return fmt.Errorf("caddy routes rewrite failed: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("caddy update error: status=%d body=%s", resp.StatusCode, string(respBody)) + return fmt.Errorf("caddy routes rewrite error: status=%d body=%s", resp.StatusCode, string(respBody)) } return nil } -func (a *App) deleteCaddyRouteAtIndex(ctx context.Context, idx int) error { - url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes/%d", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID, idx) - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) - if err != nil { - return err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("caddy delete request failed: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode >= 300 { - respBody, _ := io.ReadAll(resp.Body) - return fmt.Errorf("caddy delete error: status=%d body=%s", resp.StatusCode, string(respBody)) - } - return nil -} - -func findCaddyRouteIndexesByID(cfg map[string]any, serverID, routeID string) []int { - var out []int +func mergedDedupedRoutes(cfg map[string]any, serverID string, desired map[string]any) ([]any, error) { apps, ok := cfg["apps"].(map[string]any) if !ok { - return out + return nil, errors.New("caddy config missing apps") } httpApp, ok := apps["http"].(map[string]any) if !ok { - return out + return nil, errors.New("caddy config missing apps.http") } servers, ok := httpApp["servers"].(map[string]any) if !ok { - return out + return nil, errors.New("caddy config missing apps.http.servers") } server, ok := servers[serverID].(map[string]any) if !ok { - return out + return nil, fmt.Errorf("caddy config missing server %q", serverID) } routes, ok := server["routes"].([]any) if !ok { - return out + routes = []any{} } + desiredID, _ := desired["@id"].(string) + var out []any + seenDesired := false for i, r := range routes { + _ = i rm, ok := r.(map[string]any) if !ok { + out = append(out, r) continue } - if rid, ok := rm["@id"].(string); ok && rid == routeID { - out = append(out, i) + if rid, ok := rm["@id"].(string); ok && rid == desiredID { + if !seenDesired { + out = append(out, desired) + seenDesired = true + } + continue } + out = append(out, r) } - return out + if !seenDesired { + out = append(out, desired) + } + return out, nil } func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {