2020-10-23 21:30:45 -04:00
|
|
|
package cache
|
2020-04-09 13:15:01 -04:00
|
|
|
|
|
|
|
|
import (
|
2020-07-24 13:30:27 -04:00
|
|
|
"context"
|
2022-12-18 12:22:12 -05:00
|
|
|
"errors"
|
2020-07-24 13:30:27 -04:00
|
|
|
"io"
|
2020-04-09 13:15:01 -04:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2020-07-24 13:30:27 -04:00
|
|
|
"strings"
|
2020-04-09 13:15:01 -04:00
|
|
|
|
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
2022-12-18 12:22:12 -05:00
|
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
2022-07-26 16:47:16 -04:00
|
|
|
. "github.com/onsi/ginkgo/v2"
|
2020-04-09 13:15:01 -04:00
|
|
|
. "github.com/onsi/gomega"
|
|
|
|
|
)
|
|
|
|
|
|
2020-07-24 15:40:27 -04:00
|
|
|
// Call NewFileCache and wait for it to be ready
|
|
|
|
|
func callNewFileCache(name, cacheSize, cacheFolder string, maxItems int, getReader ReadFunc) *fileCache {
|
2023-01-17 22:04:04 -05:00
|
|
|
fc := NewFileCache(name, cacheSize, cacheFolder, maxItems, getReader).(*fileCache)
|
2023-02-15 21:21:59 -05:00
|
|
|
Eventually(func() bool { return fc.ready.Load() }).Should(BeTrue())
|
2023-01-17 22:04:04 -05:00
|
|
|
return fc
|
2020-07-24 15:40:27 -04:00
|
|
|
}
|
|
|
|
|
|
2020-04-09 13:15:01 -04:00
|
|
|
var _ = Describe("File Caches", func() {
|
|
|
|
|
BeforeEach(func() {
|
2022-12-18 12:22:12 -05:00
|
|
|
tmpDir, _ := os.MkdirTemp("", "file_caches")
|
|
|
|
|
DeferCleanup(func() {
|
|
|
|
|
configtest.SetupConfig()
|
|
|
|
|
_ = os.RemoveAll(tmpDir)
|
|
|
|
|
})
|
refactor(conf): replace eager dir creation with lazy Dir type (#5495)
* feat(conf): add Dir type with lazy directory creation
Introduces the Dir type that wraps a directory path string and defers
os.MkdirAll until the first call to Path() or MustPath(), using sync.Once
to ensure the creation happens exactly once. Implements fmt.Stringer,
encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration.
Includes Ginkgo/Gomega tests covering all methods and error paths.
* refactor(conf): replace eager dir creation with lazy Dir type
Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from
string to Dir. Remove all os.MkdirAll calls from Load() so directories
are created lazily on first Path()/MustPath() call. Artwork folder
creation was already handled at point-of-use in image_upload.go.
Add SnapshotConfig() to conf package for safe test config save/restore
that avoids copying sync.Once inside Dir fields. Fix copy-lock vet
warning in nativeapi/config.go by marshalling pointer instead of value.
* refactor(conf): migrate tests and db init to lazy Dir type
Update all test files to use conf.NewDir() for Dir field assignments.
Ensure DataFolder is created lazily when the database is first opened
in db.Db(). Remove eager directory creation from conf.Load() tests.
* fix(conf): address review findings for Dir type
- Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match
original behavior). Add NewDirWithPerm for PluginsFolder (0700).
- Use Path() instead of MustPath() in db.Prune() to avoid logFatal
from background cron job.
- Panic on marshal/unmarshal errors in SnapshotConfig (test helper).
- Clean up redundant String()/MustPath() calls in plugin manager.
- Remove dead code in dir_test.go.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(conf): add GoString to Dir for clean config dump output
Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path
string instead of internal struct fields (sync.Once, perm, err).
Also add TODO comment to configtest about removing the indirection.
* fix(dir): improve error logging in MustPath method
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(conf): address PR review feedback
- Ensure Plugins.Folder always uses 0700, even when user-configured
(previously only the derived default got restrictive permissions).
- Create LogFile parent directory before opening, so LogFile paths
inside a not-yet-created DataFolder work correctly.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-13 17:44:22 -03:00
|
|
|
conf.Server.CacheFolder = conf.NewDir(tmpDir)
|
2020-04-09 13:15:01 -04:00
|
|
|
})
|
|
|
|
|
|
2020-07-24 12:42:11 -04:00
|
|
|
Describe("NewFileCache", func() {
|
2020-04-09 13:15:01 -04:00
|
|
|
It("creates the cache folder", func() {
|
2020-07-24 15:40:27 -04:00
|
|
|
Expect(callNewFileCache("test", "1k", "test", 0, nil)).ToNot(BeNil())
|
2020-04-09 13:15:01 -04:00
|
|
|
|
refactor(conf): replace eager dir creation with lazy Dir type (#5495)
* feat(conf): add Dir type with lazy directory creation
Introduces the Dir type that wraps a directory path string and defers
os.MkdirAll until the first call to Path() or MustPath(), using sync.Once
to ensure the creation happens exactly once. Implements fmt.Stringer,
encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration.
Includes Ginkgo/Gomega tests covering all methods and error paths.
* refactor(conf): replace eager dir creation with lazy Dir type
Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from
string to Dir. Remove all os.MkdirAll calls from Load() so directories
are created lazily on first Path()/MustPath() call. Artwork folder
creation was already handled at point-of-use in image_upload.go.
Add SnapshotConfig() to conf package for safe test config save/restore
that avoids copying sync.Once inside Dir fields. Fix copy-lock vet
warning in nativeapi/config.go by marshalling pointer instead of value.
* refactor(conf): migrate tests and db init to lazy Dir type
Update all test files to use conf.NewDir() for Dir field assignments.
Ensure DataFolder is created lazily when the database is first opened
in db.Db(). Remove eager directory creation from conf.Load() tests.
* fix(conf): address review findings for Dir type
- Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match
original behavior). Add NewDirWithPerm for PluginsFolder (0700).
- Use Path() instead of MustPath() in db.Prune() to avoid logFatal
from background cron job.
- Panic on marshal/unmarshal errors in SnapshotConfig (test helper).
- Clean up redundant String()/MustPath() calls in plugin manager.
- Remove dead code in dir_test.go.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(conf): add GoString to Dir for clean config dump output
Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path
string instead of internal struct fields (sync.Once, perm, err).
Also add TODO comment to configtest about removing the indirection.
* fix(dir): improve error logging in MustPath method
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(conf): address PR review feedback
- Ensure Plugins.Folder always uses 0700, even when user-configured
(previously only the derived default got restrictive permissions).
- Create LogFile parent directory before opening, so LogFile paths
inside a not-yet-created DataFolder work correctly.
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-13 17:44:22 -03:00
|
|
|
_, err := os.Stat(filepath.Join(conf.Server.CacheFolder.String(), "test"))
|
2020-04-09 13:15:01 -04:00
|
|
|
Expect(os.IsNotExist(err)).To(BeFalse())
|
|
|
|
|
})
|
2020-04-09 13:36:05 -04:00
|
|
|
|
|
|
|
|
It("creates the cache folder with invalid size", func() {
|
2020-07-24 15:40:27 -04:00
|
|
|
fc := callNewFileCache("test", "abc", "test", 0, nil)
|
2020-07-24 12:42:11 -04:00
|
|
|
Expect(fc.cache).ToNot(BeNil())
|
2020-07-26 00:45:33 -04:00
|
|
|
Expect(fc.disabled).To(BeFalse())
|
2020-04-09 13:36:05 -04:00
|
|
|
})
|
2020-04-14 19:15:47 -04:00
|
|
|
|
|
|
|
|
It("returns empty if cache size is '0'", func() {
|
2020-07-24 15:40:27 -04:00
|
|
|
fc := callNewFileCache("test", "0", "test", 0, nil)
|
2020-07-24 12:42:11 -04:00
|
|
|
Expect(fc.cache).To(BeNil())
|
2020-07-26 00:45:33 -04:00
|
|
|
Expect(fc.disabled).To(BeTrue())
|
2020-04-14 19:15:47 -04:00
|
|
|
})
|
2025-05-21 21:48:49 -04:00
|
|
|
|
|
|
|
|
It("reports when cache is disabled", func() {
|
|
|
|
|
fc := callNewFileCache("test", "0", "test", 0, nil)
|
|
|
|
|
Expect(fc.Disabled(context.Background())).To(BeTrue())
|
|
|
|
|
fc = callNewFileCache("test", "1KB", "test", 0, nil)
|
|
|
|
|
Expect(fc.Disabled(context.Background())).To(BeFalse())
|
|
|
|
|
})
|
2020-04-09 13:15:01 -04:00
|
|
|
})
|
2020-07-24 12:42:11 -04:00
|
|
|
|
|
|
|
|
Describe("FileCache", func() {
|
2020-07-24 13:30:27 -04:00
|
|
|
It("caches data if cache is enabled", func() {
|
|
|
|
|
called := false
|
2020-10-24 15:10:27 -04:00
|
|
|
fc := callNewFileCache("test", "1KB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
2020-07-24 13:30:27 -04:00
|
|
|
called = true
|
2020-10-24 15:10:27 -04:00
|
|
|
return strings.NewReader(arg.Key()), nil
|
2020-07-24 13:30:27 -04:00
|
|
|
})
|
|
|
|
|
// First call is a MISS
|
2022-12-18 12:22:12 -05:00
|
|
|
s, err := fc.Get(context.Background(), &testArg{"test"})
|
2020-07-24 13:30:27 -04:00
|
|
|
Expect(err).To(BeNil())
|
2020-07-24 18:46:08 -04:00
|
|
|
Expect(s.Cached).To(BeFalse())
|
2021-07-01 12:07:32 -04:00
|
|
|
Expect(s.Closer).To(BeNil())
|
2021-07-20 20:00:58 -04:00
|
|
|
Expect(io.ReadAll(s)).To(Equal([]byte("test")))
|
2020-07-24 13:30:27 -04:00
|
|
|
|
|
|
|
|
// Second call is a HIT
|
|
|
|
|
called = false
|
2022-12-18 12:22:12 -05:00
|
|
|
s, err = fc.Get(context.Background(), &testArg{"test"})
|
2020-07-24 13:30:27 -04:00
|
|
|
Expect(err).To(BeNil())
|
2021-07-20 20:00:58 -04:00
|
|
|
Expect(io.ReadAll(s)).To(Equal([]byte("test")))
|
2020-07-24 18:46:08 -04:00
|
|
|
Expect(s.Cached).To(BeTrue())
|
2021-07-01 12:07:32 -04:00
|
|
|
Expect(s.Closer).ToNot(BeNil())
|
2020-07-24 13:30:27 -04:00
|
|
|
Expect(called).To(BeFalse())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("does not cache data if cache is disabled", func() {
|
|
|
|
|
called := false
|
2020-10-24 15:10:27 -04:00
|
|
|
fc := callNewFileCache("test", "0", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
2020-07-24 13:30:27 -04:00
|
|
|
called = true
|
2020-10-24 15:10:27 -04:00
|
|
|
return strings.NewReader(arg.Key()), nil
|
2020-07-24 13:30:27 -04:00
|
|
|
})
|
|
|
|
|
// First call is a MISS
|
2022-12-18 12:22:12 -05:00
|
|
|
s, err := fc.Get(context.Background(), &testArg{"test"})
|
2020-07-24 13:30:27 -04:00
|
|
|
Expect(err).To(BeNil())
|
2020-07-24 18:46:08 -04:00
|
|
|
Expect(s.Cached).To(BeFalse())
|
2021-07-20 20:00:58 -04:00
|
|
|
Expect(io.ReadAll(s)).To(Equal([]byte("test")))
|
2020-07-24 12:42:11 -04:00
|
|
|
|
2020-07-24 13:30:27 -04:00
|
|
|
// Second call is also a MISS
|
|
|
|
|
called = false
|
2022-12-18 12:22:12 -05:00
|
|
|
s, err = fc.Get(context.Background(), &testArg{"test"})
|
2020-07-24 13:30:27 -04:00
|
|
|
Expect(err).To(BeNil())
|
2021-07-20 20:00:58 -04:00
|
|
|
Expect(io.ReadAll(s)).To(Equal([]byte("test")))
|
2020-07-24 18:46:08 -04:00
|
|
|
Expect(s.Cached).To(BeFalse())
|
2020-07-24 13:30:27 -04:00
|
|
|
Expect(called).To(BeTrue())
|
|
|
|
|
})
|
2022-12-18 12:22:12 -05:00
|
|
|
|
2026-06-23 22:23:54 -04:00
|
|
|
It("writes a completion marker after a successful cache write", func() {
|
|
|
|
|
fc := callNewFileCache("test", "1KB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
return strings.NewReader("complete-data"), nil
|
|
|
|
|
})
|
|
|
|
|
s, err := fc.Get(context.Background(), &testArg{"markme"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
_, _ = io.ReadAll(s)
|
|
|
|
|
_ = s.Close()
|
|
|
|
|
|
|
|
|
|
dataPath := fcSpreadFS(fc).KeyMapper((&testArg{"markme"}).Key())
|
|
|
|
|
Eventually(func() bool {
|
|
|
|
|
_, statErr := os.Stat(dataPath + ".complete")
|
|
|
|
|
return statErr == nil
|
|
|
|
|
}).Should(BeTrue())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("serves a concurrent reader from an in-progress write and marks complete once", func() {
|
|
|
|
|
pr, pw := io.Pipe()
|
|
|
|
|
fc := callNewFileCache("test", "10MB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
return pr, nil // slow, still-being-produced stream
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// First Get → MISS; the cache starts copying pr into the entry in a goroutine.
|
|
|
|
|
s1, err := fc.Get(context.Background(), &testArg{"live"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
// Write the first chunk so the entry exists with in-flight bytes,
|
|
|
|
|
// but leave the pipe open so the second reader can attach mid-stream.
|
|
|
|
|
// io.Pipe writes block until the cache goroutine reads them, giving us
|
|
|
|
|
// a deterministic happens-before: the entry is live before we call Get again.
|
|
|
|
|
_, err = pw.Write([]byte("hello "))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
// Second Get while the pipe is still open → attaches to the in-progress entry.
|
|
|
|
|
s2, err := fc.Get(context.Background(), &testArg{"live"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
|
|
|
|
|
// Drain both readers concurrently; they race against the producer below.
|
|
|
|
|
ch1 := make(chan []byte, 1)
|
|
|
|
|
ch2 := make(chan []byte, 1)
|
|
|
|
|
go func() { b, _ := io.ReadAll(s1); ch1 <- b }()
|
|
|
|
|
go func() { b, _ := io.ReadAll(s2); ch2 <- b }()
|
|
|
|
|
|
|
|
|
|
// Deliver the rest of the stream and close; both draining goroutines must see it.
|
|
|
|
|
_, err = pw.Write([]byte("world"))
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
Expect(pw.Close()).To(Succeed())
|
|
|
|
|
|
|
|
|
|
Expect(string(<-ch1)).To(Equal("hello world"))
|
|
|
|
|
Expect(string(<-ch2)).To(Equal("hello world"))
|
|
|
|
|
_ = s1.Close()
|
|
|
|
|
_ = s2.Close()
|
|
|
|
|
|
|
|
|
|
// Exactly one completion marker must appear.
|
|
|
|
|
dataPath := fcSpreadFS(fc).KeyMapper((&testArg{"live"}).Key())
|
|
|
|
|
Eventually(func() bool {
|
|
|
|
|
_, e := os.Stat(dataPath + ".complete")
|
|
|
|
|
return e == nil
|
|
|
|
|
}).Should(BeTrue())
|
|
|
|
|
|
|
|
|
|
// Steady-state HIT: full data, Cached flag set.
|
|
|
|
|
s3, err := fc.Get(context.Background(), &testArg{"live"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
got3, _ := io.ReadAll(s3)
|
|
|
|
|
_ = s3.Close()
|
|
|
|
|
Expect(s3.Cached).To(BeTrue())
|
|
|
|
|
Expect(string(got3)).To(Equal("hello world"))
|
|
|
|
|
})
|
|
|
|
|
|
2022-12-18 12:22:12 -05:00
|
|
|
Context("reader errors", func() {
|
|
|
|
|
When("creating a reader fails", func() {
|
|
|
|
|
It("does not cache", func() {
|
|
|
|
|
fc := callNewFileCache("test", "1KB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
return nil, errors.New("failed")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
_, err := fc.Get(context.Background(), &testArg{"test"})
|
|
|
|
|
Expect(err).To(MatchError("failed"))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
When("reader returns error", func() {
|
2025-05-27 19:53:10 -04:00
|
|
|
It("does not cache", func() {
|
2022-12-18 12:22:12 -05:00
|
|
|
fc := callNewFileCache("test", "1KB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
2025-05-27 19:53:10 -04:00
|
|
|
return errFakeReader{errors.New("read failure")}, nil
|
2022-12-18 12:22:12 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
s, err := fc.Get(context.Background(), &testArg{"test"})
|
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
2025-05-27 19:53:10 -04:00
|
|
|
_, _ = io.Copy(io.Discard, s)
|
|
|
|
|
// TODO How to make the fscache reader return the underlying reader error?
|
|
|
|
|
//Expect(err).To(MatchError("read failure"))
|
2022-12-18 12:22:12 -05:00
|
|
|
|
2025-05-27 19:53:10 -04:00
|
|
|
// Data should not be cached (or eventually be removed from cache)
|
2022-12-18 12:22:12 -05:00
|
|
|
Eventually(func() bool {
|
|
|
|
|
s, _ = fc.Get(context.Background(), &testArg{"test"})
|
|
|
|
|
if s != nil {
|
|
|
|
|
return s.Cached
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}).Should(BeFalse())
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-06-23 22:23:54 -04:00
|
|
|
|
|
|
|
|
Context("crash leftover (issue #5636)", func() {
|
|
|
|
|
It("does not serve a partial file left on disk as a complete HIT", func() {
|
|
|
|
|
// First init: empties + writes the migration sentinel.
|
|
|
|
|
fc1 := callNewFileCache("test", "10MB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
return strings.NewReader("UNUSED"), nil
|
|
|
|
|
})
|
|
|
|
|
_ = fc1
|
|
|
|
|
|
|
|
|
|
// Plant a partial file (no marker), simulating a killed process.
|
|
|
|
|
sfs, err := NewSpreadFS(filepath.Join(conf.Server.CacheFolder.String(), "test"), 0755)
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
partialPath := sfs.KeyMapper((&testArg{"track"}).Key())
|
|
|
|
|
Expect(os.MkdirAll(filepath.Dir(partialPath), 0755)).To(Succeed())
|
|
|
|
|
Expect(os.WriteFile(partialPath, []byte("PARTIAL"), 0600)).To(Succeed())
|
|
|
|
|
|
|
|
|
|
// "Restart": a fresh cache over the same folder (sentinel present → strict).
|
|
|
|
|
getReaderCalled := false
|
|
|
|
|
fc2 := callNewFileCache("test", "10MB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
getReaderCalled = true
|
|
|
|
|
return strings.NewReader("FULL-TRANSCODE"), nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
s, err := fc2.Get(context.Background(), &testArg{"track"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
data, _ := io.ReadAll(s)
|
|
|
|
|
_ = s.Close()
|
|
|
|
|
|
|
|
|
|
Expect(getReaderCalled).To(BeTrue()) // re-transcoded, not served stale
|
|
|
|
|
Expect(string(data)).To(Equal("FULL-TRANSCODE"))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Context("live error path still invalidates", func() {
|
|
|
|
|
It("leaves no data file and no marker after a mid-stream reader error", func() {
|
|
|
|
|
fc := callNewFileCache("test", "10MB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
return errFakeReader{errors.New("boom")}, nil
|
|
|
|
|
})
|
|
|
|
|
s, err := fc.Get(context.Background(), &testArg{"err"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
_, _ = io.Copy(io.Discard, s)
|
|
|
|
|
_ = s.Close()
|
|
|
|
|
|
|
|
|
|
dataPath := fcSpreadFS(fc).KeyMapper((&testArg{"err"}).Key())
|
|
|
|
|
Eventually(func() bool {
|
|
|
|
|
_, e1 := os.Stat(dataPath)
|
|
|
|
|
_, e2 := os.Stat(dataPath + ".complete")
|
|
|
|
|
return os.IsNotExist(e1) && os.IsNotExist(e2)
|
|
|
|
|
}).Should(BeTrue())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("does not write a completion marker when the write fails after partial bytes", func() {
|
|
|
|
|
// Mimics a transcode that produces real output and then dies:
|
|
|
|
|
// the bytes land on disk, but the entry must NOT be marked complete.
|
|
|
|
|
fc := callNewFileCache("test", "10MB", "test", 0, func(ctx context.Context, arg Item) (io.Reader, error) {
|
|
|
|
|
return &partialThenErrReader{data: []byte("PARTIAL-OUTPUT"), err: errors.New("transcoder died")}, nil
|
|
|
|
|
})
|
|
|
|
|
s, err := fc.Get(context.Background(), &testArg{"partial"})
|
|
|
|
|
Expect(err).To(BeNil())
|
|
|
|
|
_, _ = io.Copy(io.Discard, s)
|
|
|
|
|
_ = s.Close()
|
|
|
|
|
|
|
|
|
|
dataPath := fcSpreadFS(fc).KeyMapper((&testArg{"partial"}).Key())
|
|
|
|
|
// The marker must never appear for a failed write. Give the async
|
|
|
|
|
// writer time to finish, then assert the marker stays absent.
|
|
|
|
|
Consistently(func() bool {
|
|
|
|
|
_, e := os.Stat(dataPath + ".complete")
|
|
|
|
|
return os.IsNotExist(e)
|
|
|
|
|
}).Should(BeTrue())
|
|
|
|
|
})
|
|
|
|
|
})
|
2020-07-24 12:42:11 -04:00
|
|
|
})
|
2020-04-09 13:15:01 -04:00
|
|
|
})
|
2020-07-24 13:30:27 -04:00
|
|
|
|
|
|
|
|
type testArg struct{ s string }
|
|
|
|
|
|
2020-10-24 15:10:27 -04:00
|
|
|
func (t *testArg) Key() string { return t.s }
|
2022-12-18 12:22:12 -05:00
|
|
|
|
2025-05-27 19:53:10 -04:00
|
|
|
type errFakeReader struct{ err error }
|
2022-12-18 12:22:12 -05:00
|
|
|
|
2025-05-27 19:53:10 -04:00
|
|
|
func (e errFakeReader) Read([]byte) (int, error) { return 0, e.err }
|
2026-06-23 22:23:54 -04:00
|
|
|
|
|
|
|
|
// partialThenErrReader emits data once, then fails — mimicking a transcoder
|
|
|
|
|
// that produces some output and then dies mid-stream.
|
|
|
|
|
type partialThenErrReader struct {
|
|
|
|
|
data []byte
|
|
|
|
|
err error
|
|
|
|
|
done bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *partialThenErrReader) Read(p []byte) (int, error) {
|
|
|
|
|
if r.done {
|
|
|
|
|
return 0, r.err
|
|
|
|
|
}
|
|
|
|
|
r.done = true
|
|
|
|
|
return copy(p, r.data), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fcSpreadFS(fc *fileCache) *spreadFS {
|
|
|
|
|
return fc.fs
|
|
|
|
|
}
|