mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
WebDAV: add support for X-OC-Mtime header
it is used by Nextcloud compatible clients to set the modification time Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
6ebe7691db
commit
2290137868
6 changed files with 102 additions and 4 deletions
|
@ -32,4 +32,6 @@ SFTPGo has a minimal implementation for [Dead Properties](https://tools.ietf.org
|
|||
|
||||
To properly support dead properties we need a design decision, probably the best solution is to write a plugin and store them inside a supported data provider.
|
||||
|
||||
SFTPGo also supports setting the modification time using the `X-OC-Mtime` header. Nextcloud compatible clients set this header.
|
||||
|
||||
If you find any other quirks or problems please let us know opening a GitHub issue, thank you!
|
||||
|
|
2
go.mod
2
go.mod
|
@ -96,7 +96,7 @@ require (
|
|||
github.com/aws/smithy-go v1.13.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
|
|
3
go.sum
3
go.sum
|
@ -318,8 +318,9 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n
|
|||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
|
|
|
@ -19,7 +19,9 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/webdav"
|
||||
|
||||
|
@ -36,6 +38,18 @@ type Connection struct {
|
|||
request *http.Request
|
||||
}
|
||||
|
||||
func (c *Connection) getModificationTime() time.Time {
|
||||
if c.request == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if val := c.request.Header.Get("X-OC-Mtime"); val != "" {
|
||||
if unixTime, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
return time.Unix(unixTime, 0)
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// GetClientVersion returns the connected client's version.
|
||||
func (c *Connection) GetClientVersion() string {
|
||||
if c.request != nil {
|
||||
|
@ -85,7 +99,19 @@ func (c *Connection) Rename(ctx context.Context, oldName, newName string) error
|
|||
oldName = util.CleanPath(oldName)
|
||||
newName = util.CleanPath(newName)
|
||||
|
||||
return c.BaseConnection.Rename(oldName, newName)
|
||||
err := c.BaseConnection.Rename(oldName, newName)
|
||||
if err == nil {
|
||||
if mtime := c.getModificationTime(); !mtime.IsZero() {
|
||||
attrs := &common.StatAttributes{
|
||||
Flags: common.StatAttrTimes,
|
||||
Atime: mtime,
|
||||
Mtime: mtime,
|
||||
}
|
||||
setStatErr := c.SetStat(newName, attrs)
|
||||
c.Log(logger.LevelDebug, "mtime header found for %q, value: %s, err: %v", newName, mtime, setStatErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Stat returns a FileInfo describing the named file/directory, or an error,
|
||||
|
@ -202,6 +228,8 @@ func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, re
|
|||
|
||||
baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,
|
||||
common.TransferUpload, 0, 0, maxWriteSize, 0, true, fs, transferQuota)
|
||||
mtime := c.getModificationTime()
|
||||
baseTransfer.SetTimes(resolvedPath, mtime, mtime)
|
||||
|
||||
return newWebDavFile(baseTransfer, w, nil), nil
|
||||
}
|
||||
|
@ -260,6 +288,8 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
|||
|
||||
baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,
|
||||
common.TransferUpload, 0, initialSize, maxWriteSize, truncatedSize, false, fs, transferQuota)
|
||||
mtime := c.getModificationTime()
|
||||
baseTransfer.SetTimes(resolvedPath, mtime, mtime)
|
||||
|
||||
return newWebDavFile(baseTransfer, w, nil), nil
|
||||
}
|
||||
|
|
|
@ -470,6 +470,7 @@ func TestConnWithNilRequest(t *testing.T) {
|
|||
assert.Empty(t, c.GetClientVersion())
|
||||
assert.Empty(t, c.GetCommand())
|
||||
assert.Empty(t, c.GetRemoteAddress())
|
||||
assert.True(t, c.getModificationTime().IsZero())
|
||||
}
|
||||
|
||||
func TestResolvePathErrors(t *testing.T) {
|
||||
|
|
|
@ -245,6 +245,7 @@ XMf5HU3ThYqYn3bYypZZ8nQ7BXVh4LqGNqG29wR4v6l+dLO6odXnLzfApGD9e+d4
|
|||
tlsClient1Username = "client1"
|
||||
tlsClient2Username = "client2"
|
||||
emptyPwdPlaceholder = "empty"
|
||||
ocMtimeHeader = "X-OC-Mtime"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -820,6 +821,66 @@ func TestLockAfterDelete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMtimeHeader(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
client := getWebDavClient(user, false, nil)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
|
||||
false, testFileSize, client, dataprovider.KeyValue{Key: ocMtimeHeader, Value: "1668879480"})
|
||||
assert.NoError(t, err)
|
||||
// check the modification time
|
||||
info, err := client.Stat(testFileName)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, time.Unix(1668879480, 0).UTC(), info.ModTime().UTC())
|
||||
}
|
||||
// test on overwrite
|
||||
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
|
||||
false, testFileSize, client, dataprovider.KeyValue{Key: ocMtimeHeader, Value: "1667879480"})
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(testFileName)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, time.Unix(1667879480, 0).UTC(), info.ModTime().UTC())
|
||||
}
|
||||
// invalid time will be silently ignored and the time set to now
|
||||
err = uploadFileWithRawClient(testFilePath, testFileName, user.Username, defaultPassword,
|
||||
false, testFileSize, client, dataprovider.KeyValue{Key: ocMtimeHeader, Value: "not unix time"})
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(testFileName)
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotEqual(t, time.Unix(1667879480, 0).UTC(), info.ModTime().UTC())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("MOVE", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Overwrite", "T")
|
||||
req.Header.Set("Destination", path.Join("/", testFileName+"rename"))
|
||||
req.Header.Set(ocMtimeHeader, "1666779480")
|
||||
req.SetBasicAuth(u.Username, u.Password)
|
||||
httpClient := httpclient.GetHTTPClient()
|
||||
resp, err := httpClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
assert.NoError(t, err)
|
||||
// check the modification time
|
||||
info, err = client.Stat(testFileName + "rename")
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, time.Unix(1666779480, 0).UTC(), info.ModTime().UTC())
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRenameWithLock(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
|
@ -2989,7 +3050,7 @@ func checkFileSize(remoteDestPath string, expectedSize int64, client *gowebdav.C
|
|||
}
|
||||
|
||||
func uploadFileWithRawClient(localSourcePath string, remoteDestPath string, username, password string,
|
||||
useTLS bool, expectedSize int64, client *gowebdav.Client,
|
||||
useTLS bool, expectedSize int64, client *gowebdav.Client, headers ...dataprovider.KeyValue,
|
||||
) error {
|
||||
srcFile, err := os.Open(localSourcePath)
|
||||
if err != nil {
|
||||
|
@ -3012,6 +3073,9 @@ func uploadFileWithRawClient(localSourcePath string, remoteDestPath string, user
|
|||
return err
|
||||
}
|
||||
req.SetBasicAuth(username, password)
|
||||
for _, kv := range headers {
|
||||
req.Header.Set(kv.Key, kv.Value)
|
||||
}
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
if tlsConfig != nil {
|
||||
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
|
Loading…
Reference in a new issue