-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve copying of PHP and legacy CLI files
- Check the files on each run - Try to avoid partial writes - Move temporary directory from /tmp to the home directory
- Loading branch information
1 parent
38ff2e3
commit f2d3fd0
Showing
12 changed files
with
411 additions
and
184 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package config | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
"syscall" | ||
) | ||
|
||
// TempDir returns the path to a user-specific temporary directory, suitable for caches. | ||
// | ||
// It creates the temporary directory if it does not already exist. | ||
// | ||
// The directory can be specified in the {ENV_PREFIX}TMP environment variable. | ||
// | ||
// This does not use os.TempDir, as on Linux/Unix systems that usually returns a | ||
// global /tmp directory, which could conflict with other users. It also does not | ||
// use os.MkdirTemp, as the CLI usually needs a stable (not random) directory | ||
// path. It therefore uses os.UserCacheDir which in turn will use XDG_CACHE_HOME | ||
// or the home directory. | ||
func (c *Config) TempDir() (string, error) { | ||
if c.tempDir != "" { | ||
return c.tempDir, nil | ||
} | ||
d := os.Getenv(c.Application.EnvPrefix + "TMP") | ||
if d == "" { | ||
ucd, err := os.UserCacheDir() | ||
if err != nil { | ||
return "", err | ||
} | ||
d = ucd | ||
} | ||
|
||
// Windows already has a user-specific temporary directory. | ||
if runtime.GOOS == "windows" { | ||
osTemp := os.TempDir() | ||
if strings.HasPrefix(osTemp, d) { | ||
d = osTemp | ||
} | ||
} | ||
|
||
path := filepath.Join(d, c.Application.TempSubDir) | ||
|
||
// If the subdirectory cannot be created due to a read-only filesystem, fall back to /tmp. | ||
if err := os.MkdirAll(path, 0o700); err != nil { | ||
if !errors.Is(err, syscall.EROFS) { | ||
return "", err | ||
} | ||
path = filepath.Join(os.TempDir(), c.Application.TempSubDir) | ||
if err := os.MkdirAll(path, 0o700); err != nil { | ||
return "", err | ||
} | ||
} | ||
c.tempDir = path | ||
|
||
return path, nil | ||
} | ||
|
||
// WritableUserDir returns the path to a writable user-level directory. | ||
// Deprecated: unless backwards compatibility is desired, TempDir is preferable. | ||
func (c *Config) WritableUserDir() (string, error) { | ||
if c.writableUserDir != "" { | ||
return c.writableUserDir, nil | ||
} | ||
hd, err := c.HomeDir() | ||
if err != nil { | ||
return "", err | ||
} | ||
path := filepath.Join(hd, c.Application.WritableUserDir) | ||
if err := os.MkdirAll(path, 0o700); err != nil { | ||
return "", err | ||
} | ||
c.writableUserDir = path | ||
|
||
return path, nil | ||
} | ||
|
||
// HomeDir returns the user's home directory, which can be overridden with the {ENV_PREFIX}HOME variable. | ||
func (c *Config) HomeDir() (string, error) { | ||
d := os.Getenv(c.Application.EnvPrefix + "HOME") | ||
if d != "" { | ||
return d, nil | ||
} | ||
return os.UserHomeDir() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package file | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"io" | ||
"io/fs" | ||
"os" | ||
) | ||
|
||
// WriteIfNeeded writes data to a destination file, only if the file does not exist or if it was partially written. | ||
// To save time, it only checks that the file size is correct, and then matches the end of its contents (up to 32KB). | ||
func WriteIfNeeded(destFilename string, source []byte, perm os.FileMode) error { | ||
matches, err := probablyMatches(destFilename, source, 32*1024) | ||
if err != nil || matches { | ||
return err | ||
} | ||
return Write(destFilename, source, perm) | ||
} | ||
|
||
// Write creates or overwrites a file, somewhat atomically, using a temporary file next to it. | ||
func Write(path string, content []byte, fileMode fs.FileMode) error { | ||
tmpFile := path + ".tmp" | ||
if err := os.WriteFile(tmpFile, content, fileMode); err != nil { | ||
return err | ||
} | ||
|
||
return os.Rename(tmpFile, path) | ||
} | ||
|
||
// probablyMatches checks if a file exists and matches the end of source data (up to checkSize bytes). | ||
func probablyMatches(filename string, data []byte, checkSize int) (bool, error) { | ||
f, err := os.Open(filename) | ||
if err != nil { | ||
if errors.Is(err, fs.ErrNotExist) { | ||
return false, nil | ||
} | ||
return false, err | ||
} | ||
defer f.Close() | ||
|
||
fi, err := f.Stat() | ||
if err != nil { | ||
return false, err | ||
} | ||
if fi.Size() != int64(len(data)) { | ||
return false, nil | ||
} | ||
|
||
buf := make([]byte, min(checkSize, len(data))) | ||
offset := max(0, len(data)-checkSize) | ||
n, err := f.ReadAt(buf, int64(offset)) | ||
if err != nil && err != io.EOF { | ||
return false, err | ||
} | ||
|
||
return bytes.Equal(data[offset:], buf[:n]), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package file | ||
|
||
import ( | ||
"bytes" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestWriteIfNeeded(t *testing.T) { | ||
largeContentLength := 128 * 1024 | ||
largeContent := make([]byte, largeContentLength) | ||
largeContent[0] = 'f' | ||
largeContent[largeContentLength-2] = 'o' | ||
largeContent[largeContentLength-1] = 'o' | ||
|
||
largeContent2 := make([]byte, largeContentLength) | ||
largeContent2[0] = 'b' | ||
largeContent2[largeContentLength-2] = 'a' | ||
largeContent2[largeContentLength-1] = 'r' | ||
|
||
assert.Equal(t, len(largeContent), len(largeContent2)) | ||
|
||
cases := []struct { | ||
name string | ||
initialData []byte | ||
sourceData []byte | ||
expectWrite bool | ||
}{ | ||
{"File does not exist", nil, []byte("new data"), true}, | ||
{"File matches source", []byte("same data"), []byte("same data"), false}, | ||
{"File content differs", []byte("old data"), []byte("new data"), true}, | ||
{"Larger file content differs", largeContent, largeContent2, true}, | ||
{"Larger file content matches", largeContent, largeContent, false}, | ||
{"File size differs", []byte("short"), []byte("much longer data"), true}, | ||
{"Empty source", []byte("existing data"), []byte{}, true}, | ||
} | ||
|
||
tmpDir := t.TempDir() | ||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
destFile := filepath.Join(tmpDir, "testfile") | ||
|
||
if c.initialData != nil { | ||
require.NoError(t, os.WriteFile(destFile, c.initialData, 0o600)) | ||
time.Sleep(time.Millisecond * 5) | ||
} | ||
|
||
var modTimeBefore time.Time | ||
stat, err := os.Stat(destFile) | ||
if c.initialData == nil { | ||
require.True(t, os.IsNotExist(err)) | ||
} else { | ||
require.NoError(t, err) | ||
modTimeBefore = stat.ModTime() | ||
} | ||
|
||
err = WriteIfNeeded(destFile, c.sourceData, 0o600) | ||
require.NoError(t, err) | ||
|
||
statAfter, err := os.Stat(destFile) | ||
require.NoError(t, err) | ||
modTimeAfter := statAfter.ModTime() | ||
|
||
if c.expectWrite { | ||
assert.Greater(t, modTimeAfter.Truncate(time.Millisecond), modTimeBefore.Truncate(time.Millisecond)) | ||
} else { | ||
assert.Equal(t, modTimeBefore.Truncate(time.Millisecond), modTimeAfter.Truncate(time.Millisecond)) | ||
} | ||
|
||
data, err := os.ReadFile(destFile) | ||
require.NoError(t, err) | ||
|
||
assert.True(t, bytes.Equal(data, c.sourceData)) | ||
}) | ||
} | ||
} |
Oops, something went wrong.