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
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, s...)
|
fmt.Fprint(w, s...)
|
||||||
|
fmt.Fprintf(w, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info prints the formatted strings to cobra's error writer (stdErr by default)
|
// 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
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, t, s...)
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SimpleDateTimeFormat = "02-Jan-2006_15:04:05"
|
||||||
|
)
|
||||||
|
|
||||||
// FormatTime produces the standard format for corso time values.
|
// FormatTime produces the standard format for corso time values.
|
||||||
// Always formats into the UTC timezone.
|
// Always formats into the UTC timezone.
|
||||||
func FormatTime(t time.Time) string {
|
func FormatTime(t time.Time) string {
|
||||||
return t.UTC().Format(time.RFC3339Nano)
|
return t.UTC().Format(time.RFC3339Nano)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatSimpleDateTime produces standard format for
|
// FormatSimpleDateTime produces a simple datetime of the format
|
||||||
// GraphConnector. Format used on CI testing and default folder
|
// "02-Jan-2006_15:04:05"
|
||||||
// creation during the restore process
|
|
||||||
func FormatSimpleDateTime(t time.Time) string {
|
func FormatSimpleDateTime(t time.Time) string {
|
||||||
timeFolderFormat := "02-Jan-2006_15:04:05"
|
return t.UTC().Format(SimpleDateTimeFormat)
|
||||||
return t.UTC().Format(timeFolderFormat)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTime makes a best attempt to produce a time value from
|
// 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")
|
return time.Time{}, errors.New("cannot interpret an empty string as time.Time")
|
||||||
}
|
}
|
||||||
t, err := time.Parse(time.RFC3339Nano, s)
|
t, err := time.Parse(time.RFC3339Nano, s)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return time.Time{}, err
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
absser "github.com/microsoft/kiota-abstractions-go/serialization"
|
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()
|
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.
|
// GetMailFolderID query function to retrieve the M365 ID based on the folder's displayName.
|
||||||
// @param folderName the target folder's display name. Case sensitive
|
// @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
|
// @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
|
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 {
|
type graphService struct {
|
||||||
client msgraphsdk.GraphServiceClient
|
client msgraphsdk.GraphServiceClient
|
||||||
adapter msgraphsdk.GraphRequestAdapter
|
adapter msgraphsdk.GraphRequestAdapter
|
||||||
failFast bool // if true service will exit sequence upon encountering an error
|
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
|
return &gs.client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *graphService) Adapter() *msgraphsdk.GraphRequestAdapter {
|
func (gs graphService) Adapter() *msgraphsdk.GraphRequestAdapter {
|
||||||
return &gs.adapter
|
return &gs.adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *graphService) ErrPolicy() bool {
|
func (gs graphService) ErrPolicy() bool {
|
||||||
return gs.failFast
|
return gs.failFast
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -153,7 +153,7 @@ func (suite *GraphConnectorIntegrationSuite) TestGraphConnector_CreateAndDeleteF
|
|||||||
aFolder, err := exchange.CreateMailFolder(&suite.connector.graphService, userID, folderName)
|
aFolder, err := exchange.CreateMailFolder(&suite.connector.graphService, userID, folderName)
|
||||||
assert.NoError(suite.T(), err, support.ConnectorStackErrorTrace(err))
|
assert.NoError(suite.T(), err, support.ConnectorStackErrorTrace(err))
|
||||||
if aFolder != nil {
|
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)
|
assert.NoError(suite.T(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user