diff --git a/src/internal/m365/collection/drive/helper_test.go b/src/internal/m365/collection/drive/helper_test.go index 6825b6abb..36ccf1d6a 100644 --- a/src/internal/m365/collection/drive/helper_test.go +++ b/src/internal/m365/collection/drive/helper_test.go @@ -942,7 +942,7 @@ var defaultOneDriveLocationIDer = func(driveID string, elems ...string) details. } var defaultSharePointLocationIDer = func(driveID string, elems ...string) details.LocationIDer { - return details.NewSharePointLocationIDer(driveID, elems...) + return details.NewSharePointLocationIDer(path.LibrariesCategory, driveID, elems...) } func (h mockBackupHandler[T]) IsAllPass() bool { diff --git a/src/internal/m365/collection/drive/site_handler.go b/src/internal/m365/collection/drive/site_handler.go index 11adf8fa6..189131e04 100644 --- a/src/internal/m365/collection/drive/site_handler.go +++ b/src/internal/m365/collection/drive/site_handler.go @@ -149,7 +149,8 @@ func (h siteBackupHandler) NewLocationIDer( driveID string, elems ...string, ) details.LocationIDer { - return details.NewSharePointLocationIDer(driveID, elems...) + _, cat := h.ServiceCat() + return details.NewSharePointLocationIDer(cat, driveID, elems...) } func (h siteBackupHandler) GetItemPermission( diff --git a/src/internal/m365/service/onedrive/mock/handlers.go b/src/internal/m365/service/onedrive/mock/handlers.go index 13be790d0..98526ca54 100644 --- a/src/internal/m365/service/onedrive/mock/handlers.go +++ b/src/internal/m365/service/onedrive/mock/handlers.go @@ -296,7 +296,7 @@ var defaultOneDriveLocationIDer = func(driveID string, elems ...string) details. } var defaultSharePointLocationIDer = func(driveID string, elems ...string) details.LocationIDer { - return details.NewSharePointLocationIDer(driveID, elems...) + return details.NewSharePointLocationIDer(path.LibrariesCategory, driveID, elems...) } func (h BackupHandler[T]) IsAllPass() bool { diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go index 15a520dae..8ba7b94dd 100644 --- a/src/pkg/backup/details/details_test.go +++ b/src/pkg/backup/details/details_test.go @@ -116,7 +116,7 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { expectVs: []string{"deadbeef", "sender", "Parent", "subject", nowStr}, }, { - name: "sharepoint info", + name: "sharepoint library info", entry: Entry{ RepoRef: "reporef", ShortRef: "deadbeef", @@ -125,6 +125,7 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { ItemInfo: ItemInfo{ SharePoint: &SharePointInfo{ ItemName: "itemName", + ItemType: SharePointLibrary, ParentPath: "parentPath", Size: 1000, WebURL: "https://not.a.real/url", @@ -147,6 +148,66 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { nowStr, }, }, + { + name: "sharepoint list info for genericList template", + entry: Entry{ + RepoRef: "reporef", + ShortRef: "deadbeef", + LocationRef: "locationref", + ItemRef: "itemref", + ItemInfo: ItemInfo{ + SharePoint: &SharePointInfo{ + ItemType: SharePointList, + List: &ListInfo{ + Name: "list1", + ItemCount: 50, + Template: "genericList", + Created: now, + Modified: now, + WebURL: "https://10rqc2.sharepoint.com/sites/site-4754-small-lists/Lists/list1", + }, + }, + }, + }, + expectHs: []string{"ID", "List", "Items", "Created", "Modified"}, + expectVs: []string{ + "deadbeef", + "list1", + "50", + nowStr, + nowStr, + }, + }, + { + name: "sharepoint list info for documentLibrary template", + entry: Entry{ + RepoRef: "reporef", + ShortRef: "deadbeef", + LocationRef: "locationref", + ItemRef: "itemref", + ItemInfo: ItemInfo{ + SharePoint: &SharePointInfo{ + ItemType: SharePointList, + List: &ListInfo{ + Name: "Shared%20Documents", + ItemCount: 50, + Template: "documentLibrary", + Created: now, + Modified: now, + WebURL: "https://10rqc2.sharepoint.com/sites/site-4754-small-lists/Lists/Shared%20Documents", + }, + }, + }, + }, + expectHs: []string{"ID", "List", "Items", "Created", "Modified"}, + expectVs: []string{ + "deadbeef", + "Shared%20Documents", + "50", + nowStr, + nowStr, + }, + }, { name: "oneDrive info", entry: Entry{ @@ -846,6 +907,34 @@ var pathItemsTable = []struct { expectRepoRefs: []string{"abcde", "12345", "foo.meta"}, expectLocationRefs: []string{"locationref", "locationref2", "locationref.dirmeta"}, }, + { + name: "multiple entries with not recoverables", + ents: []Entry{ + { + RepoRef: "abcde", + LocationRef: "locationref", + }, + { + RepoRef: "foo.meta", + LocationRef: "locationref.dirmeta", + ItemRef: "itemref.meta", + ItemInfo: ItemInfo{ + SharePoint: &SharePointInfo{ItemType: SharePointList}, + }, + }, + { + RepoRef: "invalid-template-list-file", + LocationRef: "locationref-invalid-template-list-file", + ItemRef: "itemref-template-list-file", + ItemInfo: ItemInfo{ + SharePoint: &SharePointInfo{ItemType: SharePointList}, + NotRecoverable: true, + }, + }, + }, + expectRepoRefs: []string{"abcde", "foo.meta"}, + expectLocationRefs: []string{"locationref", "locationref.dirmeta"}, + }, } func (suite *DetailsUnitSuite) TestDetailsModel_Path() { @@ -1277,12 +1366,16 @@ func (suite *DetailsUnitSuite) TestUnarshalTo() { func (suite *DetailsUnitSuite) TestLocationIDer_FromEntry() { const ( - rrString = "tenant-id/%s/user-id/%s/drives/drive-id/root:/some/folder/stuff/item" - driveID = "driveID" + rrString = "tenant-id/%s/user-id/%s/drives/drive-id/root:/some/folder/stuff/item" + listsRrString = "tenant-id/%s/site-id/%s/lists/list-id/list-id" + driveID = "driveID" + listID = "listID" expectedUniqueLocFmt = "%s/" + driveID + "/root:/some/folder/stuff" expectedExchangeUniqueLocFmt = "%s/root:/some/folder/stuff" expectedDetailsLoc = "root:/some/folder/stuff" + expectedListUniqueLocFmt = "%s/" + listID + expectedListDetailsLoc = listID ) table := []struct { @@ -1379,6 +1472,21 @@ func (suite *DetailsUnitSuite) TestLocationIDer_FromEntry() { backupVersion: version.OneDrive7LocationRef, expectedErr: require.Error, }, + { + name: "SharePoint List With LocationRef", + service: path.SharePointService.String(), + category: path.ListsCategory.String(), + itemInfo: ItemInfo{ + SharePoint: &SharePointInfo{ + ItemType: SharePointList, + List: &ListInfo{}, + }, + }, + backupVersion: version.OneDrive7LocationRef, + hasLocRef: true, + expectedErr: require.NoError, + expectedUniqueLoc: fmt.Sprintf(expectedListUniqueLocFmt, path.ListsCategory), + }, { name: "Exchange Email With LocationRef Old Version", service: path.ExchangeService.String(), @@ -1479,13 +1587,23 @@ func (suite *DetailsUnitSuite) TestLocationIDer_FromEntry() { suite.Run(test.name, func() { t := suite.T() + rr := "" + detailsLoc := "" + if test.category == path.ListsCategory.String() { + rr = fmt.Sprintf(listsRrString, test.service, test.category) + detailsLoc = expectedListDetailsLoc + } else { + rr = fmt.Sprintf(rrString, test.service, test.category) + detailsLoc = expectedDetailsLoc + } + entry := Entry{ - RepoRef: fmt.Sprintf(rrString, test.service, test.category), + RepoRef: rr, ItemInfo: test.itemInfo, } if test.hasLocRef { - entry.LocationRef = expectedDetailsLoc + entry.LocationRef = detailsLoc } loc, err := entry.ToLocationIDer(test.backupVersion) @@ -1502,7 +1620,7 @@ func (suite *DetailsUnitSuite) TestLocationIDer_FromEntry() { "unique location") assert.Equal( t, - expectedDetailsLoc, + detailsLoc, loc.InDetails().String(), "details location") }) diff --git a/src/pkg/backup/details/iteminfo.go b/src/pkg/backup/details/iteminfo.go index e2eaf357e..3e316a911 100644 --- a/src/pkg/backup/details/iteminfo.go +++ b/src/pkg/backup/details/iteminfo.go @@ -74,7 +74,8 @@ type ItemInfo struct { OneDrive *OneDriveInfo `json:"oneDrive,omitempty"` Groups *GroupsInfo `json:"groups,omitempty"` // Optional item extension data - Extension *ExtensionData `json:"extension,omitempty"` + Extension *ExtensionData `json:"extension,omitempty"` + NotRecoverable bool `json:"notRecoverable,omitempty"` } // typedInfo should get embedded in each sesrvice type to track diff --git a/src/pkg/backup/details/model.go b/src/pkg/backup/details/model.go index 062621732..dcd75ce56 100644 --- a/src/pkg/backup/details/model.go +++ b/src/pkg/backup/details/model.go @@ -67,7 +67,9 @@ func (dm DetailsModel) Paths() []string { r := make([]string, 0, len(dm.Entries)) for _, ent := range dm.Entries { - if ent.Folder != nil || ent.isMetaFile() { + if ent.Folder != nil || + ent.isMetaFile() || + ent.NotRecoverable { continue } @@ -85,7 +87,9 @@ func (dm DetailsModel) Items() entrySet { for i := 0; i < len(dm.Entries); i++ { ent := dm.Entries[i] - if ent.Folder != nil || ent.isMetaFile() { + if ent.Folder != nil || + ent.isMetaFile() || + ent.NotRecoverable { continue } diff --git a/src/pkg/backup/details/sharepoint.go b/src/pkg/backup/details/sharepoint.go index 88f134713..1c51d428e 100644 --- a/src/pkg/backup/details/sharepoint.go +++ b/src/pkg/backup/details/sharepoint.go @@ -1,6 +1,7 @@ package details import ( + "fmt" "time" "github.com/alcionai/clues" @@ -13,16 +14,24 @@ import ( // NewSharePointLocationIDer builds a LocationIDer for the drive and folder // path. The path denoted by the folders should be unique within the drive. func NewSharePointLocationIDer( + category path.CategoryType, driveID string, escapedFolders ...string, ) uniqueLoc { - pb := path.Builder{}. - Append(path.LibrariesCategory.String(), driveID). - Append(escapedFolders...) + pb := path.Builder{}.Append(category.String()) + prefixElems := 1 + + if len(driveID) > 0 { // for library category + pb = pb.Append(driveID) + + prefixElems = 2 + } + + pb = pb.Append(escapedFolders...) return uniqueLoc{ pb: pb, - prefixElems: 2, + prefixElems: prefixElems, } } @@ -39,26 +48,55 @@ type SharePointInfo struct { Size int64 `json:"size,omitempty"` WebURL string `json:"webUrl,omitempty"` SiteID string `json:"siteID,omitempty"` + List *ListInfo `json:"list,omitempty"` +} + +type ListInfo struct { + Name string `json:"name,omitempty"` + ItemCount int64 `json:"itemCount,omitempty"` + Template string `json:"template,omitempty"` + WebURL string `json:"webUrl,omitempty"` + Created time.Time `json:"created,omitempty"` + Modified time.Time `json:"modified,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"} + switch i.ItemType { + case SharePointLibrary: + return []string{"ItemName", "Library", "ParentPath", "Size", "Owner", "Created", "Modified"} + case SharePointList: + return []string{"List", "Items", "Created", "Modified"} + } + + return []string{} } // 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), + switch i.ItemType { + case SharePointLibrary: + return []string{ + i.ItemName, + i.DriveName, + i.ParentPath, + humanize.Bytes(uint64(i.Size)), + i.Owner, + dttm.FormatToTabularDisplay(i.Created), + dttm.FormatToTabularDisplay(i.Modified), + } + case SharePointList: + return []string{ + i.List.Name, + fmt.Sprintf("%d", i.List.ItemCount), + dttm.FormatToTabularDisplay(i.List.Created), + dttm.FormatToTabularDisplay(i.List.Modified), + } } + + return []string{} } func (i *SharePointInfo) UpdateParentPath(newLocPath *path.Builder) { @@ -66,11 +104,14 @@ func (i *SharePointInfo) UpdateParentPath(newLocPath *path.Builder) { } func (i *SharePointInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, error) { - if len(i.DriveID) == 0 { - return nil, clues.New("empty drive ID") - } + loc := uniqueLoc{} - loc := NewSharePointLocationIDer(i.DriveID, baseLoc.Elements()...) + switch i.ItemType { + case SharePointLibrary, OneDriveItem: + loc = NewSharePointLocationIDer(path.LibrariesCategory, i.DriveID, baseLoc.Elements()...) + case SharePointList: + loc = NewSharePointLocationIDer(path.ListsCategory, "", baseLoc.Elements()...) + } return &loc, nil } @@ -78,8 +119,11 @@ func (i *SharePointInfo) uniqueLocation(baseLoc *path.Builder) (*uniqueLoc, erro 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 { + switch i.ItemType { + case OneDriveItem, SharePointLibrary: return updateFolderWithinDrive(SharePointLibrary, i.DriveName, i.DriveID, f) + case SharePointList: + return nil } return clues.New("unsupported non-SharePoint ItemType").With("item_type", i.ItemType) diff --git a/src/pkg/services/m365/api/graph/testdata/errors.go b/src/pkg/services/m365/api/graph/testdata/errors.go index 26d727f32..62bfb04e6 100644 --- a/src/pkg/services/m365/api/graph/testdata/errors.go +++ b/src/pkg/services/m365/api/graph/testdata/errors.go @@ -6,12 +6,13 @@ import ( "io" "testing" - "github.com/alcionai/corso/src/internal/common/ptr" "github.com/google/uuid" "github.com/microsoft/kiota-abstractions-go/serialization" kjson "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" "github.com/stretchr/testify/require" + + "github.com/alcionai/corso/src/internal/common/ptr" ) func ODataErr(code string) *odataerrors.ODataError {