mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
WebDAV: Bound sync download size to the originals limit
This commit is contained in:
parent
487b0c0ec3
commit
0f2de0fc15
3 changed files with 90 additions and 7 deletions
|
|
@ -23,12 +23,25 @@ import (
|
|||
|
||||
// Client represents a webdav client.
|
||||
type Client struct {
|
||||
client *webdav.Client
|
||||
ctx context.Context
|
||||
endpoint *url.URL
|
||||
timeout time.Duration
|
||||
mkdir map[string]bool
|
||||
cidrs []*net.IPNet
|
||||
client *webdav.Client
|
||||
ctx context.Context
|
||||
endpoint *url.URL
|
||||
timeout time.Duration
|
||||
mkdir map[string]bool
|
||||
cidrs []*net.IPNet
|
||||
downloadLimit int64
|
||||
}
|
||||
|
||||
// SetDownloadLimit bounds the size of a single downloaded file in bytes; a value
|
||||
// of zero or less leaves downloads unbounded. The remote endpoint is a separate
|
||||
// trust domain, so this caps how much one response can write to local storage —
|
||||
// files above the configured originals limit are rejected by the indexer anyway.
|
||||
func (c *Client) SetDownloadLimit(maxBytes int64) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.downloadLimit = maxBytes
|
||||
}
|
||||
|
||||
// clientUrl returns the validated server url including username and password, if specified.
|
||||
|
|
@ -466,7 +479,21 @@ func (c *Client) Download(src, dest string, force bool) (err error) {
|
|||
return fmt.Errorf("webdav: failed to create %s", clean.Log(path.Base(dest)))
|
||||
}
|
||||
|
||||
if _, err = f.ReadFrom(reader); err != nil {
|
||||
if c.downloadLimit > 0 {
|
||||
// Read one byte past the limit so an exact-size overflow is detected
|
||||
// instead of being silently truncated into a corrupt local file.
|
||||
if n, copyErr := io.Copy(f, io.LimitReader(reader, c.downloadLimit+1)); copyErr != nil {
|
||||
err = copyErr
|
||||
} else if n > c.downloadLimit {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(dest)
|
||||
return fmt.Errorf("webdav: %s exceeds the maximum size of %d bytes", clean.Log(path.Base(dest)), c.downloadLimit)
|
||||
}
|
||||
} else {
|
||||
_, err = f.ReadFrom(reader)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
log.Errorf("webdav: %s", clean.Error(err))
|
||||
return fmt.Errorf("webdav: failed writing to %s", clean.Log(path.Base(dest)))
|
||||
|
|
|
|||
|
|
@ -640,3 +640,55 @@ func TestClient_DownloadDir(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_DownloadLimit(t *testing.T) {
|
||||
const bodySize = 2048
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
//nolint:gosec // test fixture writes a locally generated payload only
|
||||
_, _ = w.Write(make([]byte, bodySize))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
newClient := func(t *testing.T) *Client {
|
||||
t.Helper()
|
||||
c, err := NewClient(server.URL+"/", "", "", TimeoutLow, "")
|
||||
require.NoError(t, err)
|
||||
return c
|
||||
}
|
||||
|
||||
t.Run("OverLimitRejected", func(t *testing.T) {
|
||||
client := newClient(t)
|
||||
client.SetDownloadLimit(1024)
|
||||
dest := filepath.Join(t.TempDir(), "big.bin")
|
||||
err := client.Download("/big.bin", dest, false)
|
||||
require.Error(t, err)
|
||||
// The oversized partial file must not be left behind.
|
||||
assert.False(t, fs.FileExists(dest))
|
||||
})
|
||||
t.Run("UnderLimitAccepted", func(t *testing.T) {
|
||||
client := newClient(t)
|
||||
client.SetDownloadLimit(4096)
|
||||
dest := filepath.Join(t.TempDir(), "ok.bin")
|
||||
err := client.Download("/ok.bin", dest, false)
|
||||
require.NoError(t, err)
|
||||
info, statErr := os.Stat(dest)
|
||||
require.NoError(t, statErr)
|
||||
assert.Equal(t, int64(bodySize), info.Size())
|
||||
})
|
||||
t.Run("ZeroLimitUnbounded", func(t *testing.T) {
|
||||
client := newClient(t)
|
||||
client.SetDownloadLimit(-1)
|
||||
dest := filepath.Join(t.TempDir(), "unbounded.bin")
|
||||
err := client.Download("/unbounded.bin", dest, false)
|
||||
require.NoError(t, err)
|
||||
info, statErr := os.Stat(dest)
|
||||
require.NoError(t, statErr)
|
||||
assert.Equal(t, int64(bodySize), info.Size())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@ func (w *Sync) download(a entity.Service) (complete bool, err error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
// Bound each download to the originals size limit so a remote sync target
|
||||
// cannot overrun local storage with a single oversized file.
|
||||
client.SetDownloadLimit(w.conf.OriginalsLimitBytes())
|
||||
|
||||
var baseDir string
|
||||
|
||||
if a.SyncFilenames {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue