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
359 lines
9.6 KiB
Go
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
|
|
}
|