// main is the central application entrypoint. package main import ( "bytes" "context" "errors" "fmt" "io" "io/ioutil" "math/rand" "net/http" "os" "os/exec" "path/filepath" "slices" "varia.zone/snackbar/components" "github.com/a-h/templ" "github.com/codeclysm/extract/v3" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-git/go-git/v5" ) // App is the application state. type App struct { ctx context.Context } // NewApp creates a new application state. func NewApp() *App { return &App{} } func (a *App) startup(ctx context.Context) { sites, err := existingSites() if err != nil { // TODO(d1): is this the correct approach for error handling? panic(fmt.Sprintf("unable to read sites from %s", SitesDir)) } randPort := rand.Intn(1400-1300) + 1300 for _, siteName := range sites { cmd := exec.Command(HugoBinPath, "serve", "--port", fmt.Sprintf("%v", randPort), "--buildDrafts", "--source", filepath.Join(SitesDir, siteName), "--theme", "nostyleplease") if err := cmd.Run(); err != nil { // TODO(d1): is this the correct approach for error handling? panic(fmt.Sprintf("unable to serve site: %s", err)) } } } func (a *App) shutdown(ctx context.Context) { fmt.Print("SHUTTING DOWN!") } func ensureDataDir() error { paths := []string{LocalDir, HomeDir, HugoDir, SitesDir} for _, fpath := range paths { if _, err := os.Stat(fpath); err != nil && os.IsNotExist(err) { if err := os.Mkdir(fpath, 0764); err != nil { return err } } } return nil } func ensureHugoBin() error { if _, err := os.Stat(HugoBinPath); err == nil { return nil } else if errors.Is(err, os.ErrNotExist) { if err := httpGetFile(HugoTarPath, HugoReleaseURL); err != nil { return err } } fpath, err := ioutil.ReadFile(HugoTarPath) if err != nil { return err } if err := extract.Gz(context.TODO(), bytes.NewBuffer(fpath), HugoDir, nil); err != nil { return err } return nil } func httpGetFile(filepath, url string) error { out, err := os.Create(filepath) if err != nil { return err } defer out.Close() resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return err } _, err = io.Copy(out, resp.Body) if err != nil { return err } return nil } func existingSites() ([]string, error) { var sites []string siteNameFiles, err := os.ReadDir(SitesDir) if err != nil { return sites, err } for _, siteNameFile := range siteNameFiles { sites = append(sites, siteNameFile.Name()) } return sites, nil } func initialise(w http.ResponseWriter, r *http.Request) { if err := ensureDataDir(); err != nil { w.Write([]byte(fmt.Sprintf("unable to create data directory: %s", err))) } if err := ensureHugoBin(); err != nil { w.Write([]byte(fmt.Sprintf("unable to download hugo binary: %s", err))) } sites, err := existingSites() if err != nil { w.Write([]byte(fmt.Sprintf("unable to list existing sites: %s", err))) } templ.Handler(components.Homepage(sites)).ServeHTTP(w, r) } func hugoNewSite(w http.ResponseWriter, r *http.Request) { siteName := r.FormValue("site-name") newSiteDir := filepath.Join(SitesDir, siteName) cmd := exec.Command(HugoBinPath, "new", "site", newSiteDir) if err := cmd.Run(); err != nil { w.Write([]byte(fmt.Sprintf("unable to create new site: %s", err))) } // TODO(d1): choose default theme if _, err := git.PlainClone(filepath.Join(newSiteDir, "themes", "nostyleplease"), false, &git.CloneOptions{ URL: "https://github.com/g-hanwen/hugo-theme-nostyleplease", }); err != nil { w.Write([]byte(fmt.Sprintf("unable to clone 'nostyleplease' theme: %s", err))) } sites, err := existingSites() if err != nil { w.Write([]byte(fmt.Sprintf("unable to list existing sites: %s", err))) } templ.Handler(components.Homepage(sites)).ServeHTTP(w, r) } func siteConfig(w http.ResponseWriter, r *http.Request) { siteName := chi.URLParam(r, "site-name") sites, err := existingSites() if err != nil { w.Write([]byte(fmt.Sprintf("unable to list existing sites: %s", err))) } if !slices.Contains(sites, siteName) { w.Write([]byte(fmt.Sprintf("site '%s' does not exist?", siteName))) } templ.Handler(components.SiteConfig(siteName)).ServeHTTP(w, r) } // NewRouter creates a new web router. func NewRouter() *chi.Mux { router := chi.NewRouter() router.Use(middleware.Logger) router.Use(middleware.Recoverer) router.Get("/init", initialise) router.Post("/hugo/new", hugoNewSite) router.Get("/{site-name}/config", siteConfig) return router }