add basic groups boilerplate (#3971)
Adding in some basic boilerplate for groups service. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature
This commit is contained in:
parent
2cc380b9b9
commit
e3c51b7dc9
@ -110,7 +110,7 @@ func (ctrl *Controller) ProduceBackupCollections(
|
|||||||
bpc,
|
bpc,
|
||||||
ctrl.AC,
|
ctrl.AC,
|
||||||
ctrl.credentials,
|
ctrl.credentials,
|
||||||
ctrl,
|
ctrl.UpdateStatus,
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, false, err
|
return nil, nil, false, err
|
||||||
|
|||||||
@ -307,7 +307,7 @@ func (suite *DataCollectionIntgSuite) TestSharePointDataCollection() {
|
|||||||
bpc,
|
bpc,
|
||||||
suite.ac,
|
suite.ac,
|
||||||
ctrl.credentials,
|
ctrl.credentials,
|
||||||
ctrl,
|
ctrl.UpdateStatus,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
require.NoError(t, err, clues.ToCore(err))
|
||||||
assert.True(t, canUsePreviousBackup, "can use previous backup")
|
assert.True(t, canUsePreviousBackup, "can use previous backup")
|
||||||
|
|||||||
85
src/internal/m365/groups/backup.go
Normal file
85
src/internal/m365/groups/backup.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package groups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
|
||||||
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
|
"github.com/alcionai/corso/src/pkg/account"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProduceBackupCollections(
|
||||||
|
ctx context.Context,
|
||||||
|
bpc inject.BackupProducerConfig,
|
||||||
|
ac api.Client,
|
||||||
|
creds account.M365Config,
|
||||||
|
su support.StatusUpdater,
|
||||||
|
errs *fault.Bus,
|
||||||
|
) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, bool, error) {
|
||||||
|
b, err := bpc.Selector.ToGroupsBackup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, clues.Wrap(err, "groupsDataCollection: parsing selector")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
el = errs.Local()
|
||||||
|
collections = []data.BackupCollection{}
|
||||||
|
categories = map[path.CategoryType]struct{}{}
|
||||||
|
ssmb = prefixmatcher.NewStringSetBuilder()
|
||||||
|
canUsePreviousBackup bool
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = clues.Add(
|
||||||
|
ctx,
|
||||||
|
"group_id", clues.Hide(bpc.ProtectedResource.ID()),
|
||||||
|
"group_name", clues.Hide(bpc.ProtectedResource.Name()))
|
||||||
|
|
||||||
|
for _, scope := range b.Scopes() {
|
||||||
|
if el.Failure() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar := observe.MessageWithCompletion(
|
||||||
|
ctx,
|
||||||
|
observe.Bulletf("%s", scope.Category().PathType()))
|
||||||
|
defer close(progressBar)
|
||||||
|
|
||||||
|
var dbcs []data.BackupCollection
|
||||||
|
|
||||||
|
switch scope.Category().PathType() {
|
||||||
|
case path.LibrariesCategory: // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
collections = append(collections, dbcs...)
|
||||||
|
|
||||||
|
categories[scope.Category().PathType()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(collections) > 0 {
|
||||||
|
baseCols, err := graph.BaseCollections(
|
||||||
|
ctx,
|
||||||
|
collections,
|
||||||
|
creds.AzureTenantID,
|
||||||
|
bpc.ProtectedResource.ID(),
|
||||||
|
path.UnknownService, // path.GroupsService
|
||||||
|
categories,
|
||||||
|
su,
|
||||||
|
errs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
collections = append(collections, baseCols...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return collections, ssmb.ToReader(), canUsePreviousBackup, el.Failure()
|
||||||
|
}
|
||||||
93
src/internal/m365/groups/restore.go
Normal file
93
src/internal/m365/groups/restore.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package groups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
|
"github.com/alcionai/corso/src/internal/data"
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConsumeRestoreCollections will restore the specified data collections into OneDrive
|
||||||
|
func ConsumeRestoreCollections(
|
||||||
|
ctx context.Context,
|
||||||
|
rcc inject.RestoreConsumerConfig,
|
||||||
|
ac api.Client,
|
||||||
|
backupDriveIDNames idname.Cacher,
|
||||||
|
dcs []data.RestoreCollection,
|
||||||
|
deets *details.Builder,
|
||||||
|
errs *fault.Bus,
|
||||||
|
ctr *count.Bus,
|
||||||
|
) (*support.ControllerOperationStatus, error) {
|
||||||
|
var (
|
||||||
|
restoreMetrics support.CollectionMetrics
|
||||||
|
// caches = onedrive.NewRestoreCaches(backupDriveIDNames)
|
||||||
|
el = errs.Local()
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: uncomment when a handler is available
|
||||||
|
// err := caches.Populate(ctx, lrh, rcc.ProtectedResource.ID())
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, clues.Wrap(err, "initializing restore caches")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Reorder collections so that the parents directories are created
|
||||||
|
// before the child directories; a requirement for permissions.
|
||||||
|
data.SortRestoreCollections(dcs)
|
||||||
|
|
||||||
|
// Iterate through the data collections and restore the contents of each
|
||||||
|
for _, dc := range dcs {
|
||||||
|
if el.Failure() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
category = dc.FullPath().Category()
|
||||||
|
metrics support.CollectionMetrics
|
||||||
|
ictx = clues.Add(ctx,
|
||||||
|
"category", category,
|
||||||
|
"restore_location", clues.Hide(rcc.RestoreConfig.Location),
|
||||||
|
"protected_resource", clues.Hide(dc.FullPath().ResourceOwner()),
|
||||||
|
"full_path", dc.FullPath())
|
||||||
|
)
|
||||||
|
|
||||||
|
switch dc.FullPath().Category() {
|
||||||
|
case path.LibrariesCategory:
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, clues.New("data category not supported").
|
||||||
|
With("category", category).
|
||||||
|
WithClues(ictx)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreMetrics = support.CombineMetrics(restoreMetrics, metrics)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
el.AddRecoverable(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status := support.CreateStatus(
|
||||||
|
ctx,
|
||||||
|
support.Restore,
|
||||||
|
len(dcs),
|
||||||
|
restoreMetrics,
|
||||||
|
rcc.RestoreConfig.Location)
|
||||||
|
|
||||||
|
return status, el.Failure()
|
||||||
|
}
|
||||||
@ -21,18 +21,12 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type statusUpdater interface {
|
|
||||||
UpdateStatus(status *support.ControllerOperationStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProduceBackupCollections returns a set of DataCollection which represents the SharePoint data
|
|
||||||
// for the specified user
|
|
||||||
func ProduceBackupCollections(
|
func ProduceBackupCollections(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
bpc inject.BackupProducerConfig,
|
bpc inject.BackupProducerConfig,
|
||||||
ac api.Client,
|
ac api.Client,
|
||||||
creds account.M365Config,
|
creds account.M365Config,
|
||||||
su statusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, bool, error) {
|
) ([]data.BackupCollection, *prefixmatcher.StringSetMatcher, bool, error) {
|
||||||
b, err := bpc.Selector.ToSharePointBackup()
|
b, err := bpc.Selector.ToSharePointBackup()
|
||||||
@ -129,7 +123,7 @@ func ProduceBackupCollections(
|
|||||||
bpc.ProtectedResource.ID(),
|
bpc.ProtectedResource.ID(),
|
||||||
path.SharePointService,
|
path.SharePointService,
|
||||||
categories,
|
categories,
|
||||||
su.UpdateStatus,
|
su,
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, false, err
|
return nil, nil, false, err
|
||||||
@ -146,7 +140,7 @@ func collectLists(
|
|||||||
bpc inject.BackupProducerConfig,
|
bpc inject.BackupProducerConfig,
|
||||||
ac api.Client,
|
ac api.Client,
|
||||||
tenantID string,
|
tenantID string,
|
||||||
updater statusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, error) {
|
) ([]data.BackupCollection, error) {
|
||||||
logger.Ctx(ctx).Debug("Creating SharePoint List Collections")
|
logger.Ctx(ctx).Debug("Creating SharePoint List Collections")
|
||||||
@ -181,7 +175,7 @@ func collectLists(
|
|||||||
dir,
|
dir,
|
||||||
ac,
|
ac,
|
||||||
List,
|
List,
|
||||||
updater.UpdateStatus,
|
su,
|
||||||
bpc.Options)
|
bpc.Options)
|
||||||
collection.AddJob(tuple.id)
|
collection.AddJob(tuple.id)
|
||||||
|
|
||||||
@ -200,7 +194,7 @@ func collectLibraries(
|
|||||||
tenantID string,
|
tenantID string,
|
||||||
ssmb *prefixmatcher.StringSetMatchBuilder,
|
ssmb *prefixmatcher.StringSetMatchBuilder,
|
||||||
scope selectors.SharePointScope,
|
scope selectors.SharePointScope,
|
||||||
updater statusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, bool, error) {
|
) ([]data.BackupCollection, bool, error) {
|
||||||
logger.Ctx(ctx).Debug("creating SharePoint Library collections")
|
logger.Ctx(ctx).Debug("creating SharePoint Library collections")
|
||||||
@ -211,7 +205,7 @@ func collectLibraries(
|
|||||||
&libraryBackupHandler{ad, scope},
|
&libraryBackupHandler{ad, scope},
|
||||||
tenantID,
|
tenantID,
|
||||||
bpc.ProtectedResource.ID(),
|
bpc.ProtectedResource.ID(),
|
||||||
updater.UpdateStatus,
|
su,
|
||||||
bpc.Options)
|
bpc.Options)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -230,7 +224,7 @@ func collectPages(
|
|||||||
bpc inject.BackupProducerConfig,
|
bpc inject.BackupProducerConfig,
|
||||||
creds account.M365Config,
|
creds account.M365Config,
|
||||||
ac api.Client,
|
ac api.Client,
|
||||||
updater statusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, error) {
|
) ([]data.BackupCollection, error) {
|
||||||
logger.Ctx(ctx).Debug("creating SharePoint Pages collections")
|
logger.Ctx(ctx).Debug("creating SharePoint Pages collections")
|
||||||
@ -277,7 +271,7 @@ func collectPages(
|
|||||||
dir,
|
dir,
|
||||||
ac,
|
ac,
|
||||||
Pages,
|
Pages,
|
||||||
updater.UpdateStatus,
|
su,
|
||||||
bpc.Options)
|
bpc.Options)
|
||||||
collection.betaService = betaService
|
collection.betaService = betaService
|
||||||
collection.AddJob(tuple.ID)
|
collection.AddJob(tuple.ID)
|
||||||
|
|||||||
@ -217,7 +217,7 @@ func (suite *SharePointPagesSuite) TestCollectPages() {
|
|||||||
bpc,
|
bpc,
|
||||||
creds,
|
creds,
|
||||||
ac,
|
ac,
|
||||||
&MockGraphService{},
|
(&MockGraphService{}).UpdateStatus,
|
||||||
fault.New(true))
|
fault.New(true))
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
assert.NotEmpty(t, col)
|
assert.NotEmpty(t, col)
|
||||||
|
|||||||
143
src/pkg/backup/details/builder.go
Normal file
143
src/pkg/backup/details/builder.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder should be used to create a details model.
|
||||||
|
type Builder struct {
|
||||||
|
d Details
|
||||||
|
mu sync.Mutex `json:"-"`
|
||||||
|
knownFolders map[string]Entry `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Add(
|
||||||
|
repoRef path.Path,
|
||||||
|
locationRef *path.Builder,
|
||||||
|
updated bool,
|
||||||
|
info ItemInfo,
|
||||||
|
) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
entry, err := b.d.add(
|
||||||
|
repoRef,
|
||||||
|
locationRef,
|
||||||
|
updated,
|
||||||
|
info)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "adding entry to details")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.addFolderEntries(
|
||||||
|
repoRef.ToBuilder().Dir(),
|
||||||
|
locationRef,
|
||||||
|
entry,
|
||||||
|
); err != nil {
|
||||||
|
return clues.Wrap(err, "adding folder entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) addFolderEntries(
|
||||||
|
repoRef, locationRef *path.Builder,
|
||||||
|
entry Entry,
|
||||||
|
) error {
|
||||||
|
if len(repoRef.Elements()) < len(locationRef.Elements()) {
|
||||||
|
return clues.New("RepoRef shorter than LocationRef").
|
||||||
|
With("repo_ref", repoRef, "location_ref", locationRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.knownFolders == nil {
|
||||||
|
b.knownFolders = map[string]Entry{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need a unique location because we want to have separate folders for
|
||||||
|
// different drives and categories even if there's duplicate folder names in
|
||||||
|
// them.
|
||||||
|
uniqueLoc, err := entry.uniqueLocation(locationRef)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting LocationIDer")
|
||||||
|
}
|
||||||
|
|
||||||
|
for uniqueLoc.elementCount() > 0 {
|
||||||
|
mapKey := uniqueLoc.ID().ShortRef()
|
||||||
|
|
||||||
|
name := uniqueLoc.lastElem()
|
||||||
|
if len(name) == 0 {
|
||||||
|
return clues.New("folder with no display name").
|
||||||
|
With("repo_ref", repoRef, "location_ref", uniqueLoc.InDetails())
|
||||||
|
}
|
||||||
|
|
||||||
|
shortRef := repoRef.ShortRef()
|
||||||
|
rr := repoRef.String()
|
||||||
|
|
||||||
|
// Get the parent of this entry to add as the LocationRef for the folder.
|
||||||
|
uniqueLoc.dir()
|
||||||
|
|
||||||
|
repoRef = repoRef.Dir()
|
||||||
|
parentRef := repoRef.ShortRef()
|
||||||
|
|
||||||
|
folder, ok := b.knownFolders[mapKey]
|
||||||
|
if !ok {
|
||||||
|
loc := uniqueLoc.InDetails().String()
|
||||||
|
|
||||||
|
folder = Entry{
|
||||||
|
RepoRef: rr,
|
||||||
|
ShortRef: shortRef,
|
||||||
|
ParentRef: parentRef,
|
||||||
|
LocationRef: loc,
|
||||||
|
ItemInfo: ItemInfo{
|
||||||
|
Folder: &FolderInfo{
|
||||||
|
ItemType: FolderItem,
|
||||||
|
// TODO(ashmrtn): Use the item type returned by the entry once
|
||||||
|
// SharePoint properly sets it.
|
||||||
|
DisplayName: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := entry.updateFolder(folder.Folder); err != nil {
|
||||||
|
return clues.Wrap(err, "adding folder").
|
||||||
|
With("parent_repo_ref", repoRef, "location_ref", loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.Folder.Size += entry.size()
|
||||||
|
folder.Updated = folder.Updated || entry.Updated
|
||||||
|
|
||||||
|
itemModified := entry.Modified()
|
||||||
|
if folder.Folder.Modified.Before(itemModified) {
|
||||||
|
folder.Folder.Modified = itemModified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the map because we're storing structs not pointers to
|
||||||
|
// structs.
|
||||||
|
b.knownFolders[mapKey] = folder
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Details() *Details {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
ents := make([]Entry, len(b.d.Entries))
|
||||||
|
copy(ents, b.d.Entries)
|
||||||
|
|
||||||
|
// Write the cached folder entries to details
|
||||||
|
details := &Details{
|
||||||
|
DetailsModel{
|
||||||
|
Entries: append(ents, maps.Values(b.knownFolders)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return details
|
||||||
|
}
|
||||||
@ -1,22 +1,13 @@
|
|||||||
package details
|
package details
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/print"
|
|
||||||
"github.com/alcionai/corso/src/internal/common/dttm"
|
|
||||||
"github.com/alcionai/corso/src/internal/m365/onedrive/metadata"
|
"github.com/alcionai/corso/src/internal/m365/onedrive/metadata"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,383 +15,6 @@ import (
|
|||||||
// more than this, then we just show a summary.
|
// more than this, then we just show a summary.
|
||||||
const maxPrintLimit = 50
|
const maxPrintLimit = 50
|
||||||
|
|
||||||
// LocationIDer provides access to location information but guarantees that it
|
|
||||||
// can also generate a unique location (among items in the same service but
|
|
||||||
// possibly across data types within the service) that can be used as a key in
|
|
||||||
// maps and other structures. The unique location may be different than
|
|
||||||
// InDetails, the location used in backup details.
|
|
||||||
type LocationIDer interface {
|
|
||||||
ID() *path.Builder
|
|
||||||
InDetails() *path.Builder
|
|
||||||
}
|
|
||||||
|
|
||||||
type uniqueLoc struct {
|
|
||||||
pb *path.Builder
|
|
||||||
prefixElems int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ul uniqueLoc) ID() *path.Builder {
|
|
||||||
return ul.pb
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ul uniqueLoc) InDetails() *path.Builder {
|
|
||||||
return path.Builder{}.Append(ul.pb.Elements()[ul.prefixElems:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// elementCount returns the number of non-prefix elements in the LocationIDer
|
|
||||||
// (i.e. the number of elements in the InDetails path.Builder).
|
|
||||||
func (ul uniqueLoc) elementCount() int {
|
|
||||||
res := len(ul.pb.Elements()) - ul.prefixElems
|
|
||||||
if res < 0 {
|
|
||||||
res = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ul *uniqueLoc) dir() {
|
|
||||||
if ul.elementCount() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.pb = ul.pb.Dir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// lastElem returns the unescaped last element in the location. If the location
|
|
||||||
// is empty returns an empty string.
|
|
||||||
func (ul uniqueLoc) lastElem() string {
|
|
||||||
if ul.elementCount() == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return ul.pb.LastElem()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Having service-specific constructors can be kind of clunky, but in this case
|
|
||||||
// I think they'd be useful to ensure the proper args are used since this
|
|
||||||
// path.Builder is used as a key in some maps.
|
|
||||||
|
|
||||||
// NewExchangeLocationIDer builds a LocationIDer for the given category and
|
|
||||||
// folder path. The path denoted by the folders should be unique within the
|
|
||||||
// category.
|
|
||||||
func NewExchangeLocationIDer(
|
|
||||||
category path.CategoryType,
|
|
||||||
escapedFolders ...string,
|
|
||||||
) (uniqueLoc, error) {
|
|
||||||
if err := path.ValidateServiceAndCategory(path.ExchangeService, category); err != nil {
|
|
||||||
return uniqueLoc{}, clues.Wrap(err, "making exchange LocationIDer")
|
|
||||||
}
|
|
||||||
|
|
||||||
pb := path.Builder{}.Append(category.String()).Append(escapedFolders...)
|
|
||||||
|
|
||||||
return uniqueLoc{
|
|
||||||
pb: pb,
|
|
||||||
prefixElems: 1,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOneDriveLocationIDer builds a LocationIDer for the drive and folder path.
|
|
||||||
// The path denoted by the folders should be unique within the drive.
|
|
||||||
func NewOneDriveLocationIDer(
|
|
||||||
driveID string,
|
|
||||||
escapedFolders ...string,
|
|
||||||
) uniqueLoc {
|
|
||||||
pb := path.Builder{}.
|
|
||||||
Append(path.FilesCategory.String(), driveID).
|
|
||||||
Append(escapedFolders...)
|
|
||||||
|
|
||||||
return uniqueLoc{
|
|
||||||
pb: pb,
|
|
||||||
prefixElems: 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSharePointLocationIDer builds a LocationIDer for the drive and folder
|
|
||||||
// path. The path denoted by the folders should be unique within the drive.
|
|
||||||
func NewSharePointLocationIDer(
|
|
||||||
driveID string,
|
|
||||||
escapedFolders ...string,
|
|
||||||
) uniqueLoc {
|
|
||||||
pb := path.Builder{}.
|
|
||||||
Append(path.LibrariesCategory.String(), driveID).
|
|
||||||
Append(escapedFolders...)
|
|
||||||
|
|
||||||
return uniqueLoc{
|
|
||||||
pb: pb,
|
|
||||||
prefixElems: 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------
|
|
||||||
// Model
|
|
||||||
// --------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// DetailsModel describes what was stored in a Backup
|
|
||||||
type DetailsModel struct {
|
|
||||||
Entries []Entry `json:"entries"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print writes the DetailModel Entries to StdOut, in the format
|
|
||||||
// requested by the caller.
|
|
||||||
func (dm DetailsModel) PrintEntries(ctx context.Context) {
|
|
||||||
printEntries(ctx, dm.Entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
type infoer interface {
|
|
||||||
Entry | *Entry
|
|
||||||
// Need this here so we can access the infoType function without a type
|
|
||||||
// assertion. See https://stackoverflow.com/a/71378366 for more details.
|
|
||||||
infoType() ItemType
|
|
||||||
}
|
|
||||||
|
|
||||||
func printEntries[T infoer](ctx context.Context, entries []T) {
|
|
||||||
if print.DisplayJSONFormat() {
|
|
||||||
printJSON(ctx, entries)
|
|
||||||
} else {
|
|
||||||
printTable(ctx, entries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printTable[T infoer](ctx context.Context, entries []T) {
|
|
||||||
perType := map[ItemType][]print.Printable{}
|
|
||||||
|
|
||||||
for _, ent := range entries {
|
|
||||||
it := ent.infoType()
|
|
||||||
ps, ok := perType[it]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
ps = []print.Printable{}
|
|
||||||
}
|
|
||||||
|
|
||||||
perType[it] = append(ps, print.Printable(ent))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ps := range perType {
|
|
||||||
print.All(ctx, ps...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printJSON[T infoer](ctx context.Context, entries []T) {
|
|
||||||
ents := []print.Printable{}
|
|
||||||
|
|
||||||
for _, ent := range entries {
|
|
||||||
ents = append(ents, print.Printable(ent))
|
|
||||||
}
|
|
||||||
|
|
||||||
print.All(ctx, ents...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paths returns the list of Paths for non-folder and non-meta items extracted
|
|
||||||
// from the Entries slice.
|
|
||||||
func (dm DetailsModel) Paths() []string {
|
|
||||||
r := make([]string, 0, len(dm.Entries))
|
|
||||||
|
|
||||||
for _, ent := range dm.Entries {
|
|
||||||
if ent.Folder != nil || ent.isMetaFile() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
r = append(r, ent.RepoRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items returns a slice of *ItemInfo that does not contain any FolderInfo
|
|
||||||
// entries. Required because not all folders in the details are valid resource
|
|
||||||
// paths, and we want to slice out metadata.
|
|
||||||
func (dm DetailsModel) Items() entrySet {
|
|
||||||
res := make([]*Entry, 0, len(dm.Entries))
|
|
||||||
|
|
||||||
for i := 0; i < len(dm.Entries); i++ {
|
|
||||||
ent := dm.Entries[i]
|
|
||||||
if ent.Folder != nil || ent.isMetaFile() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
res = append(res, &ent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterMetaFiles returns a copy of the Details with all of the
|
|
||||||
// .meta files removed from the entries.
|
|
||||||
func (dm DetailsModel) FilterMetaFiles() DetailsModel {
|
|
||||||
d2 := DetailsModel{
|
|
||||||
Entries: []Entry{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ent := range dm.Entries {
|
|
||||||
if !ent.isMetaFile() {
|
|
||||||
d2.Entries = append(d2.Entries, ent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return d2
|
|
||||||
}
|
|
||||||
|
|
||||||
// SumNonMetaFileSizes returns the total size of items excluding all the
|
|
||||||
// .meta files from the items.
|
|
||||||
func (dm DetailsModel) SumNonMetaFileSizes() int64 {
|
|
||||||
var size int64
|
|
||||||
|
|
||||||
// Items will provide only files and filter out folders
|
|
||||||
for _, ent := range dm.FilterMetaFiles().Items() {
|
|
||||||
size += ent.size()
|
|
||||||
}
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a file is a metadata file. These are used to store
|
|
||||||
// additional data like permissions (in case of Drive items) and are
|
|
||||||
// not to be treated as regular files.
|
|
||||||
func (de Entry) isMetaFile() bool {
|
|
||||||
// sharepoint types not needed, since sharepoint permissions were
|
|
||||||
// added after IsMeta was deprecated.
|
|
||||||
// Earlier onedrive backups used to store both metafiles and files in details.
|
|
||||||
// So filter out just the onedrive items and check for metafiles
|
|
||||||
return de.ItemInfo.OneDrive != nil && de.ItemInfo.OneDrive.IsMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Builder
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Builder should be used to create a details model.
|
|
||||||
type Builder struct {
|
|
||||||
d Details
|
|
||||||
mu sync.Mutex `json:"-"`
|
|
||||||
knownFolders map[string]Entry `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) Add(
|
|
||||||
repoRef path.Path,
|
|
||||||
locationRef *path.Builder,
|
|
||||||
updated bool,
|
|
||||||
info ItemInfo,
|
|
||||||
) error {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
entry, err := b.d.add(
|
|
||||||
repoRef,
|
|
||||||
locationRef,
|
|
||||||
updated,
|
|
||||||
info)
|
|
||||||
if err != nil {
|
|
||||||
return clues.Wrap(err, "adding entry to details")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.addFolderEntries(
|
|
||||||
repoRef.ToBuilder().Dir(),
|
|
||||||
locationRef,
|
|
||||||
entry,
|
|
||||||
); err != nil {
|
|
||||||
return clues.Wrap(err, "adding folder entries")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) addFolderEntries(
|
|
||||||
repoRef, locationRef *path.Builder,
|
|
||||||
entry Entry,
|
|
||||||
) error {
|
|
||||||
if len(repoRef.Elements()) < len(locationRef.Elements()) {
|
|
||||||
return clues.New("RepoRef shorter than LocationRef").
|
|
||||||
With("repo_ref", repoRef, "location_ref", locationRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.knownFolders == nil {
|
|
||||||
b.knownFolders = map[string]Entry{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need a unique location because we want to have separate folders for
|
|
||||||
// different drives and categories even if there's duplicate folder names in
|
|
||||||
// them.
|
|
||||||
uniqueLoc, err := entry.uniqueLocation(locationRef)
|
|
||||||
if err != nil {
|
|
||||||
return clues.Wrap(err, "getting LocationIDer")
|
|
||||||
}
|
|
||||||
|
|
||||||
for uniqueLoc.elementCount() > 0 {
|
|
||||||
mapKey := uniqueLoc.ID().ShortRef()
|
|
||||||
|
|
||||||
name := uniqueLoc.lastElem()
|
|
||||||
if len(name) == 0 {
|
|
||||||
return clues.New("folder with no display name").
|
|
||||||
With("repo_ref", repoRef, "location_ref", uniqueLoc.InDetails())
|
|
||||||
}
|
|
||||||
|
|
||||||
shortRef := repoRef.ShortRef()
|
|
||||||
rr := repoRef.String()
|
|
||||||
|
|
||||||
// Get the parent of this entry to add as the LocationRef for the folder.
|
|
||||||
uniqueLoc.dir()
|
|
||||||
|
|
||||||
repoRef = repoRef.Dir()
|
|
||||||
parentRef := repoRef.ShortRef()
|
|
||||||
|
|
||||||
folder, ok := b.knownFolders[mapKey]
|
|
||||||
if !ok {
|
|
||||||
loc := uniqueLoc.InDetails().String()
|
|
||||||
|
|
||||||
folder = Entry{
|
|
||||||
RepoRef: rr,
|
|
||||||
ShortRef: shortRef,
|
|
||||||
ParentRef: parentRef,
|
|
||||||
LocationRef: loc,
|
|
||||||
ItemInfo: ItemInfo{
|
|
||||||
Folder: &FolderInfo{
|
|
||||||
ItemType: FolderItem,
|
|
||||||
// TODO(ashmrtn): Use the item type returned by the entry once
|
|
||||||
// SharePoint properly sets it.
|
|
||||||
DisplayName: name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := entry.updateFolder(folder.Folder); err != nil {
|
|
||||||
return clues.Wrap(err, "adding folder").
|
|
||||||
With("parent_repo_ref", repoRef, "location_ref", loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.Folder.Size += entry.size()
|
|
||||||
folder.Updated = folder.Updated || entry.Updated
|
|
||||||
|
|
||||||
itemModified := entry.Modified()
|
|
||||||
if folder.Folder.Modified.Before(itemModified) {
|
|
||||||
folder.Folder.Modified = itemModified
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always update the map because we're storing structs not pointers to
|
|
||||||
// structs.
|
|
||||||
b.knownFolders[mapKey] = folder
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) Details() *Details {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
ents := make([]Entry, len(b.d.Entries))
|
|
||||||
copy(ents, b.d.Entries)
|
|
||||||
|
|
||||||
// Write the cached folder entries to details
|
|
||||||
details := &Details{
|
|
||||||
DetailsModel{
|
|
||||||
Entries: append(ents, maps.Values(b.knownFolders)...),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return details
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------
|
||||||
// Details
|
// Details
|
||||||
// --------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------
|
||||||
@ -490,540 +104,65 @@ func withoutMetadataSuffix(id string) string {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Entry
|
// LocationIDer
|
||||||
// --------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Add a new type so we can transparently use PrintAll in different situations.
|
// LocationIDer provides access to location information but guarantees that it
|
||||||
type entrySet []*Entry
|
// can also generate a unique location (among items in the same service but
|
||||||
|
// possibly across data types within the service) that can be used as a key in
|
||||||
func (ents entrySet) PrintEntries(ctx context.Context) {
|
// maps and other structures. The unique location may be different than
|
||||||
printEntries(ctx, ents)
|
// InDetails, the location used in backup details.
|
||||||
|
type LocationIDer interface {
|
||||||
|
ID() *path.Builder
|
||||||
|
InDetails() *path.Builder
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaybePrintEntries is same as PrintEntries, but only prints if we
|
type uniqueLoc struct {
|
||||||
// have less than 15 items or is not json output.
|
pb *path.Builder
|
||||||
func (ents entrySet) MaybePrintEntries(ctx context.Context) {
|
prefixElems int
|
||||||
if len(ents) <= maxPrintLimit ||
|
|
||||||
print.DisplayJSONFormat() ||
|
|
||||||
print.DisplayVerbose() {
|
|
||||||
printEntries(ctx, ents)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry describes a single item stored in a Backup
|
func (ul uniqueLoc) ID() *path.Builder {
|
||||||
type Entry struct {
|
return ul.pb
|
||||||
// RepoRef is the full storage path of the item in Kopia
|
|
||||||
RepoRef string `json:"repoRef"`
|
|
||||||
ShortRef string `json:"shortRef"`
|
|
||||||
ParentRef string `json:"parentRef,omitempty"`
|
|
||||||
|
|
||||||
// LocationRef contains the logical path structure by its human-readable
|
|
||||||
// display names. IE: If an item is located at "/Inbox/Important", we
|
|
||||||
// hold that string in the LocationRef, while the actual IDs of each
|
|
||||||
// container are used for the RepoRef.
|
|
||||||
// LocationRef only holds the container values, and does not include
|
|
||||||
// the metadata prefixes (tenant, service, owner, etc) found in the
|
|
||||||
// repoRef.
|
|
||||||
// Currently only implemented for Exchange Calendars.
|
|
||||||
LocationRef string `json:"locationRef,omitempty"`
|
|
||||||
|
|
||||||
// ItemRef contains the stable id of the item itself. ItemRef is not
|
|
||||||
// guaranteed to be unique within a repository. Uniqueness guarantees
|
|
||||||
// maximally inherit from the source item. Eg: Entries for m365 mail items
|
|
||||||
// are only as unique as m365 mail item IDs themselves.
|
|
||||||
ItemRef string `json:"itemRef,omitempty"`
|
|
||||||
|
|
||||||
// Indicates the item was added or updated in this backup
|
|
||||||
// Always `true` for full backups
|
|
||||||
Updated bool `json:"updated"`
|
|
||||||
|
|
||||||
ItemInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLocationIDer takes a backup version and produces the unique location for
|
func (ul uniqueLoc) InDetails() *path.Builder {
|
||||||
// this entry if possible. Reasons it may not be possible to produce the unique
|
return path.Builder{}.Append(ul.pb.Elements()[ul.prefixElems:]...)
|
||||||
// location include an unsupported backup version or missing information.
|
}
|
||||||
func (de Entry) ToLocationIDer(backupVersion int) (LocationIDer, error) {
|
|
||||||
if len(de.LocationRef) > 0 {
|
// elementCount returns the number of non-prefix elements in the LocationIDer
|
||||||
baseLoc, err := path.Builder{}.SplitUnescapeAppend(de.LocationRef)
|
// (i.e. the number of elements in the InDetails path.Builder).
|
||||||
if err != nil {
|
func (ul uniqueLoc) elementCount() int {
|
||||||
return nil, clues.Wrap(err, "parsing base location info").
|
res := len(ul.pb.Elements()) - ul.prefixElems
|
||||||
With("location_ref", de.LocationRef)
|
if res < 0 {
|
||||||
|
res = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual services may add additional info to the base and return that.
|
return res
|
||||||
return de.ItemInfo.uniqueLocation(baseLoc)
|
}
|
||||||
|
|
||||||
|
func (ul *uniqueLoc) dir() {
|
||||||
|
if ul.elementCount() == 0 {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if backupVersion >= version.OneDrive7LocationRef ||
|
ul.pb = ul.pb.Dir()
|
||||||
(de.ItemInfo.infoType() != OneDriveItem &&
|
}
|
||||||
de.ItemInfo.infoType() != SharePointLibrary) {
|
|
||||||
return nil, clues.New("no previous location for entry")
|
// lastElem returns the unescaped last element in the location. If the location
|
||||||
|
// is empty returns an empty string.
|
||||||
|
func (ul uniqueLoc) lastElem() string {
|
||||||
|
if ul.elementCount() == 0 {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a little hacky, but we only want to try to extract the old
|
return ul.pb.LastElem()
|
||||||
// location if it's OneDrive or SharePoint libraries and it's known to
|
|
||||||
// be an older backup version.
|
|
||||||
//
|
|
||||||
// TODO(ashmrtn): Remove this code once OneDrive/SharePoint libraries
|
|
||||||
// LocationRef code has been out long enough that all delta tokens for
|
|
||||||
// previous backup versions will have expired. At that point, either
|
|
||||||
// we'll do a full backup (token expired, no newer backups) or have a
|
|
||||||
// backup of a higher version with the information we need.
|
|
||||||
rr, err := path.FromDataLayerPath(de.RepoRef, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, clues.Wrap(err, "getting item RepoRef")
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := path.ToDrivePath(rr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, clues.New("converting RepoRef to drive path")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseLoc := path.Builder{}.Append(p.Root).Append(p.Folders...)
|
|
||||||
|
|
||||||
// Individual services may add additional info to the base and return that.
|
|
||||||
return de.ItemInfo.uniqueLocation(baseLoc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CLI Output
|
// helpers
|
||||||
// --------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// interface compliance checks
|
|
||||||
var _ print.Printable = &Entry{}
|
|
||||||
|
|
||||||
// MinimumPrintable DetailsEntries is a passthrough func, because no
|
|
||||||
// reduction is needed for the json output.
|
|
||||||
func (de Entry) MinimumPrintable() any {
|
|
||||||
return de
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a DetailsEntry
|
|
||||||
// for printing out to a terminal in a columnar display.
|
|
||||||
func (de Entry) Headers() []string {
|
|
||||||
hs := []string{"ID"}
|
|
||||||
|
|
||||||
if de.ItemInfo.Folder != nil {
|
|
||||||
hs = append(hs, de.ItemInfo.Folder.Headers()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.ItemInfo.Exchange != nil {
|
|
||||||
hs = append(hs, de.ItemInfo.Exchange.Headers()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.ItemInfo.SharePoint != nil {
|
|
||||||
hs = append(hs, de.ItemInfo.SharePoint.Headers()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.ItemInfo.OneDrive != nil {
|
|
||||||
hs = append(hs, de.ItemInfo.OneDrive.Headers()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns the values matching the Headers list.
|
|
||||||
func (de Entry) Values() []string {
|
|
||||||
vs := []string{de.ShortRef}
|
|
||||||
|
|
||||||
if de.ItemInfo.Folder != nil {
|
|
||||||
vs = append(vs, de.ItemInfo.Folder.Values()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.ItemInfo.Exchange != nil {
|
|
||||||
vs = append(vs, de.ItemInfo.Exchange.Values()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.ItemInfo.SharePoint != nil {
|
|
||||||
vs = append(vs, de.ItemInfo.SharePoint.Values()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if de.ItemInfo.OneDrive != nil {
|
|
||||||
vs = append(vs, de.ItemInfo.OneDrive.Values()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return vs
|
|
||||||
}
|
|
||||||
|
|
||||||
type ItemType int
|
|
||||||
|
|
||||||
// ItemTypes are enumerated by service (hundredth digit) and data type (ones digit).
|
|
||||||
// Ex: exchange is 00x where x is the data type. Sharepoint is 10x, and etc.
|
|
||||||
// Every item info struct should get its own hundredth enumeration entry.
|
|
||||||
// Every item category for that service should get its own entry (even if differences
|
|
||||||
// between types aren't apparent on initial implementation, this future-proofs
|
|
||||||
// against breaking changes).
|
|
||||||
// Entries should not be rearranged.
|
|
||||||
// Additionally, any itemType directly assigned a number should not be altered.
|
|
||||||
// This applies to OneDriveItem and FolderItem
|
|
||||||
const (
|
|
||||||
UnknownType ItemType = iota // 0, global unknown value
|
|
||||||
|
|
||||||
// Exchange (00x)
|
|
||||||
ExchangeContact
|
|
||||||
ExchangeEvent
|
|
||||||
ExchangeMail
|
|
||||||
// SharePoint (10x)
|
|
||||||
SharePointLibrary ItemType = iota + 97 // 100
|
|
||||||
SharePointList // 101...
|
|
||||||
SharePointPage
|
|
||||||
|
|
||||||
// OneDrive (20x)
|
|
||||||
OneDriveItem ItemType = 205
|
|
||||||
|
|
||||||
// Folder Management(30x)
|
|
||||||
FolderItem ItemType = 306
|
|
||||||
)
|
|
||||||
|
|
||||||
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
|
||||||
// Only OneDrive and SharePoint have information about parent folders
|
|
||||||
// contained in them.
|
|
||||||
// Can't switch based on infoType because that's been unstable.
|
|
||||||
if item.Exchange != nil {
|
|
||||||
item.Exchange.UpdateParentPath(newLocPath)
|
|
||||||
} else if item.SharePoint != nil {
|
|
||||||
// SharePoint used to store library items with the OneDriveItem ItemType.
|
|
||||||
// Start switching them over as we see them since there's no point in
|
|
||||||
// keeping the old format.
|
|
||||||
if item.SharePoint.ItemType == OneDriveItem {
|
|
||||||
item.SharePoint.ItemType = SharePointLibrary
|
|
||||||
}
|
|
||||||
|
|
||||||
item.SharePoint.UpdateParentPath(newLocPath)
|
|
||||||
} else if item.OneDrive != nil {
|
|
||||||
item.OneDrive.UpdateParentPath(newLocPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ItemInfo is a oneOf that contains service specific
|
|
||||||
// information about the item it tracks
|
|
||||||
type ItemInfo struct {
|
|
||||||
Folder *FolderInfo `json:"folder,omitempty"`
|
|
||||||
Exchange *ExchangeInfo `json:"exchange,omitempty"`
|
|
||||||
SharePoint *SharePointInfo `json:"sharePoint,omitempty"`
|
|
||||||
OneDrive *OneDriveInfo `json:"oneDrive,omitempty"`
|
|
||||||
// Optional item extension data
|
|
||||||
Extension *ExtensionData `json:"extension,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// typedInfo should get embedded in each sesrvice type to track
|
|
||||||
// the type of item it stores for multi-item service support.
|
|
||||||
|
|
||||||
// infoType provides internal categorization for collecting like-typed ItemInfos.
|
|
||||||
// It should return the most granular value type (ex: "event" for an exchange
|
|
||||||
// calendar event).
|
|
||||||
func (i ItemInfo) infoType() ItemType {
|
|
||||||
switch {
|
|
||||||
case i.Folder != nil:
|
|
||||||
return i.Folder.ItemType
|
|
||||||
|
|
||||||
case i.Exchange != nil:
|
|
||||||
return i.Exchange.ItemType
|
|
||||||
|
|
||||||
case i.SharePoint != nil:
|
|
||||||
return i.SharePoint.ItemType
|
|
||||||
|
|
||||||
case i.OneDrive != nil:
|
|
||||||
return i.OneDrive.ItemType
|
|
||||||
}
|
|
||||||
|
|
||||||
return UnknownType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ItemInfo) size() int64 {
|
|
||||||
switch {
|
|
||||||
case i.Exchange != nil:
|
|
||||||
return i.Exchange.Size
|
|
||||||
|
|
||||||
case i.OneDrive != nil:
|
|
||||||
return i.OneDrive.Size
|
|
||||||
|
|
||||||
case i.SharePoint != nil:
|
|
||||||
return i.SharePoint.Size
|
|
||||||
|
|
||||||
case i.Folder != nil:
|
|
||||||
return i.Folder.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ItemInfo) Modified() time.Time {
|
|
||||||
switch {
|
|
||||||
case i.Exchange != nil:
|
|
||||||
return i.Exchange.Modified
|
|
||||||
|
|
||||||
case i.OneDrive != nil:
|
|
||||||
return i.OneDrive.Modified
|
|
||||||
|
|
||||||
case i.SharePoint != nil:
|
|
||||||
return i.SharePoint.Modified
|
|
||||||
|
|
||||||
case i.Folder != nil:
|
|
||||||
return i.Folder.Modified
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
||||||
switch {
|
|
||||||
case i.Exchange != nil:
|
|
||||||
return i.Exchange.uniqueLocation(baseLoc)
|
|
||||||
|
|
||||||
case i.OneDrive != nil:
|
|
||||||
return i.OneDrive.uniqueLocation(baseLoc)
|
|
||||||
|
|
||||||
case i.SharePoint != nil:
|
|
||||||
return i.SharePoint.uniqueLocation(baseLoc)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, clues.New("unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ItemInfo) updateFolder(f *FolderInfo) error {
|
|
||||||
switch {
|
|
||||||
case i.Exchange != nil:
|
|
||||||
return i.Exchange.updateFolder(f)
|
|
||||||
|
|
||||||
case i.OneDrive != nil:
|
|
||||||
return i.OneDrive.updateFolder(f)
|
|
||||||
|
|
||||||
case i.SharePoint != nil:
|
|
||||||
return i.SharePoint.updateFolder(f)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return clues.New("unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderInfo struct {
|
|
||||||
ItemType ItemType `json:"itemType,omitempty"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
Modified time.Time `json:"modified,omitempty"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
DataType ItemType `json:"dataType,omitempty"`
|
|
||||||
DriveName string `json:"driveName,omitempty"`
|
|
||||||
DriveID string `json:"driveID,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i FolderInfo) Headers() []string {
|
|
||||||
return []string{"Display Name"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i FolderInfo) Values() []string {
|
|
||||||
return []string{i.DisplayName}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExchangeInfo describes an exchange item
|
|
||||||
type ExchangeInfo struct {
|
|
||||||
ItemType ItemType `json:"itemType,omitempty"`
|
|
||||||
Sender string `json:"sender,omitempty"`
|
|
||||||
Subject string `json:"subject,omitempty"`
|
|
||||||
Recipient []string `json:"recipient,omitempty"`
|
|
||||||
ParentPath string `json:"parentPath,omitempty"`
|
|
||||||
Received time.Time `json:"received,omitempty"`
|
|
||||||
EventStart time.Time `json:"eventStart,omitempty"`
|
|
||||||
EventEnd time.Time `json:"eventEnd,omitempty"`
|
|
||||||
Organizer string `json:"organizer,omitempty"`
|
|
||||||
ContactName string `json:"contactName,omitempty"`
|
|
||||||
EventRecurs bool `json:"eventRecurs,omitempty"`
|
|
||||||
Created time.Time `json:"created,omitempty"`
|
|
||||||
Modified time.Time `json:"modified,omitempty"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in an ExchangeInfo
|
|
||||||
// for printing out to a terminal in a columnar display.
|
|
||||||
func (i ExchangeInfo) Headers() []string {
|
|
||||||
switch i.ItemType {
|
|
||||||
case ExchangeEvent:
|
|
||||||
return []string{"Organizer", "Subject", "Starts", "Ends", "Recurring"}
|
|
||||||
|
|
||||||
case ExchangeContact:
|
|
||||||
return []string{"Contact Name"}
|
|
||||||
|
|
||||||
case ExchangeMail:
|
|
||||||
return []string{"Sender", "Folder", "Subject", "Received"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns the values matching the Headers list for printing
|
|
||||||
// out to a terminal in a columnar display.
|
|
||||||
func (i ExchangeInfo) Values() []string {
|
|
||||||
switch i.ItemType {
|
|
||||||
case ExchangeEvent:
|
|
||||||
return []string{
|
|
||||||
i.Organizer,
|
|
||||||
i.Subject,
|
|
||||||
dttm.FormatToTabularDisplay(i.EventStart),
|
|
||||||
dttm.FormatToTabularDisplay(i.EventEnd),
|
|
||||||
strconv.FormatBool(i.EventRecurs),
|
|
||||||
}
|
|
||||||
|
|
||||||
case ExchangeContact:
|
|
||||||
return []string{i.ContactName}
|
|
||||||
|
|
||||||
case ExchangeMail:
|
|
||||||
return []string{
|
|
||||||
i.Sender, i.ParentPath, i.Subject,
|
|
||||||
dttm.FormatToTabularDisplay(i.Received),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ExchangeInfo) UpdateParentPath(newLocPath *path.Builder) {
|
|
||||||
i.ParentPath = newLocPath.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ExchangeInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
||||||
var category path.CategoryType
|
|
||||||
|
|
||||||
switch i.ItemType {
|
|
||||||
case ExchangeEvent:
|
|
||||||
category = path.EventsCategory
|
|
||||||
case ExchangeContact:
|
|
||||||
category = path.ContactsCategory
|
|
||||||
case ExchangeMail:
|
|
||||||
category = path.EmailCategory
|
|
||||||
}
|
|
||||||
|
|
||||||
loc, err := NewExchangeLocationIDer(category, baseLoc.Elements()...)
|
|
||||||
|
|
||||||
return &loc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ExchangeInfo) updateFolder(f *FolderInfo) error {
|
|
||||||
// Use a switch instead of a rather large if-statement. Just make sure it's an
|
|
||||||
// Exchange type. If it's not return an error.
|
|
||||||
switch i.ItemType {
|
|
||||||
case ExchangeContact, ExchangeEvent, ExchangeMail:
|
|
||||||
default:
|
|
||||||
return clues.New("unsupported non-Exchange ItemType").
|
|
||||||
With("item_type", i.ItemType)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.DataType = i.ItemType
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SharePointInfo describes a sharepoint item
|
|
||||||
type SharePointInfo struct {
|
|
||||||
Created time.Time `json:"created,omitempty"`
|
|
||||||
DriveName string `json:"driveName,omitempty"`
|
|
||||||
DriveID string `json:"driveID,omitempty"`
|
|
||||||
ItemName string `json:"itemName,omitempty"`
|
|
||||||
ItemType ItemType `json:"itemType,omitempty"`
|
|
||||||
Modified time.Time `json:"modified,omitempty"`
|
|
||||||
Owner string `json:"owner,omitempty"`
|
|
||||||
ParentPath string `json:"parentPath,omitempty"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
WebURL string `json:"webUrl,omitempty"`
|
|
||||||
SiteID string `json:"siteID,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a SharePointInfo
|
|
||||||
// for printing out to a terminal in a columnar display.
|
|
||||||
func (i SharePointInfo) Headers() []string {
|
|
||||||
return []string{"ItemName", "Library", "ParentPath", "Size", "Owner", "Created", "Modified"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns the values matching the Headers list for printing
|
|
||||||
// out to a terminal in a columnar display.
|
|
||||||
func (i SharePointInfo) Values() []string {
|
|
||||||
return []string{
|
|
||||||
i.ItemName,
|
|
||||||
i.DriveName,
|
|
||||||
i.ParentPath,
|
|
||||||
humanize.Bytes(uint64(i.Size)),
|
|
||||||
i.Owner,
|
|
||||||
dttm.FormatToTabularDisplay(i.Created),
|
|
||||||
dttm.FormatToTabularDisplay(i.Modified),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *SharePointInfo) UpdateParentPath(newLocPath *path.Builder) {
|
|
||||||
i.ParentPath = newLocPath.PopFront().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *SharePointInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
||||||
if len(i.DriveID) == 0 {
|
|
||||||
return nil, clues.New("empty drive ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
loc := NewSharePointLocationIDer(i.DriveID, baseLoc.Elements()...)
|
|
||||||
|
|
||||||
return &loc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *SharePointInfo) updateFolder(f *FolderInfo) error {
|
|
||||||
// TODO(ashmrtn): Change to just SharePointLibrary when the code that
|
|
||||||
// generates the item type is fixed.
|
|
||||||
if i.ItemType == OneDriveItem || i.ItemType == SharePointLibrary {
|
|
||||||
return updateFolderWithinDrive(SharePointLibrary, i.DriveName, i.DriveID, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return clues.New("unsupported non-SharePoint ItemType").With("item_type", i.ItemType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OneDriveInfo describes a oneDrive item
|
|
||||||
type OneDriveInfo struct {
|
|
||||||
Created time.Time `json:"created,omitempty"`
|
|
||||||
DriveID string `json:"driveID,omitempty"`
|
|
||||||
DriveName string `json:"driveName,omitempty"`
|
|
||||||
IsMeta bool `json:"isMeta,omitempty"`
|
|
||||||
ItemName string `json:"itemName,omitempty"`
|
|
||||||
ItemType ItemType `json:"itemType,omitempty"`
|
|
||||||
Modified time.Time `json:"modified,omitempty"`
|
|
||||||
Owner string `json:"owner,omitempty"`
|
|
||||||
ParentPath string `json:"parentPath"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a OneDriveInfo
|
|
||||||
// for printing out to a terminal in a columnar display.
|
|
||||||
func (i OneDriveInfo) Headers() []string {
|
|
||||||
return []string{"ItemName", "ParentPath", "Size", "Owner", "Created", "Modified"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Values returns the values matching the Headers list for printing
|
|
||||||
// out to a terminal in a columnar display.
|
|
||||||
func (i OneDriveInfo) Values() []string {
|
|
||||||
return []string{
|
|
||||||
i.ItemName,
|
|
||||||
i.ParentPath,
|
|
||||||
humanize.Bytes(uint64(i.Size)),
|
|
||||||
i.Owner,
|
|
||||||
dttm.FormatToTabularDisplay(i.Created),
|
|
||||||
dttm.FormatToTabularDisplay(i.Modified),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *OneDriveInfo) UpdateParentPath(newLocPath *path.Builder) {
|
|
||||||
i.ParentPath = newLocPath.PopFront().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *OneDriveInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
|
||||||
if len(i.DriveID) == 0 {
|
|
||||||
return nil, clues.New("empty drive ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
loc := NewOneDriveLocationIDer(i.DriveID, baseLoc.Elements()...)
|
|
||||||
|
|
||||||
return &loc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *OneDriveInfo) updateFolder(f *FolderInfo) error {
|
|
||||||
return updateFolderWithinDrive(OneDriveItem, i.DriveName, i.DriveID, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateFolderWithinDrive(
|
func updateFolderWithinDrive(
|
||||||
t ItemType,
|
t ItemType,
|
||||||
|
|||||||
175
src/pkg/backup/details/entry.go
Normal file
175
src/pkg/backup/details/entry.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/cli/print"
|
||||||
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add a new type so we can transparently use PrintAll in different situations.
|
||||||
|
type entrySet []*Entry
|
||||||
|
|
||||||
|
func (ents entrySet) PrintEntries(ctx context.Context) {
|
||||||
|
printEntries(ctx, ents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaybePrintEntries is same as PrintEntries, but only prints if we
|
||||||
|
// have less than 15 items or is not json output.
|
||||||
|
func (ents entrySet) MaybePrintEntries(ctx context.Context) {
|
||||||
|
if len(ents) <= maxPrintLimit ||
|
||||||
|
print.DisplayJSONFormat() ||
|
||||||
|
print.DisplayVerbose() {
|
||||||
|
printEntries(ctx, ents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry describes a single item stored in a Backup
|
||||||
|
type Entry struct {
|
||||||
|
// RepoRef is the full storage path of the item in Kopia
|
||||||
|
RepoRef string `json:"repoRef"`
|
||||||
|
ShortRef string `json:"shortRef"`
|
||||||
|
ParentRef string `json:"parentRef,omitempty"`
|
||||||
|
|
||||||
|
// LocationRef contains the logical path structure by its human-readable
|
||||||
|
// display names. IE: If an item is located at "/Inbox/Important", we
|
||||||
|
// hold that string in the LocationRef, while the actual IDs of each
|
||||||
|
// container are used for the RepoRef.
|
||||||
|
// LocationRef only holds the container values, and does not include
|
||||||
|
// the metadata prefixes (tenant, service, owner, etc) found in the
|
||||||
|
// repoRef.
|
||||||
|
// Currently only implemented for Exchange Calendars.
|
||||||
|
LocationRef string `json:"locationRef,omitempty"`
|
||||||
|
|
||||||
|
// ItemRef contains the stable id of the item itself. ItemRef is not
|
||||||
|
// guaranteed to be unique within a repository. Uniqueness guarantees
|
||||||
|
// maximally inherit from the source item. Eg: Entries for m365 mail items
|
||||||
|
// are only as unique as m365 mail item IDs themselves.
|
||||||
|
ItemRef string `json:"itemRef,omitempty"`
|
||||||
|
|
||||||
|
// Indicates the item was added or updated in this backup
|
||||||
|
// Always `true` for full backups
|
||||||
|
Updated bool `json:"updated"`
|
||||||
|
|
||||||
|
ItemInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLocationIDer takes a backup version and produces the unique location for
|
||||||
|
// this entry if possible. Reasons it may not be possible to produce the unique
|
||||||
|
// location include an unsupported backup version or missing information.
|
||||||
|
func (de Entry) ToLocationIDer(backupVersion int) (LocationIDer, error) {
|
||||||
|
if len(de.LocationRef) > 0 {
|
||||||
|
baseLoc, err := path.Builder{}.SplitUnescapeAppend(de.LocationRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "parsing base location info").
|
||||||
|
With("location_ref", de.LocationRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual services may add additional info to the base and return that.
|
||||||
|
return de.ItemInfo.uniqueLocation(baseLoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if backupVersion >= version.OneDrive7LocationRef ||
|
||||||
|
(de.ItemInfo.infoType() != OneDriveItem &&
|
||||||
|
de.ItemInfo.infoType() != SharePointLibrary) {
|
||||||
|
return nil, clues.New("no previous location for entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a little hacky, but we only want to try to extract the old
|
||||||
|
// location if it's OneDrive or SharePoint libraries and it's known to
|
||||||
|
// be an older backup version.
|
||||||
|
//
|
||||||
|
// TODO(ashmrtn): Remove this code once OneDrive/SharePoint libraries
|
||||||
|
// LocationRef code has been out long enough that all delta tokens for
|
||||||
|
// previous backup versions will have expired. At that point, either
|
||||||
|
// we'll do a full backup (token expired, no newer backups) or have a
|
||||||
|
// backup of a higher version with the information we need.
|
||||||
|
rr, err := path.FromDataLayerPath(de.RepoRef, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Wrap(err, "getting item RepoRef")
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := path.ToDrivePath(rr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.New("converting RepoRef to drive path")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseLoc := path.Builder{}.Append(p.Root).Append(p.Folders...)
|
||||||
|
|
||||||
|
// Individual services may add additional info to the base and return that.
|
||||||
|
return de.ItemInfo.uniqueLocation(baseLoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file is a metadata file. These are used to store
|
||||||
|
// additional data like permissions (in case of Drive items) and are
|
||||||
|
// not to be treated as regular files.
|
||||||
|
func (de Entry) isMetaFile() bool {
|
||||||
|
// sharepoint types not needed, since sharepoint permissions were
|
||||||
|
// added after IsMeta was deprecated.
|
||||||
|
// Earlier onedrive backups used to store both metafiles and files in details.
|
||||||
|
// So filter out just the onedrive items and check for metafiles
|
||||||
|
return de.ItemInfo.OneDrive != nil && de.ItemInfo.OneDrive.IsMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
// CLI Output
|
||||||
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// interface compliance checks
|
||||||
|
var _ print.Printable = &Entry{}
|
||||||
|
|
||||||
|
// MinimumPrintable DetailsEntries is a passthrough func, because no
|
||||||
|
// reduction is needed for the json output.
|
||||||
|
func (de Entry) MinimumPrintable() any {
|
||||||
|
return de
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in a DetailsEntry
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (de Entry) Headers() []string {
|
||||||
|
hs := []string{"ID"}
|
||||||
|
|
||||||
|
if de.ItemInfo.Folder != nil {
|
||||||
|
hs = append(hs, de.ItemInfo.Folder.Headers()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.ItemInfo.Exchange != nil {
|
||||||
|
hs = append(hs, de.ItemInfo.Exchange.Headers()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.ItemInfo.SharePoint != nil {
|
||||||
|
hs = append(hs, de.ItemInfo.SharePoint.Headers()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.ItemInfo.OneDrive != nil {
|
||||||
|
hs = append(hs, de.ItemInfo.OneDrive.Headers()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list.
|
||||||
|
func (de Entry) Values() []string {
|
||||||
|
vs := []string{de.ShortRef}
|
||||||
|
|
||||||
|
if de.ItemInfo.Folder != nil {
|
||||||
|
vs = append(vs, de.ItemInfo.Folder.Values()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.ItemInfo.Exchange != nil {
|
||||||
|
vs = append(vs, de.ItemInfo.Exchange.Values()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.ItemInfo.SharePoint != nil {
|
||||||
|
vs = append(vs, de.ItemInfo.SharePoint.Values()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if de.ItemInfo.OneDrive != nil {
|
||||||
|
vs = append(vs, de.ItemInfo.OneDrive.Values()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vs
|
||||||
|
}
|
||||||
127
src/pkg/backup/details/exchange.go
Normal file
127
src/pkg/backup/details/exchange.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewExchangeLocationIDer builds a LocationIDer for the given category and
|
||||||
|
// folder path. The path denoted by the folders should be unique within the
|
||||||
|
// category.
|
||||||
|
func NewExchangeLocationIDer(
|
||||||
|
category path.CategoryType,
|
||||||
|
escapedFolders ...string,
|
||||||
|
) (uniqueLoc, error) {
|
||||||
|
if err := path.ValidateServiceAndCategory(path.ExchangeService, category); err != nil {
|
||||||
|
return uniqueLoc{}, clues.Wrap(err, "making exchange LocationIDer")
|
||||||
|
}
|
||||||
|
|
||||||
|
pb := path.Builder{}.Append(category.String()).Append(escapedFolders...)
|
||||||
|
|
||||||
|
return uniqueLoc{
|
||||||
|
pb: pb,
|
||||||
|
prefixElems: 1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeInfo describes an exchange item
|
||||||
|
type ExchangeInfo struct {
|
||||||
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
|
Sender string `json:"sender,omitempty"`
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
Recipient []string `json:"recipient,omitempty"`
|
||||||
|
ParentPath string `json:"parentPath,omitempty"`
|
||||||
|
Received time.Time `json:"received,omitempty"`
|
||||||
|
EventStart time.Time `json:"eventStart,omitempty"`
|
||||||
|
EventEnd time.Time `json:"eventEnd,omitempty"`
|
||||||
|
Organizer string `json:"organizer,omitempty"`
|
||||||
|
ContactName string `json:"contactName,omitempty"`
|
||||||
|
EventRecurs bool `json:"eventRecurs,omitempty"`
|
||||||
|
Created time.Time `json:"created,omitempty"`
|
||||||
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in an ExchangeInfo
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (i ExchangeInfo) Headers() []string {
|
||||||
|
switch i.ItemType {
|
||||||
|
case ExchangeEvent:
|
||||||
|
return []string{"Organizer", "Subject", "Starts", "Ends", "Recurring"}
|
||||||
|
|
||||||
|
case ExchangeContact:
|
||||||
|
return []string{"Contact Name"}
|
||||||
|
|
||||||
|
case ExchangeMail:
|
||||||
|
return []string{"Sender", "Folder", "Subject", "Received"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list for printing
|
||||||
|
// out to a terminal in a columnar display.
|
||||||
|
func (i ExchangeInfo) Values() []string {
|
||||||
|
switch i.ItemType {
|
||||||
|
case ExchangeEvent:
|
||||||
|
return []string{
|
||||||
|
i.Organizer,
|
||||||
|
i.Subject,
|
||||||
|
dttm.FormatToTabularDisplay(i.EventStart),
|
||||||
|
dttm.FormatToTabularDisplay(i.EventEnd),
|
||||||
|
strconv.FormatBool(i.EventRecurs),
|
||||||
|
}
|
||||||
|
|
||||||
|
case ExchangeContact:
|
||||||
|
return []string{i.ContactName}
|
||||||
|
|
||||||
|
case ExchangeMail:
|
||||||
|
return []string{
|
||||||
|
i.Sender, i.ParentPath, i.Subject,
|
||||||
|
dttm.FormatToTabularDisplay(i.Received),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ExchangeInfo) UpdateParentPath(newLocPath *path.Builder) {
|
||||||
|
i.ParentPath = newLocPath.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ExchangeInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
||||||
|
var category path.CategoryType
|
||||||
|
|
||||||
|
switch i.ItemType {
|
||||||
|
case ExchangeEvent:
|
||||||
|
category = path.EventsCategory
|
||||||
|
case ExchangeContact:
|
||||||
|
category = path.ContactsCategory
|
||||||
|
case ExchangeMail:
|
||||||
|
category = path.EmailCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, err := NewExchangeLocationIDer(category, baseLoc.Elements()...)
|
||||||
|
|
||||||
|
return &loc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ExchangeInfo) updateFolder(f *FolderInfo) error {
|
||||||
|
// Use a switch instead of a rather large if-statement. Just make sure it's an
|
||||||
|
// Exchange type. If it's not return an error.
|
||||||
|
switch i.ItemType {
|
||||||
|
case ExchangeContact, ExchangeEvent, ExchangeMail:
|
||||||
|
default:
|
||||||
|
return clues.New("unsupported non-Exchange ItemType").
|
||||||
|
With("item_type", i.ItemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.DataType = i.ItemType
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
21
src/pkg/backup/details/folder.go
Normal file
21
src/pkg/backup/details/folder.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type FolderInfo struct {
|
||||||
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
DataType ItemType `json:"dataType,omitempty"`
|
||||||
|
DriveName string `json:"driveName,omitempty"`
|
||||||
|
DriveID string `json:"driveID,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i FolderInfo) Headers() []string {
|
||||||
|
return []string{"Display Name"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i FolderInfo) Values() []string {
|
||||||
|
return []string{i.DisplayName}
|
||||||
|
}
|
||||||
59
src/pkg/backup/details/groups.go
Normal file
59
src/pkg/backup/details/groups.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGroupsLocationIDer builds a LocationIDer for the groups.
|
||||||
|
func NewGroupsLocationIDer(
|
||||||
|
driveID string,
|
||||||
|
escapedFolders ...string,
|
||||||
|
) uniqueLoc {
|
||||||
|
// TODO: implement
|
||||||
|
return uniqueLoc{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupsInfo describes a groups item
|
||||||
|
type GroupsInfo struct {
|
||||||
|
Created time.Time `json:"created,omitempty"`
|
||||||
|
DriveName string `json:"driveName,omitempty"`
|
||||||
|
DriveID string `json:"driveID,omitempty"`
|
||||||
|
ItemName string `json:"itemName,omitempty"`
|
||||||
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
ParentPath string `json:"parentPath,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in a SharePointInfo
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (i GroupsInfo) Headers() []string {
|
||||||
|
return []string{"Created", "Modified"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list for printing
|
||||||
|
// out to a terminal in a columnar display.
|
||||||
|
func (i GroupsInfo) Values() []string {
|
||||||
|
return []string{
|
||||||
|
dttm.FormatToTabularDisplay(i.Created),
|
||||||
|
dttm.FormatToTabularDisplay(i.Modified),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *GroupsInfo) UpdateParentPath(newLocPath *path.Builder) {
|
||||||
|
i.ParentPath = newLocPath.PopFront().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *GroupsInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
||||||
|
return nil, clues.New("not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *GroupsInfo) updateFolder(f *FolderInfo) error {
|
||||||
|
return clues.New("not yet implemented")
|
||||||
|
}
|
||||||
169
src/pkg/backup/details/iteminfo.go
Normal file
169
src/pkg/backup/details/iteminfo.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ItemType int
|
||||||
|
|
||||||
|
// ItemTypes are enumerated by service (hundredth digit) and data type (ones digit).
|
||||||
|
// Ex: exchange is 00x where x is the data type. Sharepoint is 10x, and etc.
|
||||||
|
// Every item info struct should get its own hundredth enumeration entry.
|
||||||
|
// Every item category for that service should get its own entry (even if differences
|
||||||
|
// between types aren't apparent on initial implementation, this future-proofs
|
||||||
|
// against breaking changes).
|
||||||
|
// Entries should not be rearranged.
|
||||||
|
// Additionally, any itemType directly assigned a number should not be altered.
|
||||||
|
// This applies to OneDriveItem and FolderItem
|
||||||
|
const (
|
||||||
|
UnknownType ItemType = iota // 0, global unknown value
|
||||||
|
|
||||||
|
// Exchange (00x)
|
||||||
|
ExchangeContact
|
||||||
|
ExchangeEvent
|
||||||
|
ExchangeMail
|
||||||
|
// SharePoint (10x)
|
||||||
|
SharePointLibrary ItemType = iota + 97 // 100
|
||||||
|
SharePointList // 101...
|
||||||
|
SharePointPage
|
||||||
|
|
||||||
|
// OneDrive (20x)
|
||||||
|
OneDriveItem ItemType = 205
|
||||||
|
|
||||||
|
// Folder Management(30x)
|
||||||
|
FolderItem ItemType = 306
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateItem(item *ItemInfo, newLocPath *path.Builder) {
|
||||||
|
// Only OneDrive and SharePoint have information about parent folders
|
||||||
|
// contained in them.
|
||||||
|
// Can't switch based on infoType because that's been unstable.
|
||||||
|
if item.Exchange != nil {
|
||||||
|
item.Exchange.UpdateParentPath(newLocPath)
|
||||||
|
} else if item.SharePoint != nil {
|
||||||
|
// SharePoint used to store library items with the OneDriveItem ItemType.
|
||||||
|
// Start switching them over as we see them since there's no point in
|
||||||
|
// keeping the old format.
|
||||||
|
if item.SharePoint.ItemType == OneDriveItem {
|
||||||
|
item.SharePoint.ItemType = SharePointLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
item.SharePoint.UpdateParentPath(newLocPath)
|
||||||
|
} else if item.OneDrive != nil {
|
||||||
|
item.OneDrive.UpdateParentPath(newLocPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemInfo is a oneOf that contains service specific
|
||||||
|
// information about the item it tracks
|
||||||
|
type ItemInfo struct {
|
||||||
|
Folder *FolderInfo `json:"folder,omitempty"`
|
||||||
|
Exchange *ExchangeInfo `json:"exchange,omitempty"`
|
||||||
|
SharePoint *SharePointInfo `json:"sharePoint,omitempty"`
|
||||||
|
OneDrive *OneDriveInfo `json:"oneDrive,omitempty"`
|
||||||
|
Groups *GroupsInfo `json:"groups,omitempty"`
|
||||||
|
// Optional item extension data
|
||||||
|
Extension *ExtensionData `json:"extension,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// typedInfo should get embedded in each sesrvice type to track
|
||||||
|
// the type of item it stores for multi-item service support.
|
||||||
|
|
||||||
|
// infoType provides internal categorization for collecting like-typed ItemInfos.
|
||||||
|
// It should return the most granular value type (ex: "event" for an exchange
|
||||||
|
// calendar event).
|
||||||
|
func (i ItemInfo) infoType() ItemType {
|
||||||
|
switch {
|
||||||
|
case i.Folder != nil:
|
||||||
|
return i.Folder.ItemType
|
||||||
|
|
||||||
|
case i.Exchange != nil:
|
||||||
|
return i.Exchange.ItemType
|
||||||
|
|
||||||
|
case i.SharePoint != nil:
|
||||||
|
return i.SharePoint.ItemType
|
||||||
|
|
||||||
|
case i.OneDrive != nil:
|
||||||
|
return i.OneDrive.ItemType
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnknownType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ItemInfo) size() int64 {
|
||||||
|
switch {
|
||||||
|
case i.Exchange != nil:
|
||||||
|
return i.Exchange.Size
|
||||||
|
|
||||||
|
case i.OneDrive != nil:
|
||||||
|
return i.OneDrive.Size
|
||||||
|
|
||||||
|
case i.SharePoint != nil:
|
||||||
|
return i.SharePoint.Size
|
||||||
|
|
||||||
|
case i.Folder != nil:
|
||||||
|
return i.Folder.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ItemInfo) Modified() time.Time {
|
||||||
|
switch {
|
||||||
|
case i.Exchange != nil:
|
||||||
|
return i.Exchange.Modified
|
||||||
|
|
||||||
|
case i.OneDrive != nil:
|
||||||
|
return i.OneDrive.Modified
|
||||||
|
|
||||||
|
case i.SharePoint != nil:
|
||||||
|
return i.SharePoint.Modified
|
||||||
|
|
||||||
|
case i.Folder != nil:
|
||||||
|
return i.Folder.Modified
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ItemInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
||||||
|
switch {
|
||||||
|
case i.Exchange != nil:
|
||||||
|
return i.Exchange.uniqueLocation(baseLoc)
|
||||||
|
|
||||||
|
case i.OneDrive != nil:
|
||||||
|
return i.OneDrive.uniqueLocation(baseLoc)
|
||||||
|
|
||||||
|
case i.SharePoint != nil:
|
||||||
|
return i.SharePoint.uniqueLocation(baseLoc)
|
||||||
|
|
||||||
|
case i.Groups != nil:
|
||||||
|
return i.Groups.uniqueLocation(baseLoc)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, clues.New("unsupported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ItemInfo) updateFolder(f *FolderInfo) error {
|
||||||
|
switch {
|
||||||
|
case i.Exchange != nil:
|
||||||
|
return i.Exchange.updateFolder(f)
|
||||||
|
|
||||||
|
case i.OneDrive != nil:
|
||||||
|
return i.OneDrive.updateFolder(f)
|
||||||
|
|
||||||
|
case i.SharePoint != nil:
|
||||||
|
return i.SharePoint.updateFolder(f)
|
||||||
|
|
||||||
|
case i.Groups != nil:
|
||||||
|
return i.Groups.updateFolder(f)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return clues.New("unsupported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/pkg/backup/details/model.go
Normal file
125
src/pkg/backup/details/model.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/cli/print"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetailsModel describes what was stored in a Backup
|
||||||
|
type DetailsModel struct {
|
||||||
|
Entries []Entry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print writes the DetailModel Entries to StdOut, in the format
|
||||||
|
// requested by the caller.
|
||||||
|
func (dm DetailsModel) PrintEntries(ctx context.Context) {
|
||||||
|
printEntries(ctx, dm.Entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
type infoer interface {
|
||||||
|
Entry | *Entry
|
||||||
|
// Need this here so we can access the infoType function without a type
|
||||||
|
// assertion. See https://stackoverflow.com/a/71378366 for more details.
|
||||||
|
infoType() ItemType
|
||||||
|
}
|
||||||
|
|
||||||
|
func printEntries[T infoer](ctx context.Context, entries []T) {
|
||||||
|
if print.DisplayJSONFormat() {
|
||||||
|
printJSON(ctx, entries)
|
||||||
|
} else {
|
||||||
|
printTable(ctx, entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTable[T infoer](ctx context.Context, entries []T) {
|
||||||
|
perType := map[ItemType][]print.Printable{}
|
||||||
|
|
||||||
|
for _, ent := range entries {
|
||||||
|
it := ent.infoType()
|
||||||
|
ps, ok := perType[it]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ps = []print.Printable{}
|
||||||
|
}
|
||||||
|
|
||||||
|
perType[it] = append(ps, print.Printable(ent))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ps := range perType {
|
||||||
|
print.All(ctx, ps...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printJSON[T infoer](ctx context.Context, entries []T) {
|
||||||
|
ents := []print.Printable{}
|
||||||
|
|
||||||
|
for _, ent := range entries {
|
||||||
|
ents = append(ents, print.Printable(ent))
|
||||||
|
}
|
||||||
|
|
||||||
|
print.All(ctx, ents...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths returns the list of Paths for non-folder and non-meta items extracted
|
||||||
|
// from the Entries slice.
|
||||||
|
func (dm DetailsModel) Paths() []string {
|
||||||
|
r := make([]string, 0, len(dm.Entries))
|
||||||
|
|
||||||
|
for _, ent := range dm.Entries {
|
||||||
|
if ent.Folder != nil || ent.isMetaFile() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r = append(r, ent.RepoRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items returns a slice of *ItemInfo that does not contain any FolderInfo
|
||||||
|
// entries. Required because not all folders in the details are valid resource
|
||||||
|
// paths, and we want to slice out metadata.
|
||||||
|
func (dm DetailsModel) Items() entrySet {
|
||||||
|
res := make([]*Entry, 0, len(dm.Entries))
|
||||||
|
|
||||||
|
for i := 0; i < len(dm.Entries); i++ {
|
||||||
|
ent := dm.Entries[i]
|
||||||
|
if ent.Folder != nil || ent.isMetaFile() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, &ent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterMetaFiles returns a copy of the Details with all of the
|
||||||
|
// .meta files removed from the entries.
|
||||||
|
func (dm DetailsModel) FilterMetaFiles() DetailsModel {
|
||||||
|
d2 := DetailsModel{
|
||||||
|
Entries: []Entry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ent := range dm.Entries {
|
||||||
|
if !ent.isMetaFile() {
|
||||||
|
d2.Entries = append(d2.Entries, ent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d2
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumNonMetaFileSizes returns the total size of items excluding all the
|
||||||
|
// .meta files from the items.
|
||||||
|
func (dm DetailsModel) SumNonMetaFileSizes() int64 {
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
// Items will provide only files and filter out folders
|
||||||
|
for _, ent := range dm.FilterMetaFiles().Items() {
|
||||||
|
size += ent.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
78
src/pkg/backup/details/onedrive.go
Normal file
78
src/pkg/backup/details/onedrive.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewOneDriveLocationIDer builds a LocationIDer for the drive and folder path.
|
||||||
|
// The path denoted by the folders should be unique within the drive.
|
||||||
|
func NewOneDriveLocationIDer(
|
||||||
|
driveID string,
|
||||||
|
escapedFolders ...string,
|
||||||
|
) uniqueLoc {
|
||||||
|
pb := path.Builder{}.
|
||||||
|
Append(path.FilesCategory.String(), driveID).
|
||||||
|
Append(escapedFolders...)
|
||||||
|
|
||||||
|
return uniqueLoc{
|
||||||
|
pb: pb,
|
||||||
|
prefixElems: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneDriveInfo describes a oneDrive item
|
||||||
|
type OneDriveInfo struct {
|
||||||
|
Created time.Time `json:"created,omitempty"`
|
||||||
|
DriveID string `json:"driveID,omitempty"`
|
||||||
|
DriveName string `json:"driveName,omitempty"`
|
||||||
|
IsMeta bool `json:"isMeta,omitempty"`
|
||||||
|
ItemName string `json:"itemName,omitempty"`
|
||||||
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
ParentPath string `json:"parentPath"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in a OneDriveInfo
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (i OneDriveInfo) Headers() []string {
|
||||||
|
return []string{"ItemName", "ParentPath", "Size", "Owner", "Created", "Modified"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list for printing
|
||||||
|
// out to a terminal in a columnar display.
|
||||||
|
func (i OneDriveInfo) Values() []string {
|
||||||
|
return []string{
|
||||||
|
i.ItemName,
|
||||||
|
i.ParentPath,
|
||||||
|
humanize.Bytes(uint64(i.Size)),
|
||||||
|
i.Owner,
|
||||||
|
dttm.FormatToTabularDisplay(i.Created),
|
||||||
|
dttm.FormatToTabularDisplay(i.Modified),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *OneDriveInfo) UpdateParentPath(newLocPath *path.Builder) {
|
||||||
|
i.ParentPath = newLocPath.PopFront().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *OneDriveInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
||||||
|
if len(i.DriveID) == 0 {
|
||||||
|
return nil, clues.New("empty drive ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := NewOneDriveLocationIDer(i.DriveID, baseLoc.Elements()...)
|
||||||
|
|
||||||
|
return &loc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *OneDriveInfo) updateFolder(f *FolderInfo) error {
|
||||||
|
return updateFolderWithinDrive(OneDriveItem, i.DriveName, i.DriveID, f)
|
||||||
|
}
|
||||||
86
src/pkg/backup/details/sharepoint.go
Normal file
86
src/pkg/backup/details/sharepoint.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/common/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSharePointLocationIDer builds a LocationIDer for the drive and folder
|
||||||
|
// path. The path denoted by the folders should be unique within the drive.
|
||||||
|
func NewSharePointLocationIDer(
|
||||||
|
driveID string,
|
||||||
|
escapedFolders ...string,
|
||||||
|
) uniqueLoc {
|
||||||
|
pb := path.Builder{}.
|
||||||
|
Append(path.LibrariesCategory.String(), driveID).
|
||||||
|
Append(escapedFolders...)
|
||||||
|
|
||||||
|
return uniqueLoc{
|
||||||
|
pb: pb,
|
||||||
|
prefixElems: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharePointInfo describes a sharepoint item
|
||||||
|
type SharePointInfo struct {
|
||||||
|
Created time.Time `json:"created,omitempty"`
|
||||||
|
DriveName string `json:"driveName,omitempty"`
|
||||||
|
DriveID string `json:"driveID,omitempty"`
|
||||||
|
ItemName string `json:"itemName,omitempty"`
|
||||||
|
ItemType ItemType `json:"itemType,omitempty"`
|
||||||
|
Modified time.Time `json:"modified,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty"`
|
||||||
|
ParentPath string `json:"parentPath,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
WebURL string `json:"webUrl,omitempty"`
|
||||||
|
SiteID string `json:"siteID,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in a SharePointInfo
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (i SharePointInfo) Headers() []string {
|
||||||
|
return []string{"ItemName", "Library", "ParentPath", "Size", "Owner", "Created", "Modified"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list for printing
|
||||||
|
// out to a terminal in a columnar display.
|
||||||
|
func (i SharePointInfo) Values() []string {
|
||||||
|
return []string{
|
||||||
|
i.ItemName,
|
||||||
|
i.DriveName,
|
||||||
|
i.ParentPath,
|
||||||
|
humanize.Bytes(uint64(i.Size)),
|
||||||
|
i.Owner,
|
||||||
|
dttm.FormatToTabularDisplay(i.Created),
|
||||||
|
dttm.FormatToTabularDisplay(i.Modified),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *SharePointInfo) UpdateParentPath(newLocPath *path.Builder) {
|
||||||
|
i.ParentPath = newLocPath.PopFront().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *SharePointInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) {
|
||||||
|
if len(i.DriveID) == 0 {
|
||||||
|
return nil, clues.New("empty drive ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := NewSharePointLocationIDer(i.DriveID, baseLoc.Elements()...)
|
||||||
|
|
||||||
|
return &loc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *SharePointInfo) updateFolder(f *FolderInfo) error {
|
||||||
|
// TODO(ashmrtn): Change to just SharePointLibrary when the code that
|
||||||
|
// generates the item type is fixed.
|
||||||
|
if i.ItemType == OneDriveItem || i.ItemType == SharePointLibrary {
|
||||||
|
return updateFolderWithinDrive(SharePointLibrary, i.DriveName, i.DriveID, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clues.New("unsupported non-SharePoint ItemType").With("item_type", i.ItemType)
|
||||||
|
}
|
||||||
513
src/pkg/selectors/groups.go
Normal file
513
src/pkg/selectors/groups.go
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
package selectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Selectors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type (
|
||||||
|
// groups provides an api for selecting
|
||||||
|
// data scopes applicable to the groups service.
|
||||||
|
groups struct {
|
||||||
|
Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// groups provides an api for selecting
|
||||||
|
// data scopes applicable to the groups service,
|
||||||
|
// plus backup-specific methods.
|
||||||
|
GroupsBackup struct {
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupsRestorep provides an api for selecting
|
||||||
|
// data scopes applicable to the Groups service,
|
||||||
|
// plus restore-specific methods.
|
||||||
|
GroupsRestore struct {
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Reducer = &GroupsRestore{}
|
||||||
|
_ pathCategorier = &GroupsRestore{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGroupsBackup produces a new Selector with the service set to ServiceGroups.
|
||||||
|
func NewGroupsBackup(resources []string) *GroupsBackup {
|
||||||
|
src := GroupsBackup{
|
||||||
|
groups{
|
||||||
|
newSelector(ServiceGroups, resources),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &src
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGroupsBackup transforms the generic selector into an GroupsBackup.
|
||||||
|
// Errors if the service defined by the selector is not ServiceGroups.
|
||||||
|
func (s Selector) ToGroupsBackup() (*GroupsBackup, error) {
|
||||||
|
if s.Service != ServiceGroups {
|
||||||
|
return nil, badCastErr(ServiceGroups, s.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
src := GroupsBackup{groups{s}}
|
||||||
|
|
||||||
|
return &src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s GroupsBackup) SplitByResourceOwner(resources []string) []GroupsBackup {
|
||||||
|
sels := splitByResourceOwner[GroupsScope](s.Selector, resources, GroupsGroup)
|
||||||
|
|
||||||
|
ss := make([]GroupsBackup, 0, len(sels))
|
||||||
|
for _, sel := range sels {
|
||||||
|
ss = append(ss, GroupsBackup{groups{sel}})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGroupsRestore produces a new Selector with the service set to ServiceGroups.
|
||||||
|
func NewGroupsRestore(resources []string) *GroupsRestore {
|
||||||
|
src := GroupsRestore{
|
||||||
|
groups{
|
||||||
|
newSelector(ServiceGroups, resources),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &src
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGroupsRestore transforms the generic selector into an GroupsRestore.
|
||||||
|
// Errors if the service defined by the selector is not ServiceGroups.
|
||||||
|
func (s Selector) ToGroupsRestore() (*GroupsRestore, error) {
|
||||||
|
if s.Service != ServiceGroups {
|
||||||
|
return nil, badCastErr(ServiceGroups, s.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
src := GroupsRestore{groups{s}}
|
||||||
|
|
||||||
|
return &src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s GroupsRestore) SplitByResourceOwner(resources []string) []GroupsRestore {
|
||||||
|
sels := splitByResourceOwner[GroupsScope](s.Selector, resources, GroupsGroup)
|
||||||
|
|
||||||
|
ss := make([]GroupsRestore, 0, len(sels))
|
||||||
|
for _, sel := range sels {
|
||||||
|
ss = append(ss, GroupsRestore{groups{sel}})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathCategories produces the aggregation of discrete resources described by each type of scope.
|
||||||
|
func (s groups) PathCategories() selectorPathCategories {
|
||||||
|
return selectorPathCategories{
|
||||||
|
Excludes: pathCategoriesIn[GroupsScope, groupsCategory](s.Excludes),
|
||||||
|
Filters: pathCategoriesIn[GroupsScope, groupsCategory](s.Filters),
|
||||||
|
Includes: pathCategoriesIn[GroupsScope, groupsCategory](s.Includes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Stringers and Concealers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s GroupsScope) Conceal() string { return conceal(s) }
|
||||||
|
func (s GroupsScope) Format(fs fmt.State, r rune) { format(s, fs, r) }
|
||||||
|
func (s GroupsScope) String() string { return conceal(s) }
|
||||||
|
func (s GroupsScope) PlainString() string { return plainString(s) }
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// Scope Factories
|
||||||
|
|
||||||
|
// Include appends the provided scopes to the selector's inclusion set.
|
||||||
|
// Data is included if it matches ANY inclusion.
|
||||||
|
// The inclusion set is later filtered (all included data must pass ALL
|
||||||
|
// filters) and excluded (all included data must not match ANY exclusion).
|
||||||
|
// Data is included if it matches ANY inclusion (of the same data category).
|
||||||
|
//
|
||||||
|
// All parts of the scope must match for data to be exclucded.
|
||||||
|
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
|
||||||
|
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
|
||||||
|
// a scope value. No value will match if selectors.None() is provided.
|
||||||
|
//
|
||||||
|
// Group-level scopes will automatically apply the Any() wildcard to
|
||||||
|
// child properties.
|
||||||
|
// ex: Site(u1) automatically cascades to all folders and files owned
|
||||||
|
// by s1.
|
||||||
|
func (s *groups) Include(scopes ...[]GroupsScope) {
|
||||||
|
s.Includes = appendScopes(s.Includes, scopes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude appends the provided scopes to the selector's exclusion set.
|
||||||
|
// Every Exclusion scope applies globally, affecting all inclusion scopes.
|
||||||
|
// Data is excluded if it matches ANY exclusion.
|
||||||
|
//
|
||||||
|
// All parts of the scope must match for data to be exclucded.
|
||||||
|
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
|
||||||
|
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
|
||||||
|
// a scope value. No value will match if selectors.None() is provided.
|
||||||
|
//
|
||||||
|
// Group-level scopes will automatically apply the Any() wildcard to
|
||||||
|
// child properties.
|
||||||
|
// ex: Site(u1) automatically cascades to all folders and files owned
|
||||||
|
// by s1.
|
||||||
|
func (s *groups) Exclude(scopes ...[]GroupsScope) {
|
||||||
|
s.Excludes = appendScopes(s.Excludes, scopes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter appends the provided scopes to the selector's filters set.
|
||||||
|
// A selector with >0 filters and 0 inclusions will include any data
|
||||||
|
// that passes all filters.
|
||||||
|
// A selector with >0 filters and >0 inclusions will reduce the
|
||||||
|
// inclusion set to only the data that passes all filters.
|
||||||
|
// Data is retained if it passes ALL filters.
|
||||||
|
//
|
||||||
|
// All parts of the scope must match for data to be exclucded.
|
||||||
|
// Ex: File(s1, f1, i1) => only excludes an item if it is owned by site s1,
|
||||||
|
// located in folder f1, and ID'd as i1. Use selectors.Any() to wildcard
|
||||||
|
// a scope value. No value will match if selectors.None() is provided.
|
||||||
|
//
|
||||||
|
// Group-level scopes will automatically apply the Any() wildcard to
|
||||||
|
// child properties.
|
||||||
|
// ex: Site(u1) automatically cascades to all folders and files owned
|
||||||
|
// by s1.
|
||||||
|
func (s *groups) Filter(scopes ...[]GroupsScope) {
|
||||||
|
s.Filters = appendScopes(s.Filters, scopes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes retrieves the list of groupsScopes in the selector.
|
||||||
|
func (s *groups) Scopes() []GroupsScope {
|
||||||
|
return scopes[GroupsScope](s.Selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// Scope Factories
|
||||||
|
|
||||||
|
// Produces one or more Groups site scopes.
|
||||||
|
// One scope is created per site entry.
|
||||||
|
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
|
||||||
|
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
|
||||||
|
// If any slice is empty, it defaults to [selectors.None]
|
||||||
|
func (s *groups) AllData() []GroupsScope {
|
||||||
|
scopes := []GroupsScope{}
|
||||||
|
|
||||||
|
scopes = append(
|
||||||
|
scopes,
|
||||||
|
makeScope[GroupsScope](GroupsTODOContainer, Any()))
|
||||||
|
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO produces one or more Groups TODO scopes.
|
||||||
|
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
|
||||||
|
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
|
||||||
|
// Any empty slice defaults to [selectors.None]
|
||||||
|
func (s *groups) TODO(lists []string, opts ...option) []GroupsScope {
|
||||||
|
var (
|
||||||
|
scopes = []GroupsScope{}
|
||||||
|
os = append([]option{pathComparator()}, opts...)
|
||||||
|
)
|
||||||
|
|
||||||
|
scopes = append(scopes, makeScope[GroupsScope](GroupsTODOContainer, lists, os...))
|
||||||
|
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListTODOItemsItems produces one or more Groups TODO item scopes.
|
||||||
|
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
|
||||||
|
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
|
||||||
|
// If any slice is empty, it defaults to [selectors.None]
|
||||||
|
// options are only applied to the list scopes.
|
||||||
|
func (s *groups) TODOItems(lists, items []string, opts ...option) []GroupsScope {
|
||||||
|
scopes := []GroupsScope{}
|
||||||
|
|
||||||
|
scopes = append(
|
||||||
|
scopes,
|
||||||
|
makeScope[GroupsScope](GroupsTODOItem, items, defaultItemOptions(s.Cfg)...).
|
||||||
|
set(GroupsTODOContainer, lists, opts...))
|
||||||
|
|
||||||
|
return scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------
|
||||||
|
// ItemInfo Factories
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Categories
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// groupsCategory enumerates the type of the lowest level
|
||||||
|
// of data () in a scope.
|
||||||
|
type groupsCategory string
|
||||||
|
|
||||||
|
// interface compliance checks
|
||||||
|
var _ categorizer = GroupsCategoryUnknown
|
||||||
|
|
||||||
|
const (
|
||||||
|
GroupsCategoryUnknown groupsCategory = ""
|
||||||
|
|
||||||
|
// types of data in Groups
|
||||||
|
GroupsGroup groupsCategory = "GroupsGroup"
|
||||||
|
GroupsTODOContainer groupsCategory = "GroupsTODOContainer"
|
||||||
|
GroupsTODOItem groupsCategory = "GroupsTODOItem"
|
||||||
|
|
||||||
|
// details.itemInfo comparables
|
||||||
|
|
||||||
|
// library drive selection
|
||||||
|
GroupsInfoSiteLibraryDrive groupsCategory = "GroupsInfoSiteLibraryDrive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// groupsLeafProperties describes common metadata of the leaf categories
|
||||||
|
var groupsLeafProperties = map[categorizer]leafProperty{
|
||||||
|
GroupsTODOItem: { // the root category must be represented, even though it isn't a leaf
|
||||||
|
pathKeys: []categorizer{GroupsTODOContainer, GroupsTODOItem},
|
||||||
|
pathType: path.UnknownCategory,
|
||||||
|
},
|
||||||
|
GroupsGroup: { // the root category must be represented, even though it isn't a leaf
|
||||||
|
pathKeys: []categorizer{GroupsGroup},
|
||||||
|
pathType: path.UnknownCategory,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c groupsCategory) String() string {
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// leafCat returns the leaf category of the receiver.
|
||||||
|
// If the receiver category has multiple leaves (ex: User) or no leaves,
|
||||||
|
// (ex: Unknown), the receiver itself is returned.
|
||||||
|
// Ex: ServiceTypeFolder.leafCat() => ServiceTypeItem
|
||||||
|
// Ex: ServiceUser.leafCat() => ServiceUser
|
||||||
|
func (c groupsCategory) leafCat() categorizer {
|
||||||
|
switch c {
|
||||||
|
case GroupsTODOContainer, GroupsInfoSiteLibraryDrive:
|
||||||
|
return GroupsTODOItem
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// rootCat returns the root category type.
|
||||||
|
func (c groupsCategory) rootCat() categorizer {
|
||||||
|
return GroupsGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknownCat returns the unknown category type.
|
||||||
|
func (c groupsCategory) unknownCat() categorizer {
|
||||||
|
return GroupsCategoryUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUnion returns true if the category is a site or a webURL, which
|
||||||
|
// can act as an alternative identifier to siteID across all site types.
|
||||||
|
func (c groupsCategory) isUnion() bool {
|
||||||
|
return c == c.rootCat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLeaf is true if the category is a GroupsItem category.
|
||||||
|
func (c groupsCategory) isLeaf() bool {
|
||||||
|
return c == c.leafCat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathValues transforms the two paths to maps of identified properties.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// [tenantID, service, siteID, category, folder, itemID]
|
||||||
|
// => {spFolder: folder, spItemID: itemID}
|
||||||
|
func (c groupsCategory) pathValues(
|
||||||
|
repo path.Path,
|
||||||
|
ent details.Entry,
|
||||||
|
cfg Config,
|
||||||
|
) (map[categorizer][]string, error) {
|
||||||
|
var (
|
||||||
|
folderCat, itemCat categorizer
|
||||||
|
itemID string
|
||||||
|
rFld string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case GroupsTODOContainer, GroupsTODOItem:
|
||||||
|
if ent.Groups == nil {
|
||||||
|
return nil, clues.New("no Groups ItemInfo in details")
|
||||||
|
}
|
||||||
|
|
||||||
|
folderCat, itemCat = GroupsTODOContainer, GroupsTODOItem
|
||||||
|
rFld = ent.Groups.ParentPath
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, clues.New("unrecognized groupsCategory").With("category", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
item := ent.ItemRef
|
||||||
|
if len(item) == 0 {
|
||||||
|
item = repo.Item()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.OnlyMatchItemNames {
|
||||||
|
item = ent.ItemInfo.Groups.ItemName
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[categorizer][]string{
|
||||||
|
folderCat: {rFld},
|
||||||
|
itemCat: {item, ent.ShortRef},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(itemID) > 0 {
|
||||||
|
result[itemCat] = append(result[itemCat], itemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathKeys returns the path keys recognized by the receiver's leaf type.
|
||||||
|
func (c groupsCategory) pathKeys() []categorizer {
|
||||||
|
return groupsLeafProperties[c.leafCat()].pathKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathType converts the category's leaf type into the matching path.CategoryType.
|
||||||
|
func (c groupsCategory) PathType() path.CategoryType {
|
||||||
|
return groupsLeafProperties[c.leafCat()].pathType
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scopes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// GroupsScope specifies the data available
|
||||||
|
// when interfacing with the Groups service.
|
||||||
|
type GroupsScope scope
|
||||||
|
|
||||||
|
// interface compliance checks
|
||||||
|
var _ scoper = &GroupsScope{}
|
||||||
|
|
||||||
|
// Category describes the type of the data in scope.
|
||||||
|
func (s GroupsScope) Category() groupsCategory {
|
||||||
|
return groupsCategory(getCategory(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// categorizer type is a generic wrapper around Category.
|
||||||
|
// Primarily used by scopes.go to for abstract comparisons.
|
||||||
|
func (s GroupsScope) categorizer() categorizer {
|
||||||
|
return s.Category()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches returns true if the category is included in the scope's
|
||||||
|
// data type, and the target string matches that category's comparator.
|
||||||
|
func (s GroupsScope) Matches(cat groupsCategory, target string) bool {
|
||||||
|
return matches(s, cat, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoCategory returns the category enum of the scope info.
|
||||||
|
// If the scope is not an info type, returns GroupsUnknownCategory.
|
||||||
|
func (s GroupsScope) InfoCategory() groupsCategory {
|
||||||
|
return groupsCategory(getInfoCategory(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncludeCategory checks whether the scope includes a
|
||||||
|
// certain category of data.
|
||||||
|
// Ex: to check if the scope includes file data:
|
||||||
|
// s.IncludesCategory(selector.GroupsFile)
|
||||||
|
func (s GroupsScope) IncludesCategory(cat groupsCategory) bool {
|
||||||
|
return categoryMatches(s.Category(), cat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the category is included in the scope's data type,
|
||||||
|
// and the value is set to Any().
|
||||||
|
func (s GroupsScope) IsAny(cat groupsCategory) bool {
|
||||||
|
return isAnyTarget(s, cat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the data category in the scope. If the scope
|
||||||
|
// contains all data types for a user, it'll return the
|
||||||
|
// GroupsUser category.
|
||||||
|
func (s GroupsScope) Get(cat groupsCategory) []string {
|
||||||
|
return getCatValue(s, cat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets a value by category to the scope. Only intended for internal use.
|
||||||
|
func (s GroupsScope) set(cat groupsCategory, v []string, opts ...option) GroupsScope {
|
||||||
|
os := []option{}
|
||||||
|
|
||||||
|
switch cat {
|
||||||
|
case GroupsTODOContainer:
|
||||||
|
os = append(os, pathComparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
return set(s, cat, v, append(os, opts...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDefaults ensures that site scopes express `AnyTgt` for their child category types.
|
||||||
|
func (s GroupsScope) setDefaults() {
|
||||||
|
switch s.Category() {
|
||||||
|
case GroupsGroup:
|
||||||
|
s[GroupsTODOContainer.String()] = passAny
|
||||||
|
s[GroupsTODOItem.String()] = passAny
|
||||||
|
case GroupsTODOContainer:
|
||||||
|
s[GroupsTODOItem.String()] = passAny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Backup Details Filtering
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Reduce filters the entries in a details struct to only those that match the
|
||||||
|
// inclusions, filters, and exclusions in the selector.
|
||||||
|
func (s groups) Reduce(
|
||||||
|
ctx context.Context,
|
||||||
|
deets *details.Details,
|
||||||
|
errs *fault.Bus,
|
||||||
|
) *details.Details {
|
||||||
|
return reduce[GroupsScope](
|
||||||
|
ctx,
|
||||||
|
deets,
|
||||||
|
s.Selector,
|
||||||
|
map[path.CategoryType]groupsCategory{
|
||||||
|
path.UnknownCategory: GroupsTODOItem,
|
||||||
|
},
|
||||||
|
errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesInfo handles the standard behavior when comparing a scope and an groupsInfo
|
||||||
|
// returns true if the scope and info match for the provided category.
|
||||||
|
func (s GroupsScope) matchesInfo(dii details.ItemInfo) bool {
|
||||||
|
var (
|
||||||
|
infoCat = s.InfoCategory()
|
||||||
|
i = ""
|
||||||
|
info = dii.Groups
|
||||||
|
)
|
||||||
|
|
||||||
|
if info == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch infoCat {
|
||||||
|
case GroupsInfoSiteLibraryDrive:
|
||||||
|
ds := []string{}
|
||||||
|
|
||||||
|
if len(info.DriveName) > 0 {
|
||||||
|
ds = append(ds, info.DriveName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(info.DriveID) > 0 {
|
||||||
|
ds = append(ds, info.DriveID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchesAny(s, GroupsInfoSiteLibraryDrive, ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Matches(infoCat, i)
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ const (
|
|||||||
ServiceExchange // Exchange
|
ServiceExchange // Exchange
|
||||||
ServiceOneDrive // OneDrive
|
ServiceOneDrive // OneDrive
|
||||||
ServiceSharePoint // SharePoint
|
ServiceSharePoint // SharePoint
|
||||||
|
ServiceGroups // Groups
|
||||||
)
|
)
|
||||||
|
|
||||||
var serviceToPathType = map[service]path.ServiceType{
|
var serviceToPathType = map[service]path.ServiceType{
|
||||||
|
|||||||
@ -12,11 +12,12 @@ func _() {
|
|||||||
_ = x[ServiceExchange-1]
|
_ = x[ServiceExchange-1]
|
||||||
_ = x[ServiceOneDrive-2]
|
_ = x[ServiceOneDrive-2]
|
||||||
_ = x[ServiceSharePoint-3]
|
_ = x[ServiceSharePoint-3]
|
||||||
|
_ = x[ServiceGroups-4]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _service_name = "Unknown ServiceExchangeOneDriveSharePoint"
|
const _service_name = "Unknown ServiceExchangeOneDriveSharePointGroups"
|
||||||
|
|
||||||
var _service_index = [...]uint8{0, 15, 23, 31, 41}
|
var _service_index = [...]uint8{0, 15, 23, 31, 41, 47}
|
||||||
|
|
||||||
func (i service) String() string {
|
func (i service) String() string {
|
||||||
if i < 0 || i >= service(len(_service_index)-1) {
|
if i < 0 || i >= service(len(_service_index)-1) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user