Allow kopia directories to have streamed items and subdirectories (#505)

* Remove artificial limit on kopia directories

Originally did not allow a directory to have both child directories and
items. Remove that limit and move logic to execute callbacks on static
items to the iteration function.

* Update tests for new kopia directory structure
This commit is contained in:
ashmrtn 2022-08-05 11:18:07 -07:00 committed by GitHub
parent 133314ebaa
commit 195d5efccc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 115 additions and 48 deletions

View File

@ -26,10 +26,7 @@ const (
corsoUser = "corso" corsoUser = "corso"
) )
var ( var errNotConnected = errors.New("not connected to repo")
errNotConnected = errors.New("not connected to repo")
errUnsupportedDir = errors.New("unsupported static children in streaming directory")
)
type BackupStats struct { type BackupStats struct {
SnapshotID string SnapshotID string
@ -80,19 +77,33 @@ func (w *Wrapper) Close(ctx context.Context) error {
// kopia callbacks on directory entries. It binds the directory to the given // kopia callbacks on directory entries. It binds the directory to the given
// DataCollection. // DataCollection.
func getStreamItemFunc( func getStreamItemFunc(
collection data.Collection, staticEnts []fs.Entry,
streamedEnts data.Collection,
snapshotDetails *details.Details, snapshotDetails *details.Details,
) func(context.Context, func(context.Context, fs.Entry) error) error { ) func(context.Context, func(context.Context, fs.Entry) error) error {
return func(ctx context.Context, cb func(context.Context, fs.Entry) error) error { return func(ctx context.Context, cb func(context.Context, fs.Entry) error) error {
items := collection.Items() // Return static entries in this directory first.
for _, d := range staticEnts {
if err := cb(ctx, d); err != nil {
return errors.Wrap(err, "executing callback on static directory")
}
}
if streamedEnts == nil {
return nil
}
items := streamedEnts.Items()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case e, ok := <-items: case e, ok := <-items:
if !ok { if !ok {
return nil return nil
} }
ei, ok := e.(data.StreamInfo) ei, ok := e.(data.StreamInfo)
if !ok { if !ok {
return errors.New("item does not implement DataStreamInfo") return errors.New("item does not implement DataStreamInfo")
@ -104,7 +115,7 @@ func getStreamItemFunc(
} }
// Populate BackupDetails // Populate BackupDetails
ep := append(collection.FullPath(), e.UUID()) ep := append(streamedEnts.FullPath(), e.UUID())
snapshotDetails.Add(path.Join(ep...), ei.Info()) snapshotDetails.Add(path.Join(ep...), ei.Info())
} }
} }
@ -112,22 +123,11 @@ func getStreamItemFunc(
} }
// buildKopiaDirs recursively builds a directory hierarchy from the roots up. // buildKopiaDirs recursively builds a directory hierarchy from the roots up.
// Returned directories are either virtualfs.StreamingDirectory or // Returned directories are virtualfs.StreamingDirectory.
// virtualfs.staticDirectory.
func buildKopiaDirs(dirName string, dir *treeMap, snapshotDetails *details.Details) (fs.Directory, error) { func buildKopiaDirs(dirName string, dir *treeMap, snapshotDetails *details.Details) (fs.Directory, error) {
// Don't support directories that have both a DataCollection and a set of
// static child directories.
if dir.collection != nil && len(dir.childDirs) > 0 {
return nil, errors.New(errUnsupportedDir.Error())
}
if dir.collection != nil {
return virtualfs.NewStreamingDirectory(dirName, getStreamItemFunc(dir.collection, snapshotDetails)), nil
}
// Need to build the directory tree from the leaves up because intermediate // Need to build the directory tree from the leaves up because intermediate
// directories need to have all their entries at creation time. // directories need to have all their entries at creation time.
childDirs := []fs.Entry{} var childDirs []fs.Entry
for childName, childDir := range dir.childDirs { for childName, childDir := range dir.childDirs {
child, err := buildKopiaDirs(childName, childDir, snapshotDetails) child, err := buildKopiaDirs(childName, childDir, snapshotDetails)
@ -138,7 +138,10 @@ func buildKopiaDirs(dirName string, dir *treeMap, snapshotDetails *details.Detai
childDirs = append(childDirs, child) childDirs = append(childDirs, child)
} }
return virtualfs.NewStaticDirectory(dirName, childDirs), nil return virtualfs.NewStreamingDirectory(
dirName,
getStreamItemFunc(childDirs, dir.collection, snapshotDetails),
), nil
} }
type treeMap struct { type treeMap struct {
@ -179,8 +182,8 @@ func inflateDirTree(ctx context.Context, collections []data.Collection, snapshot
} }
for _, p := range itemPath[1 : len(itemPath)-1] { for _, p := range itemPath[1 : len(itemPath)-1] {
newDir, ok := dir.childDirs[p] newDir := dir.childDirs[p]
if !ok { if newDir == nil {
newDir = newTreeMap() newDir = newTreeMap()
if dir.childDirs == nil { if dir.childDirs == nil {
@ -200,13 +203,13 @@ func inflateDirTree(ctx context.Context, collections []data.Collection, snapshot
end := len(itemPath) - 1 end := len(itemPath) - 1
// Make sure this entry doesn't already exist. // Make sure this entry doesn't already exist.
if _, ok := dir.childDirs[itemPath[end]]; ok { tmpDir := dir.childDirs[itemPath[end]]
return nil, errors.New(errUnsupportedDir.Error()) if tmpDir == nil {
tmpDir = newTreeMap()
dir.childDirs[itemPath[end]] = tmpDir
} }
sd := newTreeMap() tmpDir.collection = s
sd.collection = s
dir.childDirs[itemPath[end]] = sd
} }
if len(roots) > 1 { if len(roots) > 1 {

View File

@ -197,7 +197,90 @@ func (suite *KopiaUnitSuite) TestBuildDirectoryTree_NoAncestorDirs() {
entries, err := fs.GetAllEntries(ctx, dirTree) entries, err := fs.GetAllEntries(ctx, dirTree)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
assert.Len(suite.T(), entries, 42) assert.Len(suite.T(), entries, expectedFileCount)
}
func (suite *KopiaUnitSuite) TestBuildDirectoryTree_MixedDirectory() {
ctx := context.Background()
// Test multiple orders of items because right now order can matter. Both
// orders result in a directory structure like:
// - a-tenant
// - user1
// - emails
// - 5 separate files
// - 42 separate files
table := []struct {
name string
layout []data.Collection
}{
{
name: "SubdirFirst",
layout: []data.Collection{
mockconnector.NewMockExchangeCollection(
[]string{testTenant, testUser, testEmailDir},
5,
),
mockconnector.NewMockExchangeCollection(
[]string{testTenant, testUser},
42,
),
},
},
{
name: "SubdirLast",
layout: []data.Collection{
mockconnector.NewMockExchangeCollection(
[]string{testTenant, testUser},
42,
),
mockconnector.NewMockExchangeCollection(
[]string{testTenant, testUser, testEmailDir},
5,
),
},
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
snapshotDetails := &details.Details{}
dirTree, err := inflateDirTree(ctx, test.layout, snapshotDetails)
require.NoError(t, err)
assert.Equal(t, testTenant, dirTree.Name())
entries, err := fs.GetAllEntries(ctx, dirTree)
require.NoError(t, err)
require.Len(t, entries, 1)
assert.Equal(t, testUser, entries[0].Name())
d, ok := entries[0].(fs.Directory)
require.True(t, ok, "returned entry is not a directory")
entries, err = fs.GetAllEntries(ctx, d)
require.NoError(t, err)
// 42 files and 1 subdirectory.
assert.Len(t, entries, 43)
// One of these entries should be a subdirectory with items in it.
subDirs := []fs.Directory(nil)
for _, e := range entries {
d, ok := e.(fs.Directory)
if !ok {
continue
}
subDirs = append(subDirs, d)
assert.Equal(t, testEmailDir, e.Name())
}
require.Len(t, subDirs, 1)
entries, err = fs.GetAllEntries(ctx, subDirs[0])
assert.NoError(t, err)
assert.Len(t, entries, 5)
})
}
} }
func (suite *KopiaUnitSuite) TestBuildDirectoryTree_Fails() { func (suite *KopiaUnitSuite) TestBuildDirectoryTree_Fails() {
@ -234,25 +317,6 @@ func (suite *KopiaUnitSuite) TestBuildDirectoryTree_Fails() {
), ),
}, },
}, },
{
"MixedDirectory",
// Directory structure would look like (but should return error):
// - a-tenant
// - user1
// - emails
// - 5 separate files
// - 42 separate files
[]data.Collection{
mockconnector.NewMockExchangeCollection(
[]string{"a-tenant", "user1", "emails"},
5,
),
mockconnector.NewMockExchangeCollection(
[]string{"a-tenant", "user1"},
42,
),
},
},
} }
for _, test := range table { for _, test := range table {