phphost: find the site's folder below the login folder (any hosting)

domains/<site>/public_html, www/<site>, sites/<site>/htdocs...: one is taken, several are offered with full paths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
p1neappleXpress
2026-10-01 00:57:40 +03:00
co-authored by Claude Sonnet 5.5
parent a327d0164d
commit 0f9babd5f0
3 changed files with 99 additions and 2 deletions
+2 -1
View File
@@ -51,7 +51,8 @@ All notable changes to the OpenFlux core. Format loosely follows
8 MB downloads run, and a 4 MB upload takes 33 s instead of timing out.
- Own node without a server: `provision/phphost` puts the PHP exit on any web
host over FTP and checks that it runs. `probe` finds the web folder and
host over FTP and checks that it runs. `probe` finds the web folder (also
a level or two down: `domains/<site>/public_html`, `www/<site>`) and
whether it is writable, `deploy` uploads the bundle embedded in the core
(`deploy/phpbox`, with the link parser as WebAssembly), keeps the token of an
earlier install and checks file sizes, `check` asks the site (passing the
+58 -1
View File
@@ -222,7 +222,22 @@ func probe(s *session, t FTP) (*Probe, error) {
p.Dir, found = "", true // the login folder is served
}
if !found {
return p, fail(CodeNoWebRoot, strings.Join(dirs, ","), "phphost: cannot tell which folder the site is served from", nil)
// Other hosts keep the site a level or two down: domains/example.com/public_html,
// www/example.com, sites/example.com/htdocs. One such folder is taken as the answer;
// several are offered, with their paths, for the user to choose.
deeper := findDeeper(s, dirs)
p.Candidates = deeper
if len(deeper) == 1 {
p.Dir, found = deeper[0], true
}
}
if !found {
cands := p.Candidates
if len(cands) == 0 {
cands = dirs
}
p.Candidates = cands
return p, fail(CodeNoWebRoot, strings.Join(cands, ","), "phphost: cannot tell which folder the site is served from", nil)
}
}
// An earlier install: keep its token.
@@ -243,6 +258,48 @@ func probe(s *session, t FTP) (*Probe, error) {
return p, nil
}
// findDeeper looks one and two levels below the login folder for folders that
// look like a site's web root: top/x where x is a usual web root name, and
// top/x/root for a top folder that holds several sites (domains, sites, www, ...).
func findDeeper(s *session, top []string) []string {
isRoot := map[string]bool{}
for _, n := range webRootNames {
isRoot[n] = true
}
var found []string
limit := 0
for _, d := range top {
if limit++; limit > 16 || strings.HasPrefix(d, ".") {
continue
}
entries, err := s.c.List(d)
if err != nil {
continue
}
for _, e := range entries {
if e.Type != ftp.EntryTypeFolder || e.Name == "." || e.Name == ".." {
continue
}
if isRoot[e.Name] {
found = append(found, path.Join(d, e.Name))
continue
}
// a site folder (example.com) holding its own web root
sub, err := s.c.List(path.Join(d, e.Name))
if err != nil {
continue
}
for _, x := range sub {
if x.Type == ftp.EntryTypeFolder && isRoot[x.Name] {
found = append(found, path.Join(d, e.Name, x.Name))
}
}
}
}
sort.Strings(found)
return found
}
func join(dir, name string) string {
if dir == "" {
return name
+39
View File
@@ -180,3 +180,42 @@ func TestRemoveDeletesOnlyWhatWasUploaded(t *testing.T) {
t.Error("the user's own file was touched")
}
}
// Other hosts keep the site deeper than the login folder.
func TestProbeFindsAWebRootBelowTheLoginFolder(t *testing.T) {
// domains/example.com/public_html: one site, taken as the answer.
one := newFakeFTP(t, "u", "pw", "domains", "domains/example.com", "domains/example.com/public_html", "logs")
p, err := ProbeHost(ctx(t), one.target())
if err != nil {
t.Fatal(err)
}
if p.Dir != "domains/example.com/public_html" || !p.Writable {
t.Errorf("probe = %+v", p)
}
// It deploys there.
if _, err := Deploy(ctx(t), one.target(), "", nil); err != nil {
t.Fatal(err)
}
if _, ok := one.files["/domains/example.com/public_html/config.php"]; !ok {
t.Error("the node was not uploaded into the site's folder")
}
// Two sites: the user chooses, and the choices carry the full paths.
two := newFakeFTP(t, "u", "pw", "domains", "domains/a.com", "domains/a.com/public_html", "domains/b.org", "domains/b.org/public_html")
_, err = ProbeHost(ctx(t), two.target())
var e *Error
if !errors.As(err, &e) || e.Code != CodeNoWebRoot || e.Param != "domains/a.com/public_html,domains/b.org/public_html" {
t.Fatalf("two sites: %v", err)
}
chosen := two.target()
chosen.Dir = "domains/b.org/public_html"
if p, err := ProbeHost(ctx(t), chosen); err != nil || p.Dir != "domains/b.org/public_html" || !p.Writable {
t.Errorf("chosen: %+v %v", p, err)
}
// www/<site>: a usual name one level down.
www := newFakeFTP(t, "u", "pw", "sites", "sites/shop", "sites/shop/www")
if p, err := ProbeHost(ctx(t), www.target()); err != nil || p.Dir != "sites/shop/www" {
t.Errorf("sites/shop/www: %+v %v", p, err)
}
}