Skip to content

Commit

Permalink
feat: normalize docroot usage in ddev config (ddev#6962)
Browse files Browse the repository at this point in the history
  • Loading branch information
stasadev authored Feb 11, 2025
1 parent 0662a3c commit 30fb117
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 36 deletions.
7 changes: 3 additions & 4 deletions cmd/ddev/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,13 @@ func handleMainConfigArgs(cmd *cobra.Command, _ []string, app *ddevapp.DdevApp)
util.Failed(err.Error())
}

// Ensure that the docroot exists
if docrootRelPathArg != "" {
if cmd.Flags().Changed("docroot") {
app.Docroot = docrootRelPathArg
// Ensure that the docroot exists
if err = app.CreateDocroot(); err != nil {
util.Failed("Could not create docroot at %s: %v", app.Docroot, err)
}
util.Success("Created docroot directory at %s", app.GetAbsDocroot(false))
} else if !cmd.Flags().Changed("docroot") {
} else {
app.Docroot = ddevapp.DiscoverDefaultDocroot(app)
}

Expand Down
77 changes: 77 additions & 0 deletions cmd/ddev/cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,83 @@ func TestConfigSetValues(t *testing.T) {
assert.Equal(webEnv, app.WebEnvironment[2])
}

// TestConfigCreateDocroot sets the docroot, then confirms that the
// value have been correctly written to the config file and docroot is created.
func TestConfigCreateDocroot(t *testing.T) {
assert := asrt.New(t)

projectName := strings.ToLower(t.Name())
origDir, _ := os.Getwd()
_, _ = exec.RunHostCommand(DdevBin, "stop", "--unlist", projectName)

// Create a temporary directory and switch to it.
tmpDir := testcommon.CreateTmpDir(t.Name())
_ = os.Chdir(tmpDir)

var err error

t.Cleanup(func() {
err = os.Chdir(origDir)
assert.NoError(err)
out, err := exec.RunHostCommand(DdevBin, "delete", "-Oy", projectName)
assert.NoError(err, "output=%s", out)
_ = os.RemoveAll(tmpDir)
})

_ = os.Chdir(tmpDir)

configFile := filepath.Join(tmpDir, ".ddev", "config.yaml")
require.NoError(t, err, "Unable to read '%s'", configFile)

// test docroot locations
testMatrix := []struct {
description string
input string
expected string
error string
}{
{"empty docroot", "", "", ""},
{"dot docroot", ".", "", ""},
{"fail for outside approot", "../somewhere-else", "", "is outside the project root"},
{"fail for absolute path", "//test", "", "must be relative"},
{"dot with slash docroot", "./", "", ""},
{"dot with slash and dir docroot", "./test", "test", ""},
{"subdir docroot", "test/dir", "test/dir", ""},
{"dir docroot", "test", "test", ""},
{"dot with slash and subdir docroot", "./test/dir", "test/dir", ""},
}

for _, tc := range testMatrix {
t.Run(tc.description, func(t *testing.T) {
args := []string{
"config",
"--docroot", tc.input,
}

out, err := exec.RunHostCommand(DdevBin, args...)
if tc.error != "" {
require.Error(t, err)
require.Contains(t, out, tc.error)
return
}
require.NoError(t, err)

configContents, err := os.ReadFile(configFile)
require.NoError(t, err, "Unable to read %s: %v", configFile, err)

app := &ddevapp.DdevApp{}
err = yaml.Unmarshal(configContents, app)
require.NoError(t, err, "Could not unmarshal %s: %v", configFile, err)

require.Equal(t, tc.expected, app.Docroot)

// Confirm that the docroot is created
require.DirExists(t, filepath.Join(tmpDir, tc.expected))
})
}

}

// TestConfigInvalidProjectname tests to make sure that invalid projectnames
// are not accepted and valid names are accepted.
func TestConfigInvalidProjectname(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion docs/content/developers/quickstart-maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ In general:
1. Add a link to the upstream installation or "Getting Started" web page, so people can know where the instructions are coming from.
2. Use `mkdir my-<projecttype>-site && cd my-<projecttype>-site` as the opener. (There are places like Magento 2 where the project name must be used later in the recipe, in those cases, use an environment variable, like `PROJECT_NAME=my-<projecttype>-site`.)
3. Composer-based recipes are preferable, unless the project does not use or prefer composer.
4. If your project type does not yet appear in the DDEV documentation, your PR should add the name to the [.spellcheckwordlist.txt](https://github.com/ddev/ddev/blob/main/.spellcheckwordlist.txt) so it can pass the spellcheck test.
4. If your project type does not yet appear in the DDEV documentation, your PR should add the name to the [.spellcheckwordlist.txt](https://github.com/ddev/ddev/blob/main/.spellcheckwordlist.txt) so it can pass the spell check test.
5. If your project installation requires providing an administrative username and/or password, make sure to indicate clearly in the instructions what it is.
6. If your project type includes folders that accept public files (such as images), for example, `public/media`, make sure to add them to the [config](../users/configuration/config.md#upload_dirs) command:

Expand Down
47 changes: 33 additions & 14 deletions pkg/ddevapp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,34 @@ func AvailablePHPDocrootLocations() []string {
}
}

// CreateDocroot normalizes the docroot path and creates it for DDEV app if it doesn't exist
func (app *DdevApp) CreateDocroot() error {
if app.Docroot == "" {
return nil
}
if filepath.IsAbs(app.Docroot) {
return fmt.Errorf("docroot %s must be relative", app.Docroot)
}
docrootAbsPath := app.GetAbsDocroot(false)
// If user provided something like "./some/path", filepath.Rel will convert it to "some/path"
relPath, err := filepath.Rel(app.GetAbsAppRoot(false), docrootAbsPath)
if err != nil || strings.HasPrefix(relPath, "..") {
return fmt.Errorf("docroot %s is outside the project root", app.Docroot)
}
if relPath == "." {
relPath = ""
}
// Normalize docroot
app.Docroot = util.WindowsPathToCygwinPath(relPath)
if !fileutil.IsDirectory(docrootAbsPath) {
if err := os.MkdirAll(docrootAbsPath, 0755); err != nil {
return err
}
util.Success("Created docroot at %s", docrootAbsPath)
}
return nil
}

// DiscoverDefaultDocroot returns the default docroot directory.
func DiscoverDefaultDocroot(app *DdevApp) string {
// Provide use the app.Docroot as the default docroot option.
Expand All @@ -1505,31 +1533,22 @@ func (app *DdevApp) docrootPrompt() error {

// Determine the document root.
output.UserOut.Printf("\nThe docroot is the directory from which your site is served.\nThis is a relative path from your project root at %s\n", app.AppRoot)
output.UserOut.Printf("Leave docroot empty (hit <RETURN>) to use the location shown in parentheses.\nOr specify a custom path if your index.php is in a different directory.\n")
output.UserOut.Printf("Leave docroot empty (hit <RETURN>) to use the location shown in parentheses.\nOr specify a custom path if your index.php is in a different directory.\nOr use '.' (a dot) to explicitly set it to the project root.\n")
var docrootPrompt = "Docroot Location"
var defaultDocroot = DiscoverDefaultDocroot(app)
// If there is a default docroot, display it in the prompt.
if defaultDocroot != "" {
docrootPrompt = fmt.Sprintf("%s (%s)", docrootPrompt, defaultDocroot)
} else if cd, _ := os.Getwd(); cd == filepath.Join(app.AppRoot, defaultDocroot) {
// Preserve the case where the docroot is the current directory
docrootPrompt = fmt.Sprintf("%s (current directory)", docrootPrompt)
} else {
// Explicitly state 'project root' when in a subdirectory
docrootPrompt = fmt.Sprintf("%s (project root)", docrootPrompt)
}

fmt.Print(docrootPrompt + ": ")
app.Docroot = util.GetInput(defaultDocroot)

// Ensure the docroot exists. If it doesn't, prompt the user to verify they entered it correctly.
fullPath := filepath.Join(app.AppRoot, app.Docroot)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
if err = os.MkdirAll(fullPath, 0755); err != nil {
return fmt.Errorf("unable to create docroot: %v", err)
}
app.Docroot = util.GetQuotedInput(defaultDocroot)

util.Success("Created docroot at %s.", fullPath)
// Ensure that the docroot exists
if err := app.CreateDocroot(); err != nil {
return fmt.Errorf("unable to create docroot at %s: %v", app.Docroot, err)
}

return nil
Expand Down
16 changes: 0 additions & 16 deletions pkg/ddevapp/ddevapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,22 +489,6 @@ func (app DdevApp) GetAbsDocroot(inContainer bool) string {
return filepath.Join(app.GetAbsAppRoot(false), app.GetDocroot())
}

// CreateDocroot creates the docroot directory for DDEV app if it doesn't exist
func (app DdevApp) CreateDocroot() error {
if app.Docroot == "" {
return nil
}
docrootAbsPath, err := filepath.Abs(app.Docroot)
if err != nil {
return err
}
if !fileutil.IsDirectory(docrootAbsPath) {
err := os.MkdirAll(docrootAbsPath, 0755)
return err
}
return nil
}

// GetAbsAppRoot returns the absolute path to the project root on the host or if
// inContainer is set to true in the container.
func (app DdevApp) GetAbsAppRoot(inContainer bool) string {
Expand Down
2 changes: 1 addition & 1 deletion pkg/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func ArrayToReadableOutput(slice []string) (response string, err error) {
// Sadly, if we have a Windows drive name, it has to be converted from C:/ to //c for Win10Home/Docker toolbox
func WindowsPathToCygwinPath(windowsPath string) string {
windowsPath = filepath.ToSlash(windowsPath)
if string(windowsPath[1]) == ":" {
if len(windowsPath) >= 2 && string(windowsPath[1]) == ":" {
drive := strings.ToLower(string(windowsPath[0]))
windowsPath = "/" + drive + windowsPath[2:]
}
Expand Down

0 comments on commit 30fb117

Please sign in to comment.