## Description Relocates the graphService struct to graph as the Service struct. Replaces GC's embedded graphService with a graph.Servicer reference. ## Type of change - [x] 🐹 Trivial/Minor ## Issue(s) * #1725 ## Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
308 lines
7.0 KiB
Go
308 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
|
|
. "github.com/alcionai/corso/src/cli/print"
|
|
"github.com/alcionai/corso/src/cli/utils"
|
|
"github.com/alcionai/corso/src/internal/common"
|
|
"github.com/alcionai/corso/src/internal/connector"
|
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
|
"github.com/alcionai/corso/src/internal/connector/onedrive"
|
|
"github.com/alcionai/corso/src/pkg/account"
|
|
"github.com/alcionai/corso/src/pkg/credentials"
|
|
"github.com/alcionai/corso/src/pkg/logger"
|
|
)
|
|
|
|
var purgeCmd = &cobra.Command{
|
|
Use: "purge",
|
|
Short: "Purge all types of m365 folders",
|
|
RunE: handleAllFolderPurge,
|
|
}
|
|
|
|
var oneDriveCmd = &cobra.Command{
|
|
Use: "onedrive",
|
|
Short: "Purges OneDrive folders",
|
|
RunE: handleOneDriveFolderPurge,
|
|
}
|
|
|
|
var (
|
|
before string
|
|
user string
|
|
tenant string
|
|
prefix string
|
|
)
|
|
|
|
var ErrPurging = errors.New("not all items were successfully purged")
|
|
|
|
// ------------------------------------------------------------------------------------------
|
|
// CLI command handlers
|
|
// ------------------------------------------------------------------------------------------
|
|
|
|
func main() {
|
|
ctx, _ := logger.SeedLevel(context.Background(), logger.Development)
|
|
ctx = SetRootCmd(ctx, purgeCmd)
|
|
|
|
defer logger.Flush(ctx)
|
|
|
|
fs := purgeCmd.PersistentFlags()
|
|
fs.StringVar(&before, "before", "", "folders older than this date are deleted. (default: now in UTC)")
|
|
fs.StringVar(&user, "user", "", "m365 user id whose folders will be deleted")
|
|
cobra.CheckErr(purgeCmd.MarkPersistentFlagRequired("user"))
|
|
fs.StringVar(&tenant, "tenant", "", "m365 tenant containing the user")
|
|
fs.StringVar(&prefix, "prefix", "", "filters mail folders by displayName prefix")
|
|
cobra.CheckErr(purgeCmd.MarkPersistentFlagRequired("prefix"))
|
|
|
|
purgeCmd.AddCommand(oneDriveCmd)
|
|
|
|
if err := purgeCmd.ExecuteContext(ctx); err != nil {
|
|
logger.Flush(ctx)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func handleAllFolderPurge(cmd *cobra.Command, args []string) error {
|
|
ctx := cmd.Context()
|
|
|
|
if utils.HasNoFlagsAndShownHelp(cmd) {
|
|
return nil
|
|
}
|
|
|
|
gc, t, err := getGCAndBoundaryTime(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = runPurgeForEachUser(
|
|
ctx, gc, t,
|
|
purgeOneDriveFolders,
|
|
)
|
|
if err != nil {
|
|
return Only(ctx, ErrPurging)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func handleOneDriveFolderPurge(cmd *cobra.Command, args []string) error {
|
|
ctx := cmd.Context()
|
|
|
|
if utils.HasNoFlagsAndShownHelp(cmd) {
|
|
return nil
|
|
}
|
|
|
|
gc, t, err := getGCAndBoundaryTime(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := runPurgeForEachUser(ctx, gc, t, purgeOneDriveFolders); err != nil {
|
|
logger.Ctx(ctx).Error(err)
|
|
return Only(ctx, errors.Wrap(ErrPurging, "OneDrive folders"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------
|
|
// Purge Controllers
|
|
// ------------------------------------------------------------------------------------------
|
|
|
|
type purgable interface {
|
|
GetDisplayName() *string
|
|
GetId() *string
|
|
}
|
|
|
|
type purger func(context.Context, *connector.GraphConnector, time.Time, string) error
|
|
|
|
func runPurgeForEachUser(
|
|
ctx context.Context,
|
|
gc *connector.GraphConnector,
|
|
boundary time.Time,
|
|
ps ...purger,
|
|
) error {
|
|
var errs error
|
|
|
|
for pn, uid := range userOrUsers(user, gc.Users) {
|
|
Infof(ctx, "\nUser: %s - %s", pn, uid)
|
|
|
|
for _, p := range ps {
|
|
if err := p(ctx, gc, boundary, pn); err != nil {
|
|
errs = multierror.Append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// ----- OneDrive
|
|
|
|
func purgeOneDriveFolders(
|
|
ctx context.Context,
|
|
gc *connector.GraphConnector,
|
|
boundary time.Time,
|
|
uid string,
|
|
) error {
|
|
getter := func(gs graph.Servicer, uid, prefix string) ([]purgable, error) {
|
|
cfs, err := onedrive.GetAllFolders(ctx, gs, uid, prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
purgables := make([]purgable, len(cfs))
|
|
|
|
for i, v := range cfs {
|
|
purgables[i] = v
|
|
}
|
|
|
|
return purgables, nil
|
|
}
|
|
|
|
deleter := func(gs graph.Servicer, uid string, f purgable) error {
|
|
driveFolder, ok := f.(*onedrive.Displayable)
|
|
if !ok {
|
|
return errors.New("non-OneDrive item")
|
|
}
|
|
|
|
return onedrive.DeleteItem(
|
|
ctx,
|
|
gs,
|
|
*driveFolder.GetParentReference().GetDriveId(),
|
|
*f.GetId(),
|
|
)
|
|
}
|
|
|
|
return purgeFolders(ctx, gc, boundary, "OneDrive Folders", uid, getter, deleter)
|
|
}
|
|
|
|
// ----- controller
|
|
|
|
func purgeFolders(
|
|
ctx context.Context,
|
|
gc *connector.GraphConnector,
|
|
boundary time.Time,
|
|
data, uid string,
|
|
getter func(graph.Servicer, string, string) ([]purgable, error),
|
|
deleter func(graph.Servicer, string, purgable) error,
|
|
) error {
|
|
Infof(ctx, "Container: %s", data)
|
|
|
|
// get them folders
|
|
fs, err := getter(gc.Service, uid, prefix)
|
|
if err != nil {
|
|
return Only(ctx, errors.Wrapf(err, "retrieving %s folders", data))
|
|
}
|
|
|
|
if len(fs) == 0 {
|
|
Info(ctx, "None Matched")
|
|
return nil
|
|
}
|
|
|
|
var errs error
|
|
|
|
// delete any containers that don't pass the boundary
|
|
for _, fld := range fs {
|
|
// compare the folder time to the deletion boundary time first
|
|
displayName := *fld.GetDisplayName()
|
|
|
|
dnTime, err := common.ExtractTime(displayName)
|
|
if err != nil && !errors.Is(err, common.ErrNoTimeString) {
|
|
err = errors.Wrapf(err, "!! Error: parsing container named [%s]", displayName)
|
|
errs = multierror.Append(errs, err)
|
|
Info(ctx, err)
|
|
|
|
continue
|
|
}
|
|
|
|
if !dnTime.Before(boundary) || dnTime == (time.Time{}) {
|
|
continue
|
|
}
|
|
|
|
Infof(ctx, "∙ Deleting [%s]", displayName)
|
|
|
|
err = deleter(gc.Service, uid, fld)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "!! Error")
|
|
errs = multierror.Append(errs, err)
|
|
Info(ctx, err)
|
|
}
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------
|
|
// Helpers
|
|
// ------------------------------------------------------------------------------------------
|
|
|
|
func getGC(ctx context.Context) (*connector.GraphConnector, error) {
|
|
// get account info
|
|
m365Cfg := account.M365Config{
|
|
M365: credentials.GetM365(),
|
|
AzureTenantID: common.First(tenant, os.Getenv(account.AzureTenantID)),
|
|
}
|
|
|
|
acct, err := account.NewAccount(account.ProviderM365, m365Cfg)
|
|
if err != nil {
|
|
return nil, Only(ctx, errors.Wrap(err, "finding m365 account details"))
|
|
}
|
|
|
|
// build a graph connector
|
|
gc, err := connector.NewGraphConnector(ctx, acct, connector.Users)
|
|
if err != nil {
|
|
return nil, Only(ctx, errors.Wrap(err, "connecting to graph api"))
|
|
}
|
|
|
|
return gc, nil
|
|
}
|
|
|
|
func getBoundaryTime(ctx context.Context) (time.Time, error) {
|
|
// format the time input
|
|
var (
|
|
err error
|
|
boundaryTime = time.Now().UTC()
|
|
)
|
|
|
|
if len(before) > 0 {
|
|
boundaryTime, err = common.ParseTime(before)
|
|
if err != nil {
|
|
return time.Time{}, Only(ctx, errors.Wrap(err, "parsing before flag to time"))
|
|
}
|
|
}
|
|
|
|
return boundaryTime, nil
|
|
}
|
|
|
|
func getGCAndBoundaryTime(ctx context.Context) (*connector.GraphConnector, time.Time, error) {
|
|
gc, err := getGC(ctx)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
|
|
t, err := getBoundaryTime(ctx)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
|
|
return gc, t, nil
|
|
}
|
|
|
|
func userOrUsers(u string, us map[string]string) map[string]string {
|
|
if len(u) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if u == "*" {
|
|
return us
|
|
}
|
|
|
|
return map[string]string{u: u}
|
|
}
|