Keepers e86592f51e
replace graph Wrap and Stack with clues (#5018)
Now that graph errors are always transformed as part of the graph client wrapper or http_wrapper, we don't need to call graph.Stack or graph.Wrap outside of those inner helpers any longer. This PR swaps all those graph calls with equivalent clues funcs as a cleanup.

Should not contain any logical changes.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #4685 

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
2024-02-06 01:08:26 +00:00

359 lines
9.6 KiB
Go

package api
import (
"context"
"fmt"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/drives"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/errs/core"
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
)
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
func (c Client) Drives() Drives {
return Drives{c}
}
// Drives is an interface-compliant provider of the client.
type Drives struct {
Client
}
// ---------------------------------------------------------------------------
// Folders
// ---------------------------------------------------------------------------
const (
itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s"
createLinkShareURLFmt = "https://graph.microsoft.com/beta/drives/%s/items/%s/createLink"
)
var ErrFolderNotFound = clues.New("folder not found")
// GetFolderByName will lookup the specified folder by name within the parentFolderID folder.
func (c Drives) GetFolderByName(
ctx context.Context,
driveID, parentFolderID, folderName string,
) (models.DriveItemable, error) {
// The `Children().Get()` API doesn't yet support $filter, so using that to find a folder
// will be sub-optimal.
// Instead, we leverage OneDrive path-based addressing -
// https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems#path-based-addressing
// - which allows us to lookup an item by its path relative to the parent ID
rawURL := fmt.Sprintf(itemByPathRawURLFmt, driveID, parentFolderID, folderName)
builder := drives.NewItemItemsDriveItemItemRequestBuilder(rawURL, c.Stable.Adapter())
foundItem, err := builder.Get(ctx, nil)
if err != nil {
if errors.Is(err, core.ErrNotFound) {
err = clues.Stack(ErrFolderNotFound, err)
}
return nil, clues.Wrap(err, "getting folder")
}
// Check if the item found is a folder, fail the call if not
if foundItem.GetFolder() == nil {
return nil, clues.WrapWC(ctx, ErrFolderNotFound, "item is not a folder")
}
return foundItem, nil
}
func (c Drives) GetRootFolder(
ctx context.Context,
driveID string,
) (models.DriveItemable, error) {
root, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Root().
Get(ctx, nil)
if err != nil {
return nil, clues.Wrap(err, "getting drive root")
}
return root, nil
}
// TODO: pagination controller needed for completion.
func (c Drives) GetFolderChildren(
ctx context.Context,
driveID, folderID string,
) ([]models.DriveItemable, error) {
response, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(folderID).
Children().
Get(ctx, nil)
if err != nil {
return nil, clues.Wrap(err, "getting folder children")
}
return response.GetValue(), nil
}
// ---------------------------------------------------------------------------
// Items
// ---------------------------------------------------------------------------
// generic drive item getter
func (c Drives) GetItem(
ctx context.Context,
driveID, itemID string,
) (models.DriveItemable, error) {
options := &drives.ItemItemsDriveItemItemRequestBuilderGetRequestConfiguration{
QueryParameters: &drives.ItemItemsDriveItemItemRequestBuilderGetQueryParameters{
// FIXME: accept a CallConfig instead of hardcoding the select.
Select: DefaultDriveItemProps(),
},
}
di, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Get(ctx, options)
return di, clues.Wrap(err, "getting item").OrNil()
}
func (c Drives) NewItemContentUpload(
ctx context.Context,
driveID, itemID string,
) (models.UploadSessionable, error) {
session := drives.NewItemItemsItemCreateUploadSessionPostRequestBody()
r, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
CreateUploadSession().
Post(ctx, session, nil)
return r, clues.Wrap(err, "uploading drive item").OrNil()
}
//nolint:lll
const itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children?@microsoft.graph.conflictBehavior=%s"
const (
conflictBehaviorFail = "fail"
conflictBehaviorRename = "rename"
conflictBehaviorReplace = "replace"
)
// PostItemInContainer creates a new item in the specified folder
func (c Drives) PostItemInContainer(
ctx context.Context,
driveID, parentFolderID string,
newItem models.DriveItemable,
onCollision control.CollisionPolicy,
) (models.DriveItemable, error) {
// graph api has no policy for Skip; instead we wrap the same-name failure
// as a graph.ErrItemAlreadyExistsConflict.
conflictBehavior := conflictBehaviorFail
switch onCollision {
case control.Replace:
conflictBehavior = conflictBehaviorReplace
case control.Copy:
conflictBehavior = conflictBehaviorRename
}
// Graph SDK doesn't yet provide a POST method for `/children` so we set the `rawUrl` ourselves as recommended
// here: https://github.com/microsoftgraph/msgraph-sdk-go/issues/155#issuecomment-1136254310
rawURL := fmt.Sprintf(itemChildrenRawURLFmt, driveID, parentFolderID, conflictBehavior)
builder := drives.NewItemItemsRequestBuilder(rawURL, c.Stable.Adapter())
newItem, err := builder.Post(ctx, newItem, nil)
if err != nil {
return nil, clues.Wrap(err, "creating item in folder")
}
return newItem, nil
}
func (c Drives) PatchItem(
ctx context.Context,
driveID, itemID string,
item models.DriveItemable,
) error {
_, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Patch(ctx, item, nil)
return clues.Wrap(err, "patching drive item").OrNil()
}
func (c Drives) PutItemContent(
ctx context.Context,
driveID, itemID string,
content []byte,
) error {
_, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Content().
Put(ctx, content, nil)
return clues.Wrap(err, "uploading drive item content").OrNil()
}
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
func (c Drives) DeleteItem(
ctx context.Context,
driveID, itemID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := c.Service(c.counter)
if err != nil {
return clues.WrapWC(ctx, err, "creating adapter to delete item permission")
}
err = srv.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Delete(ctx, nil)
return clues.Wrap(err, "deleting item").With("item_id", itemID).OrNil()
}
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
func (c Drives) GetItemPermission(
ctx context.Context,
driveID, itemID string,
) (models.PermissionCollectionResponseable, error) {
perm, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Permissions().
Get(ctx, nil)
return perm, clues.Wrap(err, "getting item permissions").OrNil()
}
func (c Drives) PostItemPermissionUpdate(
ctx context.Context,
driveID, itemID string,
body *drives.ItemItemsItemInvitePostRequestBody,
) (drives.ItemItemsItemInviteResponseable, error) {
ctx = graph.ConsumeNTokens(ctx, graph.PermissionsLC)
itm, err := c.Stable.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Invite().
Post(ctx, body, nil)
return itm, clues.Wrap(err, "posting permissions").OrNil()
}
func (c Drives) DeleteItemPermission(
ctx context.Context,
driveID, itemID, permissionID string,
) error {
// deletes require unique http clients
// https://github.com/alcionai/corso/issues/2707
srv, err := c.Service(c.counter)
if err != nil {
return clues.WrapWC(ctx, err, "creating adapter to delete item permission")
}
err = srv.
Client().
Drives().
ByDriveId(driveID).
Items().
ByDriveItemId(itemID).
Permissions().
ByPermissionId(permissionID).
Delete(graph.ConsumeNTokens(ctx, graph.PermissionsLC), nil)
return clues.Wrap(err, "deleting drive item permission").OrNil()
}
func (c Drives) PostItemLinkShareUpdate(
ctx context.Context,
driveID, itemID string,
body *drives.ItemItemsItemCreateLinkPostRequestBody,
) (models.Permissionable, error) {
ctx = graph.ConsumeNTokens(ctx, graph.PermissionsLC)
// We are using the beta version of the endpoint. This allows us
// to add recipients in the same request as well as to make it not
// send out and email for every link share the user gets added to.
rawURL := fmt.Sprintf(createLinkShareURLFmt, driveID, itemID)
builder := drives.NewItemItemsItemCreateLinkRequestBuilder(rawURL, c.Stable.Adapter())
itm, err := builder.Post(ctx, body, nil)
return itm, clues.Wrap(err, "creating link share").OrNil()
}
// ---------------------------------------------------------------------------
// helper funcs
// ---------------------------------------------------------------------------
// DriveItemCollisionKeyy constructs a key from the item name.
// collision keys are used to identify duplicate item conflicts for handling advanced restoration config.
func DriveItemCollisionKey(item models.DriveItemable) string {
if item == nil {
return ""
}
return ptr.Val(item.GetName())
}
// NewDriveItem initializes a `models.DriveItemable` with either a folder or file entry.
func NewDriveItem(name string, folder bool) *models.DriveItem {
itemToCreate := models.NewDriveItem()
itemToCreate.SetName(&name)
if folder {
itemToCreate.SetFolder(models.NewFolder())
} else {
itemToCreate.SetFile(models.NewFile())
}
return itemToCreate
}