add command to purge user's folders by prefix/date (#480)

This commit is contained in:
Keepers 2022-08-04 15:28:28 -06:00 committed by GitHub
parent 342dd2e9f9
commit a586512b42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 12 deletions

View File

@ -71,6 +71,7 @@ func info(w io.Writer, s ...any) {
return
}
fmt.Fprint(w, s...)
fmt.Fprintf(w, "\n")
}
// Info prints the formatted strings to cobra's error writer (stdErr by default)
@ -85,6 +86,7 @@ func infof(w io.Writer, t string, s ...any) {
return
}
fmt.Fprintf(w, t, s...)
fmt.Fprintf(w, "\n")
}
// ---------------------------------------------------------------------------------------------------------

122
src/cmd/purge/purge.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"os"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
. "github.com/alcionai/corso/cli/print"
"github.com/alcionai/corso/cli/utils"
"github.com/alcionai/corso/internal/common"
"github.com/alcionai/corso/internal/connector"
"github.com/alcionai/corso/internal/connector/exchange"
"github.com/alcionai/corso/pkg/account"
"github.com/alcionai/corso/pkg/credentials"
)
var purgeCmd = &cobra.Command{
Use: "purge",
Short: "Purge m365 data",
RunE: doFolderPurge,
}
var (
before string
user string
tenant string
prefix string
)
func doFolderPurge(cmd *cobra.Command, args []string) error {
if utils.HasNoFlagsAndShownHelp(cmd) {
return nil
}
// get account info
m365Cfg := account.M365Config{
M365: credentials.GetM365(),
TenantID: first(tenant, os.Getenv(account.TenantID)),
}
acct, err := account.NewAccount(account.ProviderM365, m365Cfg)
if err != nil {
return Only(errors.Wrap(err, "finding m365 account details"))
}
// build a graph connector
gc, err := connector.NewGraphConnector(acct)
if err != nil {
return Only(errors.Wrap(err, "connecting to graph api"))
}
// get them folders
mfs, err := exchange.GetAllMailFolders(gc.Service(), user, prefix)
if err != nil {
return Only(errors.Wrap(err, "retrieving mail folders"))
}
// format the time input
beforeTime := time.Now().UTC()
if len(before) > 0 {
beforeTime, err = common.ParseTime(before)
if err != nil {
return Only(errors.Wrap(err, "parsing before flag to time"))
}
}
stLen := len(common.SimpleDateTimeFormat)
// delete files
for _, mf := range mfs {
// compare the folder time to the deletion boundary time first
var delete bool
dnLen := len(mf.DisplayName)
if dnLen > stLen {
dnSuff := mf.DisplayName[dnLen-stLen:]
dnTime, err := common.ParseTime(dnSuff)
if err != nil {
Info(errors.Wrapf(err, "Error: deleting folder [%s]", mf.DisplayName))
continue
}
delete = dnTime.Before(beforeTime)
}
if !delete {
continue
}
Info("Deleting folder: ", mf.DisplayName)
err = exchange.DeleteMailFolder(gc.Service(), user, mf.ID)
if err != nil {
Info(errors.Wrapf(err, "Error: deleting folder [%s]", mf.DisplayName))
}
}
return nil
}
func main() {
fs := purgeCmd.Flags()
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.MarkFlagRequired("user"))
fs.StringVar(&tenant, "tenant", "", "m365 tenant containing the user")
fs.StringVar(&prefix, "prefix", "", "filters mail folders by displayName prefix")
cobra.CheckErr(purgeCmd.MarkFlagRequired("prefix"))
if err := purgeCmd.Execute(); err != nil {
Info("Error: ", err.Error())
os.Exit(1)
}
}
// returns the first non-zero valued string
func first(vs ...string) string {
for _, v := range vs {
if len(v) > 0 {
return v
}
}
return ""
}

View File

@ -5,18 +5,20 @@ import (
"time"
)
const (
SimpleDateTimeFormat = "02-Jan-2006_15:04:05"
)
// FormatTime produces the standard format for corso time values.
// Always formats into the UTC timezone.
func FormatTime(t time.Time) string {
return t.UTC().Format(time.RFC3339Nano)
}
// FormatSimpleDateTime produces standard format for
// GraphConnector. Format used on CI testing and default folder
// creation during the restore process
// FormatSimpleDateTime produces a simple datetime of the format
// "02-Jan-2006_15:04:05"
func FormatSimpleDateTime(t time.Time) string {
timeFolderFormat := "02-Jan-2006_15:04:05"
return t.UTC().Format(timeFolderFormat)
return t.UTC().Format(SimpleDateTimeFormat)
}
// ParseTime makes a best attempt to produce a time value from
@ -26,8 +28,12 @@ func ParseTime(s string) (time.Time, error) {
return time.Time{}, errors.New("cannot interpret an empty string as time.Time")
}
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return time.Time{}, err
if err == nil {
return t.UTC(), nil
}
return t.UTC(), nil
t, err = time.Parse(SimpleDateTimeFormat, s)
if err == nil {
return t.UTC(), nil
}
return time.Time{}, errors.New("unable to format time string: " + s)
}

View File

@ -3,6 +3,7 @@ package exchange
import (
"context"
"fmt"
"strings"
"time"
absser "github.com/microsoft/kiota-abstractions-go/serialization"
@ -76,6 +77,60 @@ func DeleteMailFolder(gs graph.Service, user, folderID string) error {
return gs.Client().UsersById(user).MailFoldersById(folderID).Delete()
}
type MailFolder struct {
ID string
DisplayName string
}
// GetAllMailFolders retrieves all mail folders for the specified user.
// If nameContains is populated, only returns mail matching that property.
// Returns a slice of {ID, DisplayName} tuples.
func GetAllMailFolders(gs graph.Service, user, nameContains string) ([]MailFolder, error) {
var (
mfs = []MailFolder{}
err error
)
opts, err := optionsForMailFolders([]string{"id", "displayName"})
if err != nil {
return nil, err
}
resp, err := gs.Client().UsersById(user).MailFolders().GetWithRequestConfigurationAndResponseHandler(opts, nil)
if err != nil {
return nil, err
}
iter, err := msgraphgocore.NewPageIterator(resp, gs.Adapter(), models.CreateMailFolderCollectionResponseFromDiscriminatorValue)
if err != nil {
return nil, err
}
cb := func(folderItem any) bool {
folder, ok := folderItem.(models.MailFolderable)
if !ok {
err = errors.New("HasFolder() iteration failure")
return false
}
include := len(nameContains) == 0 ||
(len(nameContains) > 0 && strings.Contains(*folder.GetDisplayName(), nameContains))
if include {
mfs = append(mfs, MailFolder{
ID: *folder.GetId(),
DisplayName: *folder.GetDisplayName(),
})
}
return true
}
if err := iter.Iterate(cb); err != nil {
return nil, err
}
return mfs, err
}
// GetMailFolderID query function to retrieve the M365 ID based on the folder's displayName.
// @param folderName the target folder's display name. Case sensitive
// @returns a *string if the folder exists. If the folder does not exist returns nil, error-> folder not found

View File

@ -42,21 +42,28 @@ type GraphConnector struct {
credentials account.M365Config
}
// Service returns the GC's embedded graph.Service
func (gc GraphConnector) Service() graph.Service {
return gc.graphService
}
var _ graph.Service = &graphService{}
type graphService struct {
client msgraphsdk.GraphServiceClient
adapter msgraphsdk.GraphRequestAdapter
failFast bool // if true service will exit sequence upon encountering an error
}
func (gs *graphService) Client() *msgraphsdk.GraphServiceClient {
func (gs graphService) Client() *msgraphsdk.GraphServiceClient {
return &gs.client
}
func (gs *graphService) Adapter() *msgraphsdk.GraphRequestAdapter {
func (gs graphService) Adapter() *msgraphsdk.GraphRequestAdapter {
return &gs.adapter
}
func (gs *graphService) ErrPolicy() bool {
func (gs graphService) ErrPolicy() bool {
return gs.failFast
}

View File

@ -153,7 +153,7 @@ func (suite *GraphConnectorIntegrationSuite) TestGraphConnector_CreateAndDeleteF
aFolder, err := exchange.CreateMailFolder(&suite.connector.graphService, userID, folderName)
assert.NoError(suite.T(), err, support.ConnectorStackErrorTrace(err))
if aFolder != nil {
err = exchange.DeleteMailFolder(&suite.connector.graphService, userID, *aFolder.GetId())
err = exchange.DeleteMailFolder(suite.connector.Service(), userID, *aFolder.GetId())
assert.NoError(suite.T(), err)
}
}