corso/src/internal/m365/graph/metadata_collection.go
ryanfkeepers 6ed54fad9c first pass on ServiceResource tuple compliance
First pass for updating code outside of the path package
to comply with ServiceResource tuple slices.  Compliance
is still incomplete, so the build and tests
will still fail.  Changes
in this PR are focused on the easier-to-make changes, mostly
generating ServiceResources where there were
previously manual declarations of service and resource.
2023-08-10 17:55:34 -06:00

207 lines
5.0 KiB
Go

package graph
import (
"bytes"
"context"
"encoding/json"
"io"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
)
var (
_ data.BackupCollection = &MetadataCollection{}
_ data.Stream = &MetadataItem{}
)
// MetadataCollection in a simple collection that assumes all items to be
// returned are already resident in-memory and known when the collection is
// created. This collection has no logic for lazily fetching item data.
type MetadataCollection struct {
fullPath path.Path
items []MetadataItem
statusUpdater support.StatusUpdater
}
// MetadataCollectionEntry describes a file that should get added to a metadata
// collection. The Data value will be encoded into json as part of a
// transformation into a MetadataItem.
type MetadataCollectionEntry struct {
fileName string
data any
}
func NewMetadataEntry(fileName string, mData any) MetadataCollectionEntry {
return MetadataCollectionEntry{fileName, mData}
}
func (mce MetadataCollectionEntry) toMetadataItem() (MetadataItem, error) {
if len(mce.fileName) == 0 {
return MetadataItem{}, clues.New("missing metadata filename")
}
if mce.data == nil {
return MetadataItem{}, clues.New("missing metadata")
}
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
if err := encoder.Encode(mce.data); err != nil {
return MetadataItem{}, clues.Wrap(err, "serializing metadata")
}
return NewMetadataItem(mce.fileName, buf.Bytes()), nil
}
// MakeMetadataCollection creates a metadata collection that has a file
// containing all the provided metadata as a single json object. Returns
// nil if the map does not have any entries.
func MakeMetadataCollection(
tenant, resourceOwner string,
service path.ServiceType,
cat path.CategoryType,
metadata []MetadataCollectionEntry,
statusUpdater support.StatusUpdater,
) (data.BackupCollection, error) {
if len(metadata) == 0 {
return nil, nil
}
p, err := path.Builder{}.ToServiceCategoryMetadataPath(
tenant,
[]path.ServiceResource{{
Service: service,
ProtectedResource: resourceOwner,
}},
cat,
false)
if err != nil {
return nil, clues.Wrap(err, "making metadata path")
}
items := make([]MetadataItem, 0, len(metadata))
for _, md := range metadata {
item, err := md.toMetadataItem()
if err != nil {
return nil, err
}
items = append(items, item)
}
coll := NewMetadataCollection(p, items, statusUpdater)
return coll, nil
}
func NewMetadataCollection(
p path.Path,
items []MetadataItem,
statusUpdater support.StatusUpdater,
) *MetadataCollection {
return &MetadataCollection{
fullPath: p,
items: items,
statusUpdater: statusUpdater,
}
}
func (md MetadataCollection) FullPath() path.Path {
return md.fullPath
}
// TODO(ashmrtn): Fill in with previous path once the Controller compares old
// and new folder hierarchies.
func (md MetadataCollection) PreviousPath() path.Path {
return nil
}
// TODO(ashmrtn): Fill in once the Controller compares old and new folder
// hierarchies.
func (md MetadataCollection) State() data.CollectionState {
return data.NewState
}
func (md MetadataCollection) DoNotMergeItems() bool {
return false
}
func (md MetadataCollection) Items(
ctx context.Context,
_ *fault.Bus, // not used, just here for interface compliance
) <-chan data.Stream {
res := make(chan data.Stream)
go func() {
totalBytes := int64(0)
defer func() {
// Need to report after the collection is created because otherwise
// statusUpdater may not have accounted for the fact that this collection
// will be running.
status := support.CreateStatus(
ctx,
support.Backup,
1,
support.CollectionMetrics{
Objects: len(md.items),
Successes: len(md.items),
Bytes: totalBytes,
},
md.fullPath.Folder(false),
)
md.statusUpdater(status)
}()
defer close(res)
for _, item := range md.items {
totalBytes += int64(len(item.data))
res <- item
}
}()
return res
}
// MetadataItem is an in-memory data.Stream implementation. MetadataItem does
// not implement additional interfaces like data.StreamInfo, so it should only
// be used for items with a small amount of content that don't need to be added
// to backup details.
//
// Currently the expected use-case for this struct are storing metadata for a
// backup like delta tokens or a mapping of container IDs to container paths.
type MetadataItem struct {
// uuid is an ID that can be used to refer to the item.
uuid string
// data is a buffer of data that the item refers to.
data []byte
}
func NewMetadataItem(uuid string, itemData []byte) MetadataItem {
return MetadataItem{
uuid: uuid,
data: itemData,
}
}
func (mi MetadataItem) UUID() string {
return mi.uuid
}
// TODO(ashmrtn): Fill in once we know how to handle this.
func (mi MetadataItem) Deleted() bool {
return false
}
func (mi MetadataItem) ToReader() io.ReadCloser {
return io.NopCloser(bytes.NewReader(mi.data))
}