Skip to content

Commit

Permalink
feat: embed PHP apps into the FrankenPHP binary
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas committed Dec 1, 2023
1 parent c9bf994 commit d87444b
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 8 deletions.
22 changes: 18 additions & 4 deletions caddy/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"net/http"
"path/filepath"
"strconv"

"github.com/caddyserver/caddy/v2"
Expand Down Expand Up @@ -93,8 +94,6 @@ func (f *FrankenPHPApp) Start() error {
}
}

logger.Info("FrankenPHP started 🐘", zap.String("php_version", frankenphp.Version().Version))

return nil
}

Expand Down Expand Up @@ -169,6 +168,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if wc.FileName == "" {
return errors.New(`The "file" argument must be specified`)
}

if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
}
}

f.Workers = append(f.Workers, wc)
Expand All @@ -193,7 +196,7 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
}

type FrankenPHPModule struct {
// Root sets the root folder to the site. Default: `root` directive.
// Root sets the root folder to the site. Default: `root` directive, or the path of the embed app it exists.
Root string `json:"root,omitempty"`
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
SplitPath []string `json:"split_path,omitempty"`
Expand All @@ -217,7 +220,12 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
f.logger = ctx.Logger(f)

if f.Root == "" {
f.Root = "{http.vars.root}"
if frankenphp.EmbeddedDocumentRoot == "" {
f.Root = "{http.vars.root}"
} else {
f.Root = frankenphp.EmbeddedDocumentRoot
f.ResolveRootSymlink = false
}
}
if len(f.SplitPath) == 0 {
f.SplitPath = []string{".php"}
Expand Down Expand Up @@ -425,6 +433,12 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
// unmarshaler can read it from the start
dispenser.Reset()

if phpsrv.Root == "" && frankenphp.EmbeddedAppPath != "" {
phpsrv.Root = frankenphp.EmbeddedDocumentRoot
fsrv.Root = frankenphp.EmbeddedDocumentRoot
phpsrv.ResolveRootSymlink = false
}

// set up a route list that we'll append to
routes := caddyhttp.RouteList{}

Expand Down
5 changes: 5 additions & 0 deletions caddy/php-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/certmagic"
"github.com/dunglas/frankenphp"
"go.uber.org/zap"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -60,6 +61,10 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) {
debug := fs.Bool("debug")
compress := !fs.Bool("no-compress")

if root == "" && frankenphp.EmbeddedDocumentRoot != "" {
root = frankenphp.EmbeddedDocumentRoot
}

const indexFile = "index.php"
extensions := []string{"php"}
tryFiles := []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
Expand Down
98 changes: 98 additions & 0 deletions embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package frankenphp

import (
"crypto/md5"
"embed"
_ "embed"
"encoding/hex"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)

const embedDir = "embed"
const publicDir = "public"

// The path of the embedded PHP application (empty if none)
var EmbeddedAppPath string

// The path of the document root of the embedded PHP application (empty if none)
var EmbeddedDocumentRoot string

//go:embed all:embed
var embeddedApp embed.FS

func init() {
_, err := embeddedApp.ReadDir(embedDir + "/" + publicDir)
if err != nil {
// no embedded app
return
}

e, err := os.Executable()
if err != nil {
panic(err)
}

e, err = filepath.EvalSymlinks(e)
if err != nil {
panic(err)
}

// TODO: use XXH3 instead of MD5
h := md5.Sum([]byte(e))
appPath := fmt.Sprintf("%sfrankenphp_%s/", os.TempDir(), hex.EncodeToString(h[:]))

entries, err := embeddedApp.ReadDir(embedDir)
if err != nil {
panic(err)
}

if err := os.RemoveAll(appPath); err != nil {
panic(err)
}
if err := copyToDisk(appPath, embedDir, entries); err != nil {
os.RemoveAll(appPath)
panic(err)
}

EmbeddedAppPath = appPath
EmbeddedDocumentRoot = appPath + publicDir
}

func copyToDisk(appPath string, currentDir string, entries []fs.DirEntry) error {
if err := os.Mkdir(appPath+strings.TrimPrefix(currentDir, embedDir), 0700); err != nil {
return err
}

for _, entry := range entries {
name := entry.Name()

if entry.IsDir() {
entries, err := embeddedApp.ReadDir(currentDir + "/" + name)
if err != nil {
return err
}

if err := copyToDisk(appPath, currentDir+"/"+name, entries); err != nil {
return err
}

continue
}

data, err := embeddedApp.ReadFile(currentDir + "/" + name)
if err != nil {
return err
}

f := appPath + "/" + strings.TrimPrefix(currentDir, embedDir) + "/" + name
if err := os.WriteFile(f, data, 0500); err != nil {
return err
}
}

return nil
}
Empty file added embed/.gitignore
Empty file.
20 changes: 16 additions & 4 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,13 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques
}

if fc.documentRoot == "" {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
if EmbeddedAppPath != "" {
fc.documentRoot = EmbeddedAppPath
} else {
var err error
if fc.documentRoot, err = os.Getwd(); err != nil {
return nil, err
}
}
}

Expand Down Expand Up @@ -315,7 +319,10 @@ func Init(options ...Option) error {
return err
}

logger.Debug("FrankenPHP started")
logger.Info("FrankenPHP started 🐘", zap.String("php_version", Version().Version))
if EmbeddedAppPath != "" {
logger.Info("embedded PHP app 📦", zap.String("path", EmbeddedAppPath), zap.String("document_root", EmbeddedDocumentRoot))
}

return nil
}
Expand All @@ -330,6 +337,11 @@ func Shutdown() {
// Always reset the WaitGroup to ensure we're in a clean state
workersReadyWG = sync.WaitGroup{}

// Remove the installed app
if EmbeddedAppPath != "" {
os.RemoveAll(EmbeddedAppPath)
}

logger.Debug("FrankenPHP shut down")
}

Expand Down

0 comments on commit d87444b

Please sign in to comment.