add command to purge user's folders by prefix/date (#480)
This commit is contained in:
parent
342dd2e9f9
commit
a586512b42
@ -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
122
src/cmd/purge/purge.go
Normal 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 ""
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user