-
Notifications
You must be signed in to change notification settings - Fork 574
Add download and deployment #999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
57c75fc
df638c0
93ecff0
3bf98a9
f04d20a
c13d567
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| package internal | ||
|
|
||
| import ( | ||
| "archive/zip" | ||
| "bytes" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/pocketbase/pocketbase" | ||
| "github.com/pocketbase/pocketbase/core" | ||
| ) | ||
|
|
||
| // buildSiteZip creates an in-memory ZIP archive of the generated site files | ||
| // stored under sites/<host>/… and returns the buffer. | ||
| func buildSiteZip(pb *pocketbase.PocketBase, host string) (*bytes.Buffer, error) { | ||
| fs, err := pb.NewFilesystem() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| buf := &bytes.Buffer{} | ||
| zw := zip.NewWriter(buf) | ||
| prefix := "sites/" + host + "/" | ||
|
|
||
| var walk func(string) error | ||
| walk = func(pfx string) error { | ||
| entries, err := fs.List(pfx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| for _, ent := range entries { | ||
| if ent.IsDir { | ||
| if err := walk(strings.TrimSuffix(ent.Key, "/") + "/"); err != nil { | ||
| return err | ||
| } | ||
| continue | ||
| } | ||
| r, err := fs.GetReader(ent.Key) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| f, err := zw.Create(strings.TrimPrefix(ent.Key, prefix)) | ||
| if err != nil { | ||
| r.Close() | ||
| return err | ||
| } | ||
| if _, err := io.Copy(f, r); err != nil { | ||
| r.Close() | ||
| return err | ||
| } | ||
| r.Close() | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| if err := walk(prefix); err != nil { | ||
| zw.Close() | ||
| return nil, err | ||
| } | ||
| zw.Close() | ||
|
|
||
| return buf, nil | ||
| } | ||
|
|
||
| // RegisterExportEndpoints registers endpoints used for downloading and | ||
| // deploying a generated site. The handlers expect the caller to be | ||
| // authenticated and authorised to view the site in question (same checks as | ||
| // generate.go). | ||
| func RegisterExportEndpoints(pb *pocketbase.PocketBase) error { | ||
| pb.OnServe().BindFunc(func(se *core.ServeEvent) error { | ||
| // download the generated files as a zip archive | ||
| se.Router.GET("/api/palacms/site-zip/{siteId}", func(e *core.RequestEvent) error { | ||
| site, err := pb.FindRecordById("sites", e.Request.PathValue("siteId")) | ||
| if err != nil { | ||
| return e.NotFoundError("site not found", err) | ||
| } | ||
|
|
||
| info, _ := e.RequestInfo() | ||
| canAccess, _ := e.App.CanAccessRecord(site, info, site.Collection().ViewRule) | ||
| if !canAccess { | ||
| return e.ForbiddenError("", nil) | ||
| } | ||
|
|
||
| buf, err := buildSiteZip(pb, site.GetString("host")) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| e.Response.Header().Set("Content-Type", "application/zip") | ||
| e.Response.Header().Set("Content-Disposition", | ||
| fmt.Sprintf(`attachment; filename="%s.zip"`, site.GetString("host"))) | ||
| e.Response.Write(buf.Bytes()) | ||
| return nil | ||
| }) | ||
|
|
||
| // trigger a deployment to Cloudflare Pages | ||
| se.Router.POST("/api/palacms/deploy/{siteId}", func(e *core.RequestEvent) error { | ||
| site, err := pb.FindRecordById("sites", e.Request.PathValue("siteId")) | ||
| if err != nil { | ||
| return e.NotFoundError("site not found", err) | ||
| } | ||
|
|
||
| info, _ := e.RequestInfo() | ||
| canAccess, _ := e.App.CanAccessRecord(site, info, site.Collection().UpdateRule) | ||
| if !canAccess { | ||
| return e.ForbiddenError("", nil) | ||
| } | ||
|
|
||
| // allow per-site overrides stored on the site record; fall back to | ||
| // environment variables for a global default. | ||
| acct := site.GetString("cfAccountId") | ||
| if acct == "" { | ||
| acct = os.Getenv("CF_ACCOUNT_ID") | ||
| } | ||
| proj := site.GetString("cfProjectName") | ||
| if proj == "" { | ||
| proj = os.Getenv("CF_PROJECT_NAME") | ||
| } | ||
| token := site.GetString("cfApiToken") | ||
| if token == "" { | ||
| token = os.Getenv("CF_API_TOKEN") | ||
| } | ||
| if acct == "" || proj == "" || token == "" { | ||
| return e.InternalServerError("cloudflare credentials missing", nil) | ||
| } | ||
|
|
||
| // The files are already generated and stored locally in PocketBase's data directory. | ||
| siteDir := filepath.Join(pb.DataDir(), "storage", "sites", site.GetString("host")) | ||
|
|
||
| cmd := exec.Command("npx", "-y", "wrangler@latest", "pages", "deploy", siteDir, "--project-name", proj, "--branch", "main") | ||
| cmd.Env = append(os.Environ(), | ||
| "CLOUDFLARE_ACCOUNT_ID="+acct, | ||
| "CLOUDFLARE_API_TOKEN="+token, | ||
| "WRANGLER_SEND_METRICS=false", | ||
| ) | ||
|
|
||
| out, err := cmd.CombinedOutput() | ||
| if err != nil { | ||
|
Amerlander marked this conversation as resolved.
Outdated
|
||
| return e.InternalServerError("cloudflare deployment failed: "+string(out), err) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| return e.JSON(200, map[string]any{ | ||
| "status": "deployed", | ||
| "url": "https://" + proj + ".pages.dev", | ||
| }) | ||
| }) | ||
|
|
||
| return se.Next() | ||
| }) | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package migrations | ||
|
|
||
| import ( | ||
| "github.com/pocketbase/pocketbase/core" | ||
| m "github.com/pocketbase/pocketbase/migrations" | ||
| ) | ||
|
|
||
| func init() { | ||
| m.Register( | ||
| func(app core.App) error { | ||
| collection, err := app.FindCollectionByNameOrId("sites") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Add the Cloudflare deployment fields | ||
| collection.Fields.Add( | ||
| &core.TextField{ | ||
| Name: "cfAccountId", | ||
| Required: false, | ||
| }, | ||
| &core.TextField{ | ||
| Name: "cfProjectName", | ||
| Required: false, | ||
| }, | ||
| &core.TextField{ | ||
| Name: "cfApiToken", | ||
| Required: false, | ||
| Hidden: true, | ||
| }, | ||
|
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cd /root/repo && git ls-files | grep -E "(migrations|pocketbase)" | head -20Repository: palacms/palacms Length of output: 119 🌐 Web query:
💡 Result: In PocketBase, PocketBase’s maintainer confirms there is no built-in encryption for database (record) content at rest; if you need it, you must implement it yourself (encrypt specific field values in your app) or use external tooling (e.g., encrypt the DB file / disk, SQLite encryption extensions, etc.). [3] PocketBase does have an optional “settings encryption” feature (for app settings like SMTP password / S3 credentials), enabled via Sources Citations:
🌐 Web query:
💡 Result: PocketBase “Hidden” fields: what they do (and don’t)
Field/value encryption support in PocketBase
Security implications / best practice
Note on auth collections
Citations:
Do not store Cloudflare API tokens as plain-text site fields.
Store API tokens in a separate server-side secret store or encrypted-at-rest secret table with strict access controls. Alternatively, consider storing only a non-sensitive site identifier and retrieving the actual token from a secure external service at runtime. 🤖 Prompt for AI Agents |
||
| ) | ||
|
|
||
| return app.Save(collection) | ||
| }, | ||
| func(app core.App) error { | ||
| collection, err := app.FindCollectionByNameOrId("sites") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Remove the Cloudflare deployment fields | ||
| collection.Fields.RemoveByName("cfAccountId") | ||
| collection.Fields.RemoveByName("cfProjectName") | ||
| collection.Fields.RemoveByName("cfApiToken") | ||
|
|
||
| return app.Save(collection) | ||
| }, | ||
| ) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.