diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32acffa53..7083e885b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,17 +239,29 @@ jobs: run: working-directory: src steps: - - name: Fail check + - name: Fail check if not repository_dispatch if: github.event_name != 'repository_dispatch' run: | echo "Workflow requires approval from a maintainer to run. It will be automatically rerun on approval." exit 1 + - uses: marocchino/sticky-pull-request-comment@v2 + if: github.event.client_payload.slash_command.args.named.sha != '' && contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha) + with: + message: | + Workflow run sha specified via `ok-to-test` is not the latest commit on PR. Run canceled. + + - name: Fail check if not head of PR + if: github.event.client_payload.slash_command.args.named.sha != '' && contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha) + run: | + echo "Workflow run sha specified is not the latest commit on PR. Exiting." + exit 1 + # add comment to PR with link to workflow run - uses: marocchino/sticky-pull-request-comment@v2 with: message: | - https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID + Test suite run will be available at https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID # Check out merge commit - name: Fork based /ok-to-test checkout @@ -517,7 +529,7 @@ jobs: curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Linux_x86_64.tar.gz > corso.tar.gz tar -xf corso.tar.gz ./corso --help - ./corso --version 2>&1 | grep -E "^version: ${{ env.CORSO_VERSION }}$" + ./corso --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$" - name: Validate arm64 binary artifacts uses: uraimo/run-on-arch-action@v2 with: @@ -531,7 +543,7 @@ jobs: curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Linux_arm64.tar.gz > corso.tar.gz tar -xf corso.tar.gz ./corso --help - ./corso --version 2>&1 | grep -E "^version: ${{ env.CORSO_VERSION }}$" + ./corso --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$" Validate-Docker-Artifacts: needs: [Publish-Binary, Publish-Image, SetEnv] @@ -549,11 +561,11 @@ jobs: - name: Validate amd64 container images run: | docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --help - docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --version | grep -E "^version: ${{ env.CORSO_VERSION }}$" + docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --version | grep -E "version: ${{ env.CORSO_VERSION }}$" - name: Validate arm64 container images run: | docker run --platform linux/arm64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --help - docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --version | grep -E "^version: ${{ env.CORSO_VERSION }}$" + docker run --platform linux/amd64 ${{ env.IMAGE_NAME }}:${{ env.CORSO_VERSION }} --version | grep -E "version: ${{ env.CORSO_VERSION }}$" Validate-MacOS-Artifacts: needs: [Publish-Binary, Publish-Image, SetEnv] @@ -569,7 +581,7 @@ jobs: curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Darwin_x86_64.tar.gz > corso.tar.gz tar -xf corso.tar.gz ./corso --help - ./corso --version 2>&1 | grep -E "^version: ${{ env.CORSO_VERSION }}$" + ./corso --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$" - name: Validate arm64 binary artifacts run: | set -ex @@ -590,7 +602,7 @@ jobs: curl -L https://github.com/alcionai/corso/releases/download/${{ env.CORSO_VERSION }}/corso_${{ env.CORSO_VERSION }}_Windows_x86_64.zip -o corso.zip 7z x corso.zip ./corso.exe --help - ./corso.exe --version 2>&1 | grep -E "^version: ${{ env.CORSO_VERSION }}$" + ./corso.exe --version 2>&1 | grep -E "version: ${{ env.CORSO_VERSION }}$" Publish-Website-Test: needs: [Test-Suite-Trusted, Linting, Website-Linting, SetEnv] diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml index bd6a7db67..f48e49129 100644 --- a/.github/workflows/ok-to-test.yml +++ b/.github/workflows/ok-to-test.yml @@ -19,7 +19,7 @@ jobs: private_key: ${{ secrets.PRIVATE_KEY }} - name: Slash Command Dispatch - uses: peter-evans/slash-command-dispatch@v1 + uses: peter-evans/slash-command-dispatch@v3 env: TOKEN: ${{ steps.generate_token.outputs.token }} with: @@ -27,5 +27,4 @@ jobs: reaction-token: ${{ secrets.GITHUB_TOKEN }} issue-type: pull-request commands: ok-to-test - named-args: true permission: write diff --git a/CHANGELOG.md b/CHANGELOG.md index c1362c45c..1b18a4d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] (alpha) +### Added + +- Document Corso's fault-tolerance and restartability features + +## [v0.2.0] (alpha) - 2023-1-29 + ### Fixed - Check if the user specified for an exchange backup operation has a mailbox. ### Changed -- msgraph-beta-sdk-go replaces msgraph-sdk-go for new features. This can lead to long build times. +- BetaClient introduced. Enables Corso to be able to interact with SharePoint Page objects. Package located `/internal/connector/graph/betasdk` - Handle case where user's drive has not been initialized - Inline attachments (e.g. copy/paste ) are discovered and backed up correctly ([#2163](https://github.com/alcionai/corso/issues/2163)) - Guest and External users (for cloud accounts) and non-on-premise users (for systems that use on-prem AD syncs) are now excluded from backup and restore operations. - Remove the M365 license guid check in OneDrive backup which wasn't reliable. - +- Reduced extra socket consumption while downloading multiple drive files. +- Extended timeout boundaries for exchange attachment downloads, reducing risk of cancellation on large files. +- Identify all drives associated with a user or SharePoint site instead of just the results on the first page returned by Graph API. ## [v0.1.0] (alpha) - 2023-01-13 @@ -131,7 +139,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Miscellaneous - Optional usage statistics reporting ([RM-35](https://github.com/alcionai/corso-roadmap/issues/35)) -[Unreleased]: https://github.com/alcionai/corso/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/alcionai/corso/compare/v0.2.0...HEAD +[v0.2.0]: https://github.com/alcionai/corso/compare/v0.1.0...v0.2.0 [v0.1.0]: https://github.com/alcionai/corso/compare/v0.0.4...v0.1.0 [v0.0.4]: https://github.com/alcionai/corso/compare/v0.0.3...v0.0.4 [v0.0.3]: https://github.com/alcionai/corso/compare/v0.0.2...v0.0.3 diff --git a/src/cli/backup/sharepoint.go b/src/cli/backup/sharepoint.go index 4852e7f43..3a7ca1ace 100644 --- a/src/cli/backup/sharepoint.go +++ b/src/cli/backup/sharepoint.go @@ -81,7 +81,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case createCommand: - c, fs = utils.AddCommand(cmd, sharePointCreateCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointCreateCmd(), utils.MarkPreReleaseCommand()) c.Use = c.Use + " " + sharePointServiceCommandCreateUseSuffix c.Example = sharePointServiceCommandCreateExamples @@ -101,14 +101,14 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { options.AddOperationFlags(c) case listCommand: - c, fs = utils.AddCommand(cmd, sharePointListCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointListCmd(), utils.MarkPreReleaseCommand()) fs.StringVar(&backupID, utils.BackupFN, "", "ID of the backup to retrieve.") case detailsCommand: - c, fs = utils.AddCommand(cmd, sharePointDetailsCmd()) + c, fs = utils.AddCommand(cmd, sharePointDetailsCmd(), utils.MarkPreReleaseCommand()) c.Use = c.Use + " " + sharePointServiceCommandDetailsUseSuffix c.Example = sharePointServiceCommandDetailsExamples @@ -157,7 +157,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { // "Select backup details for items created after this datetime.") case deleteCommand: - c, fs = utils.AddCommand(cmd, sharePointDeleteCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointDeleteCmd(), utils.MarkPreReleaseCommand()) c.Use = c.Use + " " + sharePointServiceCommandDeleteUseSuffix c.Example = sharePointServiceCommandDeleteExamples @@ -210,7 +210,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error { defer utils.CloseRepo(ctx, r) - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), acct, connector.Sites) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), acct, connector.Sites) if err != nil { return Only(ctx, errors.Wrap(err, "Failed to connect to Microsoft APIs")) } diff --git a/src/cli/cli.go b/src/cli/cli.go index f06354f0b..b67663d06 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -15,6 +15,7 @@ import ( "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/repo" "github.com/alcionai/corso/src/cli/restore" + "github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/pkg/logger" @@ -31,7 +32,27 @@ var corsoCmd = &cobra.Command{ Short: "Free, Secure, Open-Source Backup for M365.", Long: `Free, Secure, and Open-Source Backup for Microsoft 365.`, RunE: handleCorsoCmd, - PersistentPreRunE: config.InitFunc(), + PersistentPreRunE: preRun, +} + +func preRun(cc *cobra.Command, args []string) error { + if err := config.InitFunc(cc, args); err != nil { + return err + } + + log := logger.Ctx(cc.Context()) + + flags := utils.GetPopulatedFlags(cc) + flagSl := make([]string, 0, len(flags)) + + // currently only tracking flag names to avoid pii leakage. + for f := range flags { + flagSl = append(flagSl, f) + } + + log.Infow("cli command", "command", cc.CommandPath(), "flags", flagSl, "version", version.CurrentVersion()) + + return nil } // Handler for flat calls to `corso`. @@ -39,7 +60,7 @@ var corsoCmd = &cobra.Command{ func handleCorsoCmd(cmd *cobra.Command, args []string) error { v, _ := cmd.Flags().GetBool("version") if v { - print.Outf(cmd.Context(), "Corso version: "+version.Version) + print.Outf(cmd.Context(), "Corso version: "+version.CurrentVersion()) return nil } @@ -62,7 +83,7 @@ func BuildCommandTree(cmd *cobra.Command) { cmd.PersistentFlags().SortFlags = false cmd.Flags().BoolP("version", "v", false, "current version info") - cmd.PersistentPostRunE = config.InitFunc() + cmd.PersistentPreRunE = preRun config.AddConfigFlags(cmd) logger.AddLoggingFlags(cmd) observe.AddProgressBarFlags(cmd) @@ -85,6 +106,7 @@ func BuildCommandTree(cmd *cobra.Command) { // Handle builds and executes the cli processor. func Handle() { + //nolint:forbidigo ctx := config.Seed(context.Background()) ctx = print.SetRootCmd(ctx, corsoCmd) observe.SeedWriter(ctx, print.StderrWriter(ctx), observe.PreloadFlags()) diff --git a/src/cli/config/config.go b/src/cli/config/config.go index dbcd21422..8f532abb6 100644 --- a/src/cli/config/config.go +++ b/src/cli/config/config.go @@ -77,20 +77,18 @@ func AddConfigFlags(cmd *cobra.Command) { // InitFunc provides a func that lazily initializes viper and // verifies that the configuration was able to read a file. -func InitFunc() func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - fp := configFilePathFlag - if len(fp) == 0 || fp == displayDefaultFP { - fp = configFilePath - } - - err := initWithViper(GetViper(cmd.Context()), fp) - if err != nil { - return err - } - - return Read(cmd.Context()) +func InitFunc(cmd *cobra.Command, args []string) error { + fp := configFilePathFlag + if len(fp) == 0 || fp == displayDefaultFP { + fp = configFilePath } + + err := initWithViper(GetViper(cmd.Context()), fp) + if err != nil { + return err + } + + return Read(cmd.Context()) } // initWithViper implements InitConfig, but takes in a viper diff --git a/src/cli/restore/sharepoint.go b/src/cli/restore/sharepoint.go index 1ce4b2b91..d8c8826c8 100644 --- a/src/cli/restore/sharepoint.go +++ b/src/cli/restore/sharepoint.go @@ -35,7 +35,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command { switch cmd.Use { case restoreCommand: - c, fs = utils.AddCommand(cmd, sharePointRestoreCmd(), utils.HideCommand()) + c, fs = utils.AddCommand(cmd, sharePointRestoreCmd(), utils.MarkPreReleaseCommand()) c.Use = c.Use + " " + sharePointServiceCommandUseSuffix diff --git a/src/cli/utils/utils.go b/src/cli/utils/utils.go index 0dc43d079..029f9b2bf 100644 --- a/src/cli/utils/utils.go +++ b/src/cli/utils/utils.go @@ -59,7 +59,8 @@ func HasNoFlagsAndShownHelp(cmd *cobra.Command) bool { } type cmdCfg struct { - hidden bool + hidden bool + preRelese bool } type cmdOpt func(*cmdCfg) @@ -76,6 +77,13 @@ func HideCommand() cmdOpt { } } +func MarkPreReleaseCommand() cmdOpt { + return func(cc *cmdCfg) { + cc.hidden = true + cc.preRelese = true + } +} + // AddCommand adds a clone of the subCommand to the parent, // and returns both the clone and its pflags. func AddCommand(parent, c *cobra.Command, opts ...cmdOpt) (*cobra.Command, *pflag.FlagSet) { @@ -85,6 +93,14 @@ func AddCommand(parent, c *cobra.Command, opts ...cmdOpt) (*cobra.Command, *pfla parent.AddCommand(c) c.Hidden = cc.hidden + if cc.preRelese { + // There is a default deprecated message that always shows so we do some terminal magic to overwrite it + c.Deprecated = "\n\033[1F\033[K" + + "==================================================================================================\n" + + "\tWARNING!!! THIS IS A PRE-RELEASE COMMAND THAT MAY NOT FUNCTION PROPERLY, OR AT ALL\n" + + "==================================================================================================\n" + } + c.Flags().SortFlags = false return c, c.Flags() diff --git a/src/cmd/factory/impl/common.go b/src/cmd/factory/impl/common.go index 585118442..0ea6835dd 100644 --- a/src/cmd/factory/impl/common.go +++ b/src/cmd/factory/impl/common.go @@ -112,7 +112,7 @@ func getGCAndVerifyUser(ctx context.Context, userID string) (*connector.GraphCon } // build a graph connector - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), acct, connector.Users) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), acct, connector.Users) if err != nil { return nil, account.Account{}, errors.Wrap(err, "connecting to graph api") } diff --git a/src/cmd/getM365/getItem.go b/src/cmd/getM365/getItem.go index 24ce81d9a..d24b27d38 100644 --- a/src/cmd/getM365/getItem.go +++ b/src/cmd/getM365/getItem.go @@ -178,7 +178,7 @@ func getGC(ctx context.Context) (*connector.GraphConnector, account.M365Config, return nil, m365Cfg, Only(ctx, errors.Wrap(err, "finding m365 account details")) } - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), acct, connector.Users) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), acct, connector.Users) if err != nil { return nil, m365Cfg, Only(ctx, errors.Wrap(err, "connecting to graph API")) } diff --git a/src/cmd/purge/purge.go b/src/cmd/purge/purge.go index 32100772d..cb9c9976f 100644 --- a/src/cmd/purge/purge.go +++ b/src/cmd/purge/purge.go @@ -151,7 +151,12 @@ func purgeOneDriveFolders( uid string, ) error { getter := func(gs graph.Servicer, uid, prefix string) ([]purgable, error) { - cfs, err := onedrive.GetAllFolders(ctx, gs, uid, prefix) + pager, err := onedrive.PagerForSource(onedrive.OneDriveSource, gs, uid, nil) + if err != nil { + return nil, err + } + + cfs, err := onedrive.GetAllFolders(ctx, gs, pager, prefix) if err != nil { return nil, err } @@ -255,7 +260,7 @@ func getGC(ctx context.Context) (*connector.GraphConnector, error) { } // build a graph connector - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), acct, connector.Users) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), acct, connector.Users) if err != nil { return nil, Only(ctx, errors.Wrap(err, "connecting to graph api")) } diff --git a/src/go.mod b/src/go.mod index 3e2eb34db..a8054ab0e 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/alcionai/clues v0.0.0-20230120231953-1cf61dbafc40 - github.com/aws/aws-sdk-go v1.44.187 + github.com/aws/aws-sdk-go v1.44.190 github.com/aws/aws-xray-sdk-go v1.8.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 @@ -13,6 +13,7 @@ require ( github.com/microsoft/kiota-abstractions-go v0.16.0 github.com/microsoft/kiota-authentication-azure-go v0.6.0 github.com/microsoft/kiota-http-go v0.13.0 + github.com/microsoft/kiota-serialization-form-go v0.2.0 github.com/microsoft/kiota-serialization-json-go v0.7.2 github.com/microsoftgraph/msgraph-sdk-go v0.53.0 github.com/microsoftgraph/msgraph-sdk-go-core v0.33.0 @@ -40,7 +41,6 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/microsoft/kiota-serialization-form-go v0.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/spf13/afero v1.9.3 // indirect @@ -84,7 +84,7 @@ require ( github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/microsoft/kiota-serialization-text-go v0.6.0 // indirect + github.com/microsoft/kiota-serialization-text-go v0.6.0 github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/minio-go/v7 v7.0.45 // indirect github.com/minio/sha256-simd v1.0.0 // indirect diff --git a/src/go.sum b/src/go.sum index 22f9b12a2..72af64b2d 100644 --- a/src/go.sum +++ b/src/go.sum @@ -62,8 +62,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/aws/aws-sdk-go v1.44.187 h1:D5CsRomPnlwDHJCanL2mtaLIcbhjiWxNh5j8zvaWdJA= -github.com/aws/aws-sdk-go v1.44.187/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.190 h1:QC+Pf/Ooj7Waf2obOPZbIQOqr00hy4h54j3ZK9mvHcc= +github.com/aws/aws-sdk-go v1.44.190/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-xray-sdk-go v1.8.0 h1:0xncHZ588wB/geLjbM/esoW3FOEThWy2TJyb4VXfLFY= github.com/aws/aws-xray-sdk-go v1.8.0/go.mod h1:7LKe47H+j3evfvS1+q0wzpoaGXGrF3mUsfM+thqVO+A= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= diff --git a/src/internal/connector/data_collections.go b/src/internal/connector/data_collections.go index 0b6d20b27..7d187a854 100644 --- a/src/internal/connector/data_collections.go +++ b/src/internal/connector/data_collections.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/pkg/errors" + "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery/api" @@ -35,27 +36,27 @@ func (gc *GraphConnector) DataCollections( sels selectors.Selector, metadata []data.Collection, ctrlOpts control.Options, -) ([]data.Collection, error) { +) ([]data.Collection, map[string]struct{}, error) { ctx, end := D.Span(ctx, "gc:dataCollections", D.Index("service", sels.Service.String())) defer end() err := verifyBackupInputs(sels, gc.GetUsers(), gc.GetSiteIDs()) if err != nil { - return nil, err + return nil, nil, err } serviceEnabled, err := checkServiceEnabled(ctx, gc.Owners.Users(), path.ServiceType(sels.Service), sels.DiscreteOwner) if err != nil { - return nil, err + return nil, nil, err } if !serviceEnabled { - return []data.Collection{}, nil + return []data.Collection{}, nil, nil } switch sels.Service { case selectors.ServiceExchange: - colls, err := exchange.DataCollections( + colls, excludes, err := exchange.DataCollections( ctx, sels, metadata, @@ -64,7 +65,7 @@ func (gc *GraphConnector) DataCollections( gc.UpdateStatus, ctrlOpts) if err != nil { - return nil, err + return nil, nil, err } for _, c := range colls { @@ -79,13 +80,13 @@ func (gc *GraphConnector) DataCollections( } } - return colls, nil + return colls, excludes, nil case selectors.ServiceOneDrive: return gc.OneDriveDataCollections(ctx, sels, ctrlOpts) case selectors.ServiceSharePoint: - colls, err := sharepoint.DataCollections( + colls, excludes, err := sharepoint.DataCollections( ctx, gc.itemClient, sels, @@ -94,17 +95,17 @@ func (gc *GraphConnector) DataCollections( gc, ctrlOpts) if err != nil { - return nil, err + return nil, nil, err } for range colls { gc.incrementAwaitingMessages() } - return colls, nil + return colls, excludes, nil default: - return nil, errors.Errorf("service %s not supported", sels.Service.String()) + return nil, nil, errors.Errorf("service %s not supported", sels.Service.String()) } } @@ -182,15 +183,16 @@ func (gc *GraphConnector) OneDriveDataCollections( ctx context.Context, selector selectors.Selector, ctrlOpts control.Options, -) ([]data.Collection, error) { +) ([]data.Collection, map[string]struct{}, error) { odb, err := selector.ToOneDriveBackup() if err != nil { - return nil, errors.Wrap(err, "oneDriveDataCollection: parsing selector") + return nil, nil, errors.Wrap(err, "oneDriveDataCollection: parsing selector") } var ( user = selector.DiscreteOwner collections = []data.Collection{} + allExcludes = map[string]struct{}{} errs error ) @@ -198,7 +200,7 @@ func (gc *GraphConnector) OneDriveDataCollections( for _, scope := range odb.Scopes() { logger.Ctx(ctx).With("user", user).Debug("Creating OneDrive collections") - odcs, err := onedrive.NewCollections( + odcs, excludes, err := onedrive.NewCollections( gc.itemClient, gc.credentials.AzureTenantID, user, @@ -209,15 +211,17 @@ func (gc *GraphConnector) OneDriveDataCollections( ctrlOpts, ).Get(ctx) if err != nil { - return nil, support.WrapAndAppend(user, err, errs) + return nil, nil, support.WrapAndAppend(user, err, errs) } collections = append(collections, odcs...) + + maps.Copy(allExcludes, excludes) } for range collections { gc.incrementAwaitingMessages() } - return collections, errs + return collections, allExcludes, errs } diff --git a/src/internal/connector/data_collections_test.go b/src/internal/connector/data_collections_test.go index 57332ce1a..c90bee511 100644 --- a/src/internal/connector/data_collections_test.go +++ b/src/internal/connector/data_collections_test.go @@ -44,7 +44,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) SetupSuite() { tester.MustGetEnvVars(suite.T(), tester.M365AcctCredEnvs...) - suite.connector = loadConnector(ctx, suite.T(), graph.LargeItemClient(), AllResources) + suite.connector = loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), AllResources) suite.user = tester.M365UserID(suite.T()) suite.site = tester.M365SiteID(suite.T()) @@ -63,7 +63,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection selUsers := []string{suite.user} - connector := loadConnector(ctx, suite.T(), graph.LargeItemClient(), Users) + connector := loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), Users) tests := []struct { name string getSelector func(t *testing.T) selectors.Selector @@ -99,7 +99,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { - collections, err := exchange.DataCollections( + collections, excludes, err := exchange.DataCollections( ctx, test.getSelector(t), nil, @@ -108,6 +108,8 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection control.Options{}) require.NoError(t, err) + assert.Empty(t, excludes) + for range collections { connector.incrementAwaitingMessages() } @@ -139,7 +141,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestDataCollections_invali owners := []string{"snuffleupagus"} - connector := loadConnector(ctx, suite.T(), graph.LargeItemClient(), Users) + connector := loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), Users) tests := []struct { name string getSelector func(t *testing.T) selectors.Selector @@ -199,9 +201,10 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestDataCollections_invali for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { - collections, err := connector.DataCollections(ctx, test.getSelector(t), nil, control.Options{}) + collections, excludes, err := connector.DataCollections(ctx, test.getSelector(t), nil, control.Options{}) assert.Error(t, err) assert.Empty(t, collections) + assert.Empty(t, excludes) }) } } @@ -215,7 +218,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestSharePointDataCollecti selSites := []string{suite.site} - connector := loadConnector(ctx, suite.T(), graph.LargeItemClient(), Sites) + connector := loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), Sites) tests := []struct { name string expected int @@ -242,15 +245,17 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestSharePointDataCollecti for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { - collections, err := sharepoint.DataCollections( + collections, excludes, err := sharepoint.DataCollections( ctx, - graph.LargeItemClient(), + graph.HTTPClient(graph.NoTimeout()), test.getSelector(), connector.credentials.AzureTenantID, connector.Service, connector, control.Options{}) require.NoError(t, err) + // Not expecting excludes as this isn't an incremental backup. + assert.Empty(t, excludes) for range collections { connector.incrementAwaitingMessages() @@ -300,7 +305,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) SetupSuite() { tester.MustGetEnvSets(suite.T(), tester.M365AcctCredEnvs) - suite.connector = loadConnector(ctx, suite.T(), graph.LargeItemClient(), Sites) + suite.connector = loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), Sites) suite.user = tester.M365UserID(suite.T()) tester.LogTimeOfTest(suite.T()) @@ -313,16 +318,18 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar var ( t = suite.T() siteID = tester.M365SiteID(t) - gc = loadConnector(ctx, t, graph.LargeItemClient(), Sites) + gc = loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), Sites) siteIDs = []string{siteID} ) sel := selectors.NewSharePointBackup(siteIDs) sel.Include(sel.Libraries([]string{"foo"}, selectors.PrefixMatch())) - cols, err := gc.DataCollections(ctx, sel.Selector, nil, control.Options{}) + cols, excludes, err := gc.DataCollections(ctx, sel.Selector, nil, control.Options{}) require.NoError(t, err) assert.Len(t, cols, 1) + // No excludes yet as this isn't an incremental backup. + assert.Empty(t, excludes) for _, collection := range cols { t.Logf("Path: %s\n", collection.FullPath().String()) @@ -337,16 +344,18 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar var ( t = suite.T() siteID = tester.M365SiteID(t) - gc = loadConnector(ctx, t, graph.LargeItemClient(), Sites) + gc = loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), Sites) siteIDs = []string{siteID} ) sel := selectors.NewSharePointBackup(siteIDs) sel.Include(sel.Lists(selectors.Any(), selectors.PrefixMatch())) - cols, err := gc.DataCollections(ctx, sel.Selector, nil, control.Options{}) + cols, excludes, err := gc.DataCollections(ctx, sel.Selector, nil, control.Options{}) require.NoError(t, err) assert.Less(t, 0, len(cols)) + // No excludes yet as this isn't an incremental backup. + assert.Empty(t, excludes) for _, collection := range cols { t.Logf("Path: %s\n", collection.FullPath().String()) diff --git a/src/internal/connector/discovery/api/beta_service.go b/src/internal/connector/discovery/api/beta_service.go new file mode 100644 index 000000000..df2b1533b --- /dev/null +++ b/src/internal/connector/discovery/api/beta_service.go @@ -0,0 +1,43 @@ +package api + +import ( + "github.com/alcionai/corso/src/internal/connector/graph/betasdk" + absser "github.com/microsoft/kiota-abstractions-go/serialization" + msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" + "github.com/pkg/errors" +) + +// Service wraps BetaClient's functionality. +// Abstraction created to comply loosely with graph.Servicer +// methods for ease of switching between v1.0 and beta connnectors +type Service struct { + client *betasdk.BetaClient +} + +func (s Service) Client() *betasdk.BetaClient { + return s.client +} + +func NewBetaService(adpt *msgraphsdk.GraphRequestAdapter) *Service { + return &Service{ + client: betasdk.NewBetaClient(adpt), + } +} + +// Seraialize writes an M365 parsable object into a byte array using the built-in +// application/json writer within the adapter. +func (s Service) Serialize(object absser.Parsable) ([]byte, error) { + writer, err := s.client.Adapter(). + GetSerializationWriterFactory(). + GetSerializationWriter("application/json") + if err != nil || writer == nil { + return nil, errors.Wrap(err, "creating json serialization writer") + } + + err = writer.WriteObjectValue("", object) + if err != nil { + return nil, errors.Wrap(err, "writeObjecValue serialization") + } + + return writer.GetSerializedContent() +} diff --git a/src/internal/connector/discovery/api/beta_service_test.go b/src/internal/connector/discovery/api/beta_service_test.go new file mode 100644 index 000000000..ad67b3877 --- /dev/null +++ b/src/internal/connector/discovery/api/beta_service_test.go @@ -0,0 +1,49 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/betasdk/models" + "github.com/alcionai/corso/src/internal/tester" +) + +type BetaUnitSuite struct { + suite.Suite +} + +func TestBetaUnitSuite(t *testing.T) { + suite.Run(t, new(BetaUnitSuite)) +} + +func (suite *BetaUnitSuite) TestBetaService_Adapter() { + t := suite.T() + a := tester.NewM365Account(t) + m365, err := a.M365Config() + require.NoError(t, err) + + adpt, err := graph.CreateAdapter( + m365.AzureTenantID, + m365.AzureClientID, + m365.AzureClientSecret, + ) + require.NoError(t, err) + + service := NewBetaService(adpt) + require.NotNil(t, service) + + testPage := models.NewSitePage() + name := "testFile" + desc := "working with parsing" + + testPage.SetName(&name) + testPage.SetDescription(&desc) + + byteArray, err := service.Serialize(testPage) + assert.NotEmpty(t, byteArray) + assert.NoError(t, err) +} diff --git a/src/internal/connector/exchange/api/api.go b/src/internal/connector/exchange/api/api.go index 6edd68f57..c4858c5c1 100644 --- a/src/internal/connector/exchange/api/api.go +++ b/src/internal/connector/exchange/api/api.go @@ -2,9 +2,11 @@ package api import ( "context" + "strings" "time" "github.com/microsoft/kiota-abstractions-go/serialization" + "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" @@ -57,6 +59,11 @@ type Client struct { // The stable service is re-usable for any non-paged request. // This allows us to maintain performance across async requests. stable graph.Servicer + + // The largeItem graph servicer is configured specifically for + // downloading large items. Specifically for use when handling + // attachments, and for no other use. + largeItem graph.Servicer } // NewClient produces a new exchange api client. Must be used in @@ -67,27 +74,45 @@ func NewClient(creds account.M365Config) (Client, error) { return Client{}, err } - return Client{creds, s}, nil + li, err := newLargeItemService(creds) + if err != nil { + return Client{}, err + } + + return Client{creds, s, li}, nil } // service generates a new service. Used for paged and other long-running // requests instead of the client's stable service, so that in-flight state // within the adapter doesn't get clobbered func (c Client) service() (*graph.Service, error) { - return newService(c.Credentials) + s, err := newService(c.Credentials) + return s, err } func newService(creds account.M365Config) (*graph.Service, error) { - adapter, err := graph.CreateAdapter( + a, err := graph.CreateAdapter( + creds.AzureTenantID, + creds.AzureClientID, + creds.AzureClientSecret) + if err != nil { + return nil, errors.Wrap(err, "generating no-timeout graph adapter") + } + + return graph.NewService(a), nil +} + +func newLargeItemService(creds account.M365Config) (*graph.Service, error) { + a, err := graph.CreateAdapter( creds.AzureTenantID, creds.AzureClientID, creds.AzureClientSecret, - ) + graph.NoTimeout()) if err != nil { - return nil, errors.Wrap(err, "generating graph api service client") + return nil, errors.Wrap(err, "generating no-timeout graph adapter") } - return graph.NewService(adapter), nil + return graph.NewService(a), nil } // --------------------------------------------------------------------------- @@ -117,3 +142,14 @@ func orNow(t *time.Time) time.Time { return *t } + +func HasAttachments(body models.ItemBodyable) bool { + if body.GetContent() == nil || body.GetContentType() == nil || + *body.GetContentType() == models.TEXT_BODYTYPE || len(*body.GetContent()) == 0 { + return false + } + + content := *body.GetContent() + + return strings.Contains(content, "src=\"cid:") +} diff --git a/src/internal/connector/exchange/api/api_test.go b/src/internal/connector/exchange/api/api_test.go index d3fe51350..4fe842452 100644 --- a/src/internal/connector/exchange/api/api_test.go +++ b/src/internal/connector/exchange/api/api_test.go @@ -3,11 +3,14 @@ package api import ( "testing" + "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/mockconnector" + "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" ) @@ -190,3 +193,57 @@ func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() { }) } } + +//nolint:lll +var stubHTMLContent = "\r\n
Happy New Year,

In accordance with TPS report guidelines, there have been questions about how to address our activities SharePoint Cover page. Do you believe this is the best picture? 



Let me know if this meets our culture requirements.

Warm Regards,

Dustin
" + +func (suite *ExchangeServiceSuite) TestHasAttachments() { + tests := []struct { + name string + hasAttachment assert.BoolAssertionFunc + getBodyable func(t *testing.T) models.ItemBodyable + }{ + { + name: "Mock w/out attachment", + hasAttachment: assert.False, + getBodyable: func(t *testing.T) models.ItemBodyable { + byteArray := mockconnector.GetMockMessageWithBodyBytes( + "Test", + "This is testing", + "This is testing", + ) + message, err := support.CreateMessageFromBytes(byteArray) + require.NoError(t, err) + return message.GetBody() + }, + }, + { + name: "Mock w/ inline attachment", + hasAttachment: assert.True, + getBodyable: func(t *testing.T) models.ItemBodyable { + byteArray := mockconnector.GetMessageWithOneDriveAttachment("Test legacy") + message, err := support.CreateMessageFromBytes(byteArray) + require.NoError(t, err) + return message.GetBody() + }, + }, + { + name: "Edge Case", + hasAttachment: assert.True, + getBodyable: func(t *testing.T) models.ItemBodyable { + body := models.NewItemBody() + body.SetContent(&stubHTMLContent) + cat := models.HTML_BODYTYPE + body.SetContentType(&cat) + return body + }, + }, + } + + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + found := HasAttachments(test.getBodyable(t)) + test.hasAttachment(t, found) + }) + } +} diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index e12f4b795..0db1e964c 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/backup/details" ) @@ -47,9 +48,8 @@ func (c Contacts) CreateContactFolder( return c.stable.Client().UsersById(user).ContactFolders().Post(ctx, requestBody, nil) } -// DeleteContactFolder deletes the ContactFolder associated with the M365 ID if permissions are valid. -// Errors returned if the function call was not successful. -func (c Contacts) DeleteContactFolder( +// DeleteContainer deletes the ContactFolder associated with the M365 ID if permissions are valid. +func (c Contacts) DeleteContainer( ctx context.Context, user, folderID string, ) error { @@ -173,7 +173,7 @@ type contactPager struct { options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration } -func (p *contactPager) getPage(ctx context.Context) (pageLinker, error) { +func (p *contactPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { return p.builder.Get(ctx, p.options) } @@ -181,7 +181,7 @@ func (p *contactPager) setNext(nextLink string) { p.builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(nextLink, p.gs.Adapter()) } -func (p *contactPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { +func (p *contactPager) valuesIn(pl api.DeltaPageLinker) ([]getIDAndAddtler, error) { return toValues[models.Contactable](pl) } @@ -215,7 +215,7 @@ func (c Contacts) GetAddedAndRemovedItemIDs( } // only return on error if it is NOT a delta issue. // on bad deltas we retry the call with the regular builder - if graph.IsErrInvalidDelta(err) == nil { + if !graph.IsErrInvalidDelta(err) { return nil, nil, DeltaUpdate{}, err } diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index f78aef76b..e643c1f89 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -14,6 +14,7 @@ import ( "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/logger" @@ -49,9 +50,9 @@ func (c Events) CreateCalendar( return c.stable.Client().UsersById(user).Calendars().Post(ctx, requestbody, nil) } -// DeleteCalendar removes calendar from user's M365 account +// DeleteContainer removes a calendar from user's M365 account // Reference: https://docs.microsoft.com/en-us/graph/api/calendar-delete?view=graph-rest-1.0&tabs=go -func (c Events) DeleteCalendar( +func (c Events) DeleteContainer( ctx context.Context, user, calendarID string, ) error { @@ -85,12 +86,37 @@ func (c Events) GetItem( ctx context.Context, user, itemID string, ) (serialization.Parsable, *details.ExchangeInfo, error) { - evt, err := c.stable.Client().UsersById(user).EventsById(itemID).Get(ctx, nil) + event, err := c.stable.Client().UsersById(user).EventsById(itemID).Get(ctx, nil) if err != nil { return nil, nil, err } - return evt, EventInfo(evt), nil + var errs *multierror.Error + + if *event.GetHasAttachments() || HasAttachments(event.GetBody()) { + for count := 0; count < numberOfRetries; count++ { + attached, err := c.largeItem. + Client(). + UsersById(user). + EventsById(itemID). + Attachments(). + Get(ctx, nil) + if err == nil { + event.SetAttachments(attached.GetValue()) + break + } + + logger.Ctx(ctx).Debugw("retrying event attachment download", "err", err) + errs = multierror.Append(errs, err) + } + + if err != nil { + logger.Ctx(ctx).Errorw("event attachment download exceeded maximum retries", "err", errs) + return nil, nil, support.WrapAndAppend(itemID, errors.Wrap(err, "download event attachment"), nil) + } + } + + return event, EventInfo(event), nil } func (c Client) GetAllCalendarNamesForUser( @@ -178,7 +204,7 @@ type eventPager struct { options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration } -func (p *eventPager) getPage(ctx context.Context) (pageLinker, error) { +func (p *eventPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { resp, err := p.builder.Get(ctx, p.options) return resp, err } @@ -187,7 +213,7 @@ func (p *eventPager) setNext(nextLink string) { p.builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(nextLink, p.gs.Adapter()) } -func (p *eventPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { +func (p *eventPager) valuesIn(pl api.DeltaPageLinker) ([]getIDAndAddtler, error) { return toValues[models.Eventable](pl) } @@ -216,7 +242,7 @@ func (c Events) GetAddedAndRemovedItemIDs( } // only return on error if it is NOT a delta issue. // on bad deltas we retry the call with the regular builder - if graph.IsErrInvalidDelta(err) == nil { + if !graph.IsErrInvalidDelta(err) { return nil, nil, DeltaUpdate{}, err } @@ -249,8 +275,7 @@ func (c Events) GetAddedAndRemovedItemIDs( // Serialization // --------------------------------------------------------------------------- -// Serialize retrieves attachment data identified by the event item, and then -// serializes it into a byte slice. +// Serialize transforms the event into a byte slice. func (c Events) Serialize( ctx context.Context, item serialization.Parsable, @@ -268,31 +293,6 @@ func (c Events) Serialize( defer writer.Close() - if *event.GetHasAttachments() || support.HasAttachments(event.GetBody()) { - // getting all the attachments might take a couple attempts due to filesize - var retriesErr error - - for count := 0; count < numberOfRetries; count++ { - attached, err := c.stable. - Client(). - UsersById(user). - EventsById(itemID). - Attachments(). - Get(ctx, nil) - retriesErr = err - - if err == nil { - event.SetAttachments(attached.GetValue()) - break - } - } - - if retriesErr != nil { - logger.Ctx(ctx).Debug("exceeded maximum retries") - return nil, support.WrapAndAppend(itemID, errors.Wrap(retriesErr, "attachment failed"), nil) - } - } - if err = writer.WriteObjectValue("", event); err != nil { return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) } diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index c83c3f7bb..bbac48a66 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/logger" @@ -71,9 +72,9 @@ func (c Mail) CreateMailFolderWithParent( Post(ctx, requestBody, nil) } -// DeleteMailFolder removes a mail folder with the corresponding M365 ID from the user's M365 Exchange account +// DeleteContainer removes a mail folder with the corresponding M365 ID from the user's M365 Exchange account // Reference: https://docs.microsoft.com/en-us/graph/api/mailfolder-delete?view=graph-rest-1.0&tabs=http -func (c Mail) DeleteMailFolder( +func (c Mail) DeleteContainer( ctx context.Context, user, folderID string, ) error { @@ -97,7 +98,8 @@ func (c Mail) GetContainerByID( return service.Client().UsersById(userID).MailFoldersById(dirID).Get(ctx, ofmf) } -// GetItem retrieves a Messageable item. +// GetItem retrieves a Messageable item. If the item contains an attachment, that +// attachment is also downloaded. func (c Mail) GetItem( ctx context.Context, user, itemID string, @@ -107,6 +109,31 @@ func (c Mail) GetItem( return nil, nil, err } + var errs *multierror.Error + + if *mail.GetHasAttachments() || HasAttachments(mail.GetBody()) { + for count := 0; count < numberOfRetries; count++ { + attached, err := c.largeItem. + Client(). + UsersById(user). + MessagesById(itemID). + Attachments(). + Get(ctx, nil) + if err == nil { + mail.SetAttachments(attached.GetValue()) + break + } + + logger.Ctx(ctx).Debugw("retrying mail attachment download", "err", err) + errs = multierror.Append(errs, err) + } + + if err != nil { + logger.Ctx(ctx).Errorw("mail attachment download exceeded maximum retries", "err", errs) + return nil, nil, support.WrapAndAppend(itemID, errors.Wrap(err, "downloading mail attachment"), nil) + } + } + return mail, MailInfo(mail), nil } @@ -172,7 +199,7 @@ type mailPager struct { options *users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration } -func (p *mailPager) getPage(ctx context.Context) (pageLinker, error) { +func (p *mailPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) { return p.builder.Get(ctx, p.options) } @@ -180,7 +207,7 @@ func (p *mailPager) setNext(nextLink string) { p.builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(nextLink, p.gs.Adapter()) } -func (p *mailPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { +func (p *mailPager) valuesIn(pl api.DeltaPageLinker) ([]getIDAndAddtler, error) { return toValues[models.Messageable](pl) } @@ -215,7 +242,7 @@ func (c Mail) GetAddedAndRemovedItemIDs( } // only return on error if it is NOT a delta issue. // on bad deltas we retry the call with the regular builder - if graph.IsErrInvalidDelta(err) == nil { + if !graph.IsErrInvalidDelta(err) { return nil, nil, DeltaUpdate{}, err } @@ -238,8 +265,7 @@ func (c Mail) GetAddedAndRemovedItemIDs( // Serialization // --------------------------------------------------------------------------- -// Serialize retrieves attachment data identified by the mail item, and then -// serializes it into a byte slice. +// Serialize transforms the mail item into a byte slice. func (c Mail) Serialize( ctx context.Context, item serialization.Parsable, @@ -257,32 +283,6 @@ func (c Mail) Serialize( defer writer.Close() - if *msg.GetHasAttachments() || support.HasAttachments(msg.GetBody()) { - // getting all the attachments might take a couple attempts due to filesize - var retriesErr error - - for count := 0; count < numberOfRetries; count++ { - attached, err := c.stable. - Client(). - UsersById(user). - MessagesById(itemID). - Attachments(). - Get(ctx, nil) - retriesErr = err - - if err == nil { - msg.SetAttachments(attached.GetValue()) - break - } - } - - if retriesErr != nil { - logger.Ctx(ctx).Debug("exceeded maximum retries") - return nil, support.WrapAndAppend(itemID, - support.ConnectorStackErrorTraceWrap(retriesErr, "attachment Failed"), nil) - } - } - if err = writer.WriteObjectValue("", msg); err != nil { return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) } diff --git a/src/internal/connector/exchange/api/shared.go b/src/internal/connector/exchange/api/shared.go index c77e21fa8..d89ce7411 100644 --- a/src/internal/connector/exchange/api/shared.go +++ b/src/internal/connector/exchange/api/shared.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/connector/support" ) @@ -14,14 +15,9 @@ import ( // --------------------------------------------------------------------------- type itemPager interface { - getPage(context.Context) (pageLinker, error) + getPage(context.Context) (api.DeltaPageLinker, error) setNext(string) - valuesIn(pageLinker) ([]getIDAndAddtler, error) -} - -type pageLinker interface { - GetOdataDeltaLink() *string - GetOdataNextLink() *string + valuesIn(api.DeltaPageLinker) ([]getIDAndAddtler, error) } type getIDAndAddtler interface { @@ -72,11 +68,7 @@ func getItemsAddedAndRemovedFromContainer( // get the next page of data, check for standard errors resp, err := pager.getPage(ctx) if err != nil { - if err := graph.IsErrDeletedInFlight(err); err != nil { - return nil, nil, deltaURL, err - } - - if err := graph.IsErrInvalidDelta(err); err != nil { + if graph.IsErrDeletedInFlight(err) || graph.IsErrInvalidDelta(err) { return nil, nil, deltaURL, err } @@ -102,24 +94,24 @@ func getItemsAddedAndRemovedFromContainer( } } + nextLink, delta := api.NextAndDeltaLink(resp) + // the deltaLink is kind of like a cursor for overall data state. // once we run through pages of nextLinks, the last query will // produce a deltaLink instead (if supported), which we'll use on // the next backup to only get the changes since this run. - delta := resp.GetOdataDeltaLink() - if delta != nil && len(*delta) > 0 { - deltaURL = *delta + if len(delta) > 0 { + deltaURL = delta } // the nextLink is our page cursor within this query. // if we have more data to retrieve, we'll have a // nextLink instead of a deltaLink. - nextLink := resp.GetOdataNextLink() - if nextLink == nil || len(*nextLink) == 0 { + if len(nextLink) == 0 { break } - pager.setNext(*nextLink) + pager.setNext(nextLink) } return addedIDs, removedIDs, deltaURL, nil diff --git a/src/internal/connector/exchange/data_collections.go b/src/internal/connector/exchange/data_collections.go index 719764d35..41bc16301 100644 --- a/src/internal/connector/exchange/data_collections.go +++ b/src/internal/connector/exchange/data_collections.go @@ -167,10 +167,10 @@ func DataCollections( acct account.M365Config, su support.StatusUpdater, ctrlOpts control.Options, -) ([]data.Collection, error) { +) ([]data.Collection, map[string]struct{}, error) { eb, err := selector.ToExchangeBackup() if err != nil { - return nil, errors.Wrap(err, "exchangeDataCollection: parsing selector") + return nil, nil, errors.Wrap(err, "exchangeDataCollection: parsing selector") } var ( @@ -181,7 +181,7 @@ func DataCollections( cdps, err := parseMetadataCollections(ctx, metadata) if err != nil { - return nil, err + return nil, nil, err } for _, scope := range eb.Scopes() { @@ -196,13 +196,15 @@ func DataCollections( ctrlOpts, su) if err != nil { - return nil, support.WrapAndAppend(user, err, errs) + return nil, nil, support.WrapAndAppend(user, err, errs) } collections = append(collections, dcs...) } - return collections, errs + // Exchange does not require adding items to the global exclude list so always + // return nil. + return collections, nil, errs } func getterByType(ac api.Client, category path.CategoryType) (addedAndRemovedItemIDsGetter, error) { @@ -251,7 +253,10 @@ func createCollections( Credentials: creds, } - foldersComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf("%s - %s", qp.Category, user)) + foldersComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf( + "%s - %s", + observe.Safe(qp.Category.String()), + observe.PII(user))) defer closer() defer close(foldersComplete) diff --git a/src/internal/connector/exchange/exchange_data_collection.go b/src/internal/connector/exchange/exchange_data_collection.go index ce37beab6..d53e3dbe9 100644 --- a/src/internal/connector/exchange/exchange_data_collection.go +++ b/src/internal/connector/exchange/exchange_data_collection.go @@ -173,6 +173,9 @@ func (col *Collection) streamItems(ctx context.Context) { colProgress chan<- struct{} user = col.user + log = logger.Ctx(ctx).With( + "service", path.ExchangeService.String(), + "category", col.category.String()) ) defer func() { @@ -183,9 +186,9 @@ func (col *Collection) streamItems(ctx context.Context) { var closer func() colProgress, closer = observe.CollectionProgress( ctx, - user, col.fullPath.Category().String(), - col.fullPath.Folder()) + observe.PII(user), + observe.PII(col.fullPath.Folder())) go closer() @@ -251,58 +254,19 @@ func (col *Collection) streamItems(ctx context.Context) { err error ) - for i := 1; i <= numberOfRetries; i++ { - item, info, err = col.items.GetItem(ctx, user, id) - if err == nil { - break - } - - // If the data is no longer available just return here and chalk it up - // as a success. There's no reason to retry and no way we can backup up - // enough information to restore the item anyway. - if e := graph.IsErrDeletedInFlight(err); e != nil { - atomic.AddInt64(&success, 1) - logger.Ctx(ctx).Infow( - "Graph reported item not found", - "error", - e, - "service", - path.ExchangeService.String(), - "category", - col.category.String, - ) - - return - } - - if i < numberOfRetries { - time.Sleep(time.Duration(3*(i+1)) * time.Second) - } - } - + item, info, err = getItemWithRetries(ctx, user, id, col.items) if err != nil { // Don't report errors for deleted items as there's no way for us to - // back up data that is gone. Chalk them up as a "success" though since - // there's really nothing we can do and not reporting it will make the - // status code upset cause we won't have the same number of results as - // attempted items. - if e := graph.IsErrDeletedInFlight(err); e != nil { + // back up data that is gone. Record it as a "success", since there's + // nothing else we can do, and not reporting it will make the status + // investigation upset. + if graph.IsErrDeletedInFlight(err) { atomic.AddInt64(&success, 1) - logger.Ctx(ctx).Infow( - "Graph reported item not found", - "error", - e, - "service", - path.ExchangeService.String(), - "category", - col.category.String, - ) - - return + log.Infow("item not found", "err", err) + } else { + errUpdater(user, support.ConnectorStackErrorTraceWrap(err, "fetching item")) } - errUpdater(user, support.ConnectorStackErrorTraceWrap(err, "fetching item")) - return } @@ -333,6 +297,42 @@ func (col *Collection) streamItems(ctx context.Context) { wg.Wait() } +// get an item while handling retry and backoff. +func getItemWithRetries( + ctx context.Context, + userID, itemID string, + items itemer, +) (serialization.Parsable, *details.ExchangeInfo, error) { + var ( + item serialization.Parsable + info *details.ExchangeInfo + err error + ) + + for i := 1; i <= numberOfRetries; i++ { + item, info, err = items.GetItem(ctx, userID, itemID) + if err == nil { + break + } + + // If the data is no longer available just return here and chalk it up + // as a success. There's no reason to retry; it's gone Let it go. + if graph.IsErrDeletedInFlight(err) { + return nil, nil, err + } + + if i < numberOfRetries { + time.Sleep(time.Duration(3*(i+1)) * time.Second) + } + } + + if err != nil { + return nil, nil, err + } + + return item, info, err +} + // terminatePopulateSequence is a utility function used to close a Collection's data channel // and to send the status update through the channel. func (col *Collection) finishPopulation(ctx context.Context, success int, totalBytes int64, errs error) { diff --git a/src/internal/connector/exchange/exchange_data_collection_test.go b/src/internal/connector/exchange/exchange_data_collection_test.go index a63a7caf8..e45f3d80c 100644 --- a/src/internal/connector/exchange/exchange_data_collection_test.go +++ b/src/internal/connector/exchange/exchange_data_collection_test.go @@ -10,23 +10,33 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/alcionai/corso/src/internal/common" + "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/path" ) -type mockItemer struct{} +type mockItemer struct { + getCount int + serializeCount int + getErr error + serializeErr error +} -func (mi mockItemer) GetItem( +func (mi *mockItemer) GetItem( context.Context, string, string, ) (serialization.Parsable, *details.ExchangeInfo, error) { - return nil, nil, nil + mi.getCount++ + return nil, nil, mi.getErr } -func (mi mockItemer) Serialize(context.Context, serialization.Parsable, string, string) ([]byte, error) { - return nil, nil +func (mi *mockItemer) Serialize(context.Context, serialization.Parsable, string, string) ([]byte, error) { + mi.serializeCount++ + return nil, mi.serializeErr } type ExchangeDataCollectionSuite struct { @@ -153,10 +163,58 @@ func (suite *ExchangeDataCollectionSuite) TestNewCollection_state() { "u", test.curr, test.prev, 0, - mockItemer{}, nil, + &mockItemer{}, nil, control.Options{}, false) assert.Equal(t, test.expect, c.State()) }) } } + +func (suite *ExchangeDataCollectionSuite) TestGetItemWithRetries() { + table := []struct { + name string + items *mockItemer + expectErr func(*testing.T, error) + expectGetCalls int + }{ + { + name: "happy", + items: &mockItemer{}, + expectErr: func(t *testing.T, err error) { + assert.NoError(t, err) + }, + expectGetCalls: 1, + }, + { + name: "an error", + items: &mockItemer{getErr: assert.AnError}, + expectErr: func(t *testing.T, err error) { + assert.Error(t, err) + }, + expectGetCalls: 3, + }, + { + name: "deleted in flight", + items: &mockItemer{ + getErr: graph.ErrDeletedInFlight{ + Err: *common.EncapsulateError(assert.AnError), + }, + }, + expectErr: func(t *testing.T, err error) { + assert.True(t, graph.IsErrDeletedInFlight(err), "is ErrDeletedInFlight") + }, + expectGetCalls: 1, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + // itemer is mocked, so only the errors are configured atm. + _, _, err := getItemWithRetries(ctx, "userID", "itemID", test.items) + test.expectErr(t, err) + }) + } +} diff --git a/src/internal/connector/exchange/restore_test.go b/src/internal/connector/exchange/restore_test.go index f439ea3ad..9c32fd530 100644 --- a/src/internal/connector/exchange/restore_test.go +++ b/src/internal/connector/exchange/restore_test.go @@ -76,7 +76,7 @@ func (suite *ExchangeRestoreSuite) TestRestoreContact() { defer func() { // Remove the folder containing contact prior to exiting test - err = suite.ac.Contacts().DeleteContactFolder(ctx, userID, folderID) + err = suite.ac.Contacts().DeleteContainer(ctx, userID, folderID) assert.NoError(t, err) }() @@ -110,7 +110,7 @@ func (suite *ExchangeRestoreSuite) TestRestoreEvent() { defer func() { // Removes calendar containing events created during the test - err = suite.ac.Events().DeleteCalendar(ctx, userID, calendarID) + err = suite.ac.Events().DeleteContainer(ctx, userID, calendarID) assert.NoError(t, err) }() @@ -124,6 +124,10 @@ func (suite *ExchangeRestoreSuite) TestRestoreEvent() { assert.NotNil(t, info, "event item info") } +type containerDeleter interface { + DeleteContainer(context.Context, string, string) error +} + // TestRestoreExchangeObject verifies path.Category usage for restored objects func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { a := tester.NewM365Account(suite.T()) @@ -133,20 +137,24 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { service, err := createService(m365) require.NoError(suite.T(), err) + deleters := map[path.CategoryType]containerDeleter{ + path.EmailCategory: suite.ac.Mail(), + path.ContactsCategory: suite.ac.Contacts(), + path.EventsCategory: suite.ac.Events(), + } + userID := tester.M365UserID(suite.T()) now := time.Now() tests := []struct { name string bytes []byte category path.CategoryType - cleanupFunc func(context.Context, string, string) error destination func(*testing.T, context.Context) string }{ { - name: "Test Mail", - bytes: mockconnector.GetMockMessageBytes("Restore Exchange Object"), - category: path.EmailCategory, - cleanupFunc: suite.ac.Mail().DeleteMailFolder, + name: "Test Mail", + bytes: mockconnector.GetMockMessageBytes("Restore Exchange Object"), + category: path.EmailCategory, destination: func(t *testing.T, ctx context.Context) string { folderName := "TestRestoreMailObject: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) @@ -156,10 +164,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Mail: One Direct Attachment", - bytes: mockconnector.GetMockMessageWithDirectAttachment("Restore 1 Attachment"), - category: path.EmailCategory, - cleanupFunc: suite.ac.Mail().DeleteMailFolder, + name: "Test Mail: One Direct Attachment", + bytes: mockconnector.GetMockMessageWithDirectAttachment("Restore 1 Attachment"), + category: path.EmailCategory, destination: func(t *testing.T, ctx context.Context) string { folderName := "TestRestoreMailwithAttachment: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) @@ -169,10 +176,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Mail: One Large Attachment", - bytes: mockconnector.GetMockMessageWithLargeAttachment("Restore Large Attachment"), - category: path.EmailCategory, - cleanupFunc: suite.ac.Mail().DeleteMailFolder, + name: "Test Mail: One Large Attachment", + bytes: mockconnector.GetMockMessageWithLargeAttachment("Restore Large Attachment"), + category: path.EmailCategory, destination: func(t *testing.T, ctx context.Context) string { folderName := "TestRestoreMailwithLargeAttachment: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) @@ -182,10 +188,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Mail: Two Attachments", - bytes: mockconnector.GetMockMessageWithTwoAttachments("Restore 2 Attachments"), - category: path.EmailCategory, - cleanupFunc: suite.ac.Mail().DeleteMailFolder, + name: "Test Mail: Two Attachments", + bytes: mockconnector.GetMockMessageWithTwoAttachments("Restore 2 Attachments"), + category: path.EmailCategory, destination: func(t *testing.T, ctx context.Context) string { folderName := "TestRestoreMailwithAttachments: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) @@ -195,10 +200,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Mail: Reference(OneDrive) Attachment", - bytes: mockconnector.GetMessageWithOneDriveAttachment("Restore Reference(OneDrive) Attachment"), - category: path.EmailCategory, - cleanupFunc: suite.ac.Mail().DeleteMailFolder, + name: "Test Mail: Reference(OneDrive) Attachment", + bytes: mockconnector.GetMessageWithOneDriveAttachment("Restore Reference(OneDrive) Attachment"), + category: path.EmailCategory, destination: func(t *testing.T, ctx context.Context) string { folderName := "TestRestoreMailwithReferenceAttachment: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Mail().CreateMailFolder(ctx, userID, folderName) @@ -209,10 +213,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, // TODO: #884 - reinstate when able to specify root folder by name { - name: "Test Contact", - bytes: mockconnector.GetMockContactBytes("Test_Omega"), - category: path.ContactsCategory, - cleanupFunc: suite.ac.Contacts().DeleteContactFolder, + name: "Test Contact", + bytes: mockconnector.GetMockContactBytes("Test_Omega"), + category: path.ContactsCategory, destination: func(t *testing.T, ctx context.Context) string { folderName := "TestRestoreContactObject: " + common.FormatSimpleDateTime(now) folder, err := suite.ac.Contacts().CreateContactFolder(ctx, userID, folderName) @@ -222,10 +225,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Events", - bytes: mockconnector.GetDefaultMockEventBytes("Restored Event Object"), - category: path.EventsCategory, - cleanupFunc: suite.ac.Events().DeleteCalendar, + name: "Test Events", + bytes: mockconnector.GetDefaultMockEventBytes("Restored Event Object"), + category: path.EventsCategory, destination: func(t *testing.T, ctx context.Context) string { calendarName := "TestRestoreEventObject: " + common.FormatSimpleDateTime(now) calendar, err := suite.ac.Events().CreateCalendar(ctx, userID, calendarName) @@ -235,10 +237,9 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { }, }, { - name: "Test Event with Attachment", - bytes: mockconnector.GetMockEventWithAttachment("Restored Event Attachment"), - category: path.EventsCategory, - cleanupFunc: suite.ac.Events().DeleteCalendar, + name: "Test Event with Attachment", + bytes: mockconnector.GetMockEventWithAttachment("Restored Event Attachment"), + category: path.EventsCategory, destination: func(t *testing.T, ctx context.Context) string { calendarName := "TestRestoreEventObject_" + common.FormatSimpleDateTime(now) calendar, err := suite.ac.Events().CreateCalendar(ctx, userID, calendarName) @@ -266,9 +267,7 @@ func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() { ) assert.NoError(t, err, support.ConnectorStackErrorTrace(err)) assert.NotNil(t, info, "item info is populated") - - cleanupError := test.cleanupFunc(ctx, userID, destination) - assert.NoError(t, cleanupError) + assert.NoError(t, deleters[test.category].DeleteContainer(ctx, userID, destination)) }) } } diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index 0880ad233..b59f37877 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -93,8 +93,7 @@ func filterContainersAndFillCollections( added, removed, newDelta, err := getter.GetAddedAndRemovedItemIDs(ctx, qp.ResourceOwner, cID, prevDelta) if err != nil { - // note == nil check; only catches non-inFlight error cases. - if graph.IsErrDeletedInFlight(err) == nil { + if !graph.IsErrDeletedInFlight(err) { errs = support.WrapAndAppend(qp.ResourceOwner, err, errs) continue } diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index d385dab81..e1144249a 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -374,7 +374,11 @@ func restoreCollection( user = directory.ResourceOwner() ) - colProgress, closer := observe.CollectionProgress(ctx, user, category.String(), directory.Folder()) + colProgress, closer := observe.CollectionProgress( + ctx, + category.String(), + observe.PII(user), + observe.PII(directory.Folder())) defer closer() defer close(colProgress) diff --git a/src/internal/connector/graph/api/api.go b/src/internal/connector/graph/api/api.go new file mode 100644 index 000000000..abcf29a24 --- /dev/null +++ b/src/internal/connector/graph/api/api.go @@ -0,0 +1,30 @@ +package api + +type PageLinker interface { + GetOdataNextLink() *string +} + +type DeltaPageLinker interface { + PageLinker + GetOdataDeltaLink() *string +} + +func NextLink(pl PageLinker) string { + next := pl.GetOdataNextLink() + if next == nil || len(*next) == 0 { + return "" + } + + return *next +} + +func NextAndDeltaLink(pl DeltaPageLinker) (string, string) { + next := NextLink(pl) + + delta := pl.GetOdataDeltaLink() + if delta == nil || len(*delta) == 0 { + return next, "" + } + + return next, *delta +} diff --git a/src/internal/connector/graph/api/api_test.go b/src/internal/connector/graph/api/api_test.go new file mode 100644 index 000000000..37932396d --- /dev/null +++ b/src/internal/connector/graph/api/api_test.go @@ -0,0 +1,114 @@ +package api_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/connector/graph/api" +) + +type mockNextLink struct { + nextLink *string +} + +func (l mockNextLink) GetOdataNextLink() *string { + return l.nextLink +} + +type mockDeltaNextLink struct { + mockNextLink + deltaLink *string +} + +func (l mockDeltaNextLink) GetOdataDeltaLink() *string { + return l.deltaLink +} + +type testInput struct { + name string + inputLink *string + expectedLink string +} + +// Needs to be var not const so we can take the address of it. +var ( + emptyLink = "" + link = "foo" + link2 = "bar" + + nextLinkInputs = []testInput{ + { + name: "empty", + inputLink: &emptyLink, + expectedLink: "", + }, + { + name: "nil", + inputLink: nil, + expectedLink: "", + }, + { + name: "non_empty", + inputLink: &link, + expectedLink: link, + }, + } +) + +type APIUnitSuite struct { + suite.Suite +} + +func TestAPIUnitSuite(t *testing.T) { + suite.Run(t, new(APIUnitSuite)) +} + +func (suite *APIUnitSuite) TestNextLink() { + for _, test := range nextLinkInputs { + suite.T().Run(test.name, func(t *testing.T) { + l := mockNextLink{nextLink: test.inputLink} + assert.Equal(t, test.expectedLink, api.NextLink(l)) + }) + } +} + +func (suite *APIUnitSuite) TestNextAndDeltaLink() { + deltaTable := []testInput{ + { + name: "empty", + inputLink: &emptyLink, + expectedLink: "", + }, + { + name: "nil", + inputLink: nil, + expectedLink: "", + }, + { + name: "non_empty", + // Use a different link so we can see if the results get swapped or something. + inputLink: &link2, + expectedLink: link2, + }, + } + + for _, next := range nextLinkInputs { + for _, delta := range deltaTable { + name := strings.Join([]string{next.name, "next", delta.name, "delta"}, "_") + + suite.T().Run(name, func(t *testing.T) { + l := mockDeltaNextLink{ + mockNextLink: mockNextLink{nextLink: next.inputLink}, + deltaLink: delta.inputLink, + } + gotNext, gotDelta := api.NextAndDeltaLink(l) + + assert.Equal(t, next.expectedLink, gotNext) + assert.Equal(t, delta.expectedLink, gotDelta) + }) + } + } +} diff --git a/src/internal/connector/graph/betasdk/beta_client.go b/src/internal/connector/graph/betasdk/beta_client.go index 7b0316b38..f33b110d6 100644 --- a/src/internal/connector/graph/betasdk/beta_client.go +++ b/src/internal/connector/graph/betasdk/beta_client.go @@ -15,6 +15,7 @@ import ( // Details on how the Code was generated is present in `kioter-lock.json`. // NOTE: kiota gen file is altered to indicate what files are included in the created // +<<<<<<< HEAD // Beta files use an adapter that allows for ASync() request. This feature is disabled in main. Generic Kiota adapters do not support. // For the client, only calls that begin as client.SitesBy(siteID).Pages() have an endpoint. // @@ -24,6 +25,19 @@ import ( // Supported Call source are located within the sites subdirectory // Specifics on `betaClient.SitesById(siteID).Pages` are located: sites/site_item_request_builder.go // +======= +// Changes to Sites Directory: +// Access files send requests with an adapter's with ASync() support. +// This feature is not enabled in v1.0. Manually changed in remaining files. +// Additionally, only calls that begin as client.SitesBy(siteID).Pages() have an endpoint. +// +// The use case specific to Pages(). All other requests should be routed to the /internal/connector/graph.Servicer +// Specifics on `betaClient.SitesById(siteID).Pages` are located: sites/site_item_request_builder.go +// +// Required model files are identified as `modelFiles` in kiota-lock.json. Directory -> betasdk/models +// Required access files are identified as `sitesFiles` in kiota-lock.json. Directory -> betasdk/sites +// +>>>>>>> main // BetaClient minimal msgraph-beta-sdk-go for connecting to msgraph-beta-sdk-go // for retrieving `SharePoint.Pages`. Code is generated from kiota.dev. // requestAdapter is registered with the following the serializers: @@ -82,3 +96,8 @@ func (m *BetaClient) SitesById(id string) *i1a3c1a5501c5e41b7fd169f2d4c768dce9b0 } return i1a3c1a5501c5e41b7fd169f2d4c768dce9b096ac28fb5431bf02afcc57295411.NewSiteItemRequestBuilderInternal(urlTplParams, m.requestAdapter) } + +// Adapter() helper method to export Adapter for iterating +func (m *BetaClient) Adapter() *msgraphsdk.GraphRequestAdapter { + return m.requestAdapter +} diff --git a/src/internal/connector/graph/betasdk/kiota-lock.json b/src/internal/connector/graph/betasdk/kiota-lock.json index bbcafae09..21a111aef 100644 --- a/src/internal/connector/graph/betasdk/kiota-lock.json +++ b/src/internal/connector/graph/betasdk/kiota-lock.json @@ -1,34 +1,131 @@ { - "lockFileVersion": "1.0.0", - "kiotaVersion": "0.10.0.0", - "clientClassName": "BetaClient", - "clientNamespaceName": "github.com/alcionai/corso/src/internal/connector/graph/betasdk", - "language": "Go", - "usesBackingStore": false, - "includeAdditionalData": true, - "serializers": [ - "Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory", - "Microsoft.Kiota.Serialization.Text.TextSerializationWriterFactory", - "Microsoft.Kiota.Serialization.Form.FormSerializationWriterFactory" - ], - "deserializers": [ - "Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory", - "Microsoft.Kiota.Serialization.Text.TextParseNodeFactory", - "Microsoft.Kiota.Serialization.Form.FormParseNodeFactory" - ], - "structuredMimeTypes": [ - "application/json", - "text/plain", - "application/x-www-form-urlencoded" - ], - "includePatterns": [ - "**/sites/**" - ], - "excludePatterns": [ - "**/admin/**", - "**/users/**", - "**/groups/**", - "**/onenote/**" - ], - "disabledValidationRules": [] + "lockFileVersion": "1.0.0", + "kiotaVersion": "0.10.0.0", + "clientClassName": "BetaClient", + "clientNamespaceName": "github.com/alcionai/corso/src/internal/connector/graph/betasdk", + "language": "Go", + "betaVersion": "0.53.0", + "usesBackingStore": false, + "includeAdditionalData": true, + "serializers": [ + "Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory", + "Microsoft.Kiota.Serialization.Text.TextSerializationWriterFactory", + "Microsoft.Kiota.Serialization.Form.FormSerializationWriterFactory" + ], + "deserializers": [ + "Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory", + "Microsoft.Kiota.Serialization.Text.TextParseNodeFactory", + "Microsoft.Kiota.Serialization.Form.FormParseNodeFactory" + ], + "structuredMimeTypes": [ + "application/json", + "text/plain", + "application/x-www-form-urlencoded" + ], + "includePatterns": [ + "**/sites/**" + ], + "excludePatterns": [ + "**/admin/**", + "**/users/**", + "**/groups/**", + "**/onenote/**" + ], + "sitesFiles": [ + "count_request_builder.go", + "item_pages_count_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_count_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_horizontal_section_item_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_count_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_horizontal_section_column_item_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_item_webparts_count_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_item_webparts_item_get_position_of_web_part_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_item_webparts_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_item_webparts_web_part_item_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_item_columns_request_builder.go", + "item_pages_item_canvas_layout_horizontal_sections_request_builder.go", + "item_pages_item_canvas_layout_request_builder.go", + "item_pages_item_canvas_layout_vertical_section_request_builder.go", + "item_pages_item_canvas_layout_vertical_section_webparts_count_request_builder.go", + "item_pages_item_canvas_layout_vertical_section_webparts_item_get_position_of_web_part_request_builder.go", + "item_pages_item_canvas_layout_vertical_section_webparts_request_builder.go", + "item_pages_item_canvas_layout_vertical_section_webparts_web_part_item_request_builder.go", + "item_pages_item_get_web_parts_by_position_post_request_body.go", + "item_pages_item_get_web_parts_by_position_post_request_bodyable.go", + "item_pages_item_get_web_parts_by_position_request_builder.go", + "item_pages_item_get_web_parts_by_position_response.go", + "item_pages_item_get_web_parts_by_position_responseable.go", + "item_pages_item_publish_request_builder.go", + "item_pages_item_web_parts_count_request_builder.go", + "item_pages_item_web_parts_item_get_position_of_web_part_request_builder.go", + "item_pages_item_web_parts_request_builder.go", + "item_pages_item_web_parts_web_part_item_request_builder.go", + "item_pages_request_builder.go", + "item_pages_site_page_item_request_builder.go", + "item_sites_count_request_builder.go", + "item_sites_site_item_request_builder.go", + "site_item_request_builder.go" + ], + "modelFiles":[ + "base_item.go", + "page_layout_type.go", + "standard_web_partable.go", + "canvas_layout.go", + "page_promotion_type.go", + "text_web_part.go", + "canvas_layoutable.go", + "publication_facet.go", + "text_web_part_collection_response.go", + "horizontal_section.go", + "publication_facetable.go", + "text_web_part_collection_responseable.go", + "horizontal_section_collection_response.go", + "reactions_facet.go", + "text_web_partable.go", + "horizontal_section_collection_responseable.go", + "reactions_facetable.go", + "title_area.go", + "horizontal_section_column.go", + "section_emphasis_type.go", + "title_area_layout_type.go", + "horizontal_section_column_collection_response.go", + "server_processed_content.go", + "title_area_text_alignment_type.go", + "horizontal_section_column_collection_responseable.go", + "server_processed_contentable.go", + "title_areaable.go", + "horizontal_section_columnable.go", + "site_access_type.go", + "vertical_section.go", + "horizontal_section_layout_type.go", + "site_page.go", + "vertical_sectionable.go", + "horizontal_sectionable.go", + "site_page_collection_response.go", + "web_part.go", + "meta_data_key_string_pair.go", + "site_page_collection_responseable.go", + "web_part_collection_response.go", + "meta_data_key_string_pair_collection_response.go", + "site_pageable.go", + "web_part_collection_responseable.go", + "meta_data_key_string_pair_collection_responseable.go", + "site_security_level.go", + "web_part_data.go", + "meta_data_key_string_pairable.go", + "site_settings.go", + "web_part_dataable.go", + "meta_data_key_value_pair.go", + "site_settingsable.go", + "web_part_position.go", + "meta_data_key_value_pair_collection_response.go", + "standard_web_part.go", + "web_part_positionable.go", + "meta_data_key_value_pair_collection_responseable.go", + "standard_web_part_collection_response.go", + "web_partable.go", + "meta_data_key_value_pairable.go", + "standard_web_part_collection_responseable.go" + ], + "disabledValidationRules": [] } diff --git a/src/internal/connector/graph/errors.go b/src/internal/connector/graph/errors.go index 86cec64bd..c75e4a6cb 100644 --- a/src/internal/connector/graph/errors.go +++ b/src/internal/connector/graph/errors.go @@ -26,27 +26,32 @@ const ( errCodeMailboxNotEnabledForRESTAPI = "MailboxNotEnabledForRESTAPI" ) +var ( + Err401Unauthorized = errors.New("401 unauthorized") + // normally the graph client will catch this for us, but in case we + // run our own client Do(), we need to translate it to a timeout type + // failure locally. + Err429TooManyRequests = errors.New("429 too many requests") + Err503ServiceUnavailable = errors.New("503 Service Unavailable") +) + // The folder or item was deleted between the time we identified // it and when we tried to fetch data for it. type ErrDeletedInFlight struct { common.Err } -func IsErrDeletedInFlight(err error) error { - if asDeletedInFlight(err) { - return err +func IsErrDeletedInFlight(err error) bool { + e := ErrDeletedInFlight{} + if errors.As(err, &e) { + return true } if hasErrorCode(err, errCodeItemNotFound, errCodeSyncFolderNotFound) { - return ErrDeletedInFlight{*common.EncapsulateError(err)} + return true } - return nil -} - -func asDeletedInFlight(err error) bool { - e := ErrDeletedInFlight{} - return errors.As(err, &e) + return false } // Delta tokens can be desycned or expired. In either case, the token @@ -56,21 +61,17 @@ type ErrInvalidDelta struct { common.Err } -func IsErrInvalidDelta(err error) error { - if asInvalidDelta(err) { - return err +func IsErrInvalidDelta(err error) bool { + e := ErrInvalidDelta{} + if errors.As(err, &e) { + return true } if hasErrorCode(err, errCodeSyncStateNotFound, errCodeResyncRequired) { - return ErrInvalidDelta{*common.EncapsulateError(err)} + return true } - return nil -} - -func asInvalidDelta(err error) bool { - e := ErrInvalidDelta{} - return errors.As(err, &e) + return false } func IsErrExchangeMailFolderNotFound(err error) bool { @@ -85,23 +86,72 @@ type ErrTimeout struct { common.Err } -func IsErrTimeout(err error) error { - if asTimeout(err) { - return err +func IsErrTimeout(err error) bool { + e := ErrTimeout{} + if errors.As(err, &e) { + return true } - if isTimeoutErr(err) { - return ErrTimeout{*common.EncapsulateError(err)} + if errors.Is(err, context.DeadlineExceeded) || os.IsTimeout(err) { + return true } - return nil + switch err := err.(type) { + case *url.Error: + return err.Timeout() + default: + return false + } } -func asTimeout(err error) bool { - e := ErrTimeout{} +type ErrThrottled struct { + common.Err +} + +func IsErrThrottled(err error) bool { + if errors.Is(err, Err429TooManyRequests) { + return true + } + + e := ErrThrottled{} + return errors.As(err, &e) } +type ErrUnauthorized struct { + common.Err +} + +func IsErrUnauthorized(err error) bool { + // TODO: refine this investigation. We don't currently know if + // a specific item download url expired, or if the full connection + // auth expired. + if errors.Is(err, Err401Unauthorized) { + return true + } + + e := ErrUnauthorized{} + + return errors.As(err, &e) +} + +type ErrServiceUnavailable struct { + common.Err +} + +func IsSericeUnavailable(err error) bool { + if errors.Is(err, Err503ServiceUnavailable) { + return true + } + + e := ErrUnauthorized{} + if errors.As(err, &e) { + return true + } + + return true +} + // --------------------------------------------------------------------------- // error parsers // --------------------------------------------------------------------------- @@ -122,20 +172,3 @@ func hasErrorCode(err error, codes ...string) bool { return slices.Contains(codes, *oDataError.GetError().GetCode()) } - -// isTimeoutErr is used to determine if the Graph error returned is -// because of Timeout. This is used to restrict retries to just -// timeouts as other errors are handled within a middleware in the -// client. -func isTimeoutErr(err error) bool { - if errors.Is(err, context.DeadlineExceeded) || os.IsTimeout(err) { - return true - } - - switch err := err.(type) { - case *url.Error: - return err.Timeout() - default: - return false - } -} diff --git a/src/internal/connector/graph/service.go b/src/internal/connector/graph/service.go index 7780e7941..6c0e6dbc1 100644 --- a/src/internal/connector/graph/service.go +++ b/src/internal/connector/graph/service.go @@ -2,15 +2,28 @@ package graph import ( "context" + "net/http" + "net/http/httputil" + "os" + "time" - absser "github.com/microsoft/kiota-abstractions-go/serialization" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/microsoft/kiota-abstractions-go/serialization" + ka "github.com/microsoft/kiota-authentication-azure-go" + khttp "github.com/microsoft/kiota-http-go" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" + msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" "github.com/pkg/errors" "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" ) +const ( + logGraphRequestsEnvKey = "LOG_GRAPH_REQUESTS" +) + // AllMetadataFileNames produces the standard set of filenames used to store graph // metadata such as delta tokens and folderID->path references. func AllMetadataFileNames() []string { @@ -23,6 +36,10 @@ type QueryParams struct { Credentials account.M365Config } +// --------------------------------------------------------------------------- +// Service Handler +// --------------------------------------------------------------------------- + var _ Servicer = &Service{} type Service struct { @@ -47,7 +64,7 @@ func (s Service) Client() *msgraphsdk.GraphServiceClient { // Seraialize writes an M365 parsable object into a byte array using the built-in // application/json writer within the adapter. -func (s Service) Serialize(object absser.Parsable) ([]byte, error) { +func (s Service) Serialize(object serialization.Parsable) ([]byte, error) { writer, err := s.adapter.GetSerializationWriterFactory().GetSerializationWriter("application/json") if err != nil || writer == nil { return nil, errors.Wrap(err, "creating json serialization writer") @@ -61,6 +78,90 @@ func (s Service) Serialize(object absser.Parsable) ([]byte, error) { return writer.GetSerializedContent() } +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +type clientConfig struct { + noTimeout bool +} + +type option func(*clientConfig) + +// populate constructs a clientConfig according to the provided options. +func (c *clientConfig) populate(opts ...option) *clientConfig { + for _, opt := range opts { + opt(c) + } + + return c +} + +// apply updates the http.Client with the expected options. +func (c *clientConfig) apply(hc *http.Client) { + if c.noTimeout { + hc.Timeout = 0 + } +} + +// NoTimeout sets the httpClient.Timeout to 0 (unlimited). +// The resulting client isn't suitable for most queries, due to the +// capacity for a call to persist forever. This configuration should +// only be used when downloading very large files. +func NoTimeout() option { + return func(c *clientConfig) { + c.noTimeout = true + } +} + +// CreateAdapter uses provided credentials to log into M365 using Kiota Azure Library +// with Azure identity package. An adapter object is a necessary to component +// to create *msgraphsdk.GraphServiceClient +func CreateAdapter(tenant, client, secret string, opts ...option) (*msgraphsdk.GraphRequestAdapter, error) { + // Client Provider: Uses Secret for access to tenant-level data + cred, err := azidentity.NewClientSecretCredential(tenant, client, secret, nil) + if err != nil { + return nil, errors.Wrap(err, "creating m365 client secret credentials") + } + + auth, err := ka.NewAzureIdentityAuthenticationProviderWithScopes( + cred, + []string{"https://graph.microsoft.com/.default"}, + ) + if err != nil { + return nil, errors.Wrap(err, "creating new AzureIdentityAuthentication") + } + + httpClient := HTTPClient(opts...) + + return msgraphsdk.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient( + auth, nil, nil, httpClient) +} + +// HTTPClient creates the httpClient with middlewares and timeout configured +// +// Re-use of http clients is critical, or else we leak OS resources +// and consume relatively unbound socket connections. It is important +// to centralize this client to be passed downstream where api calls +// can utilize it on a per-download basis. +func HTTPClient(opts ...option) *http.Client { + clientOptions := msgraphsdk.GetDefaultClientOptions() + middlewares := msgraphgocore.GetDefaultMiddlewaresWithOptions(&clientOptions) + middlewares = append(middlewares, &LoggingMiddleware{}) + httpClient := msgraphgocore.GetDefaultClient(&clientOptions, middlewares...) + httpClient.Timeout = time.Second * 90 + + (&clientConfig{}). + populate(opts...). + apply(httpClient) + + return httpClient +} + +// --------------------------------------------------------------------------- +// Interfaces +// --------------------------------------------------------------------------- + type Servicer interface { // Client() returns msgraph Service client that can be used to process and execute // the majority of the queries to the M365 Backstore @@ -120,3 +221,78 @@ type ContainerResolver interface { // Items returns the containers in the cache. Items() []CachedContainer } + +// --------------------------------------------------------------------------- +// Client Middleware +// --------------------------------------------------------------------------- + +// LoggingMiddleware can be used to log the http request sent by the graph client +type LoggingMiddleware struct{} + +func (handler *LoggingMiddleware) Intercept( + pipeline khttp.Pipeline, + middlewareIndex int, + req *http.Request, +) (*http.Response, error) { + var ( + ctx = req.Context() + resp, err = pipeline.Next(req, middlewareIndex) + ) + + if resp == nil { + return resp, err + } + + // Return immediately if the response is good (2xx). + // If api logging is toggled, log a body-less dump of the request/resp. + if (resp.StatusCode / 100) == 2 { + if logger.DebugAPI || os.Getenv(logGraphRequestsEnvKey) != "" { + respDump, _ := httputil.DumpResponse(resp, false) + + metadata := []any{ + "idx", middlewareIndex, + "method", req.Method, + "status", resp.Status, + "statusCode", resp.StatusCode, + "requestLen", req.ContentLength, + "url", req.URL, + "response", respDump, + } + + logger.Ctx(ctx).Debugw("2xx graph api resp", metadata...) + } + + return resp, err + } + + // Log errors according to api debugging configurations. + // When debugging is toggled, every non-2xx is recorded with a respose dump. + // Otherwise, throttling cases and other non-2xx responses are logged + // with a slimmer reference for telemetry/supportability purposes. + if logger.DebugAPI || os.Getenv(logGraphRequestsEnvKey) != "" { + respDump, _ := httputil.DumpResponse(resp, true) + + metadata := []any{ + "idx", middlewareIndex, + "method", req.Method, + "status", resp.Status, + "statusCode", resp.StatusCode, + "requestLen", req.ContentLength, + "url", req.URL, + "response", string(respDump), + } + + logger.Ctx(ctx).Errorw("non-2xx graph api response", metadata...) + } else { + // special case for supportability: log all throttling cases. + if resp.StatusCode == http.StatusTooManyRequests { + logger.Ctx(ctx).Infow("graph api throttling", "method", req.Method, "url", req.URL) + } + + if resp.StatusCode != http.StatusTooManyRequests && (resp.StatusCode/100) != 2 { + logger.Ctx(ctx).Infow("graph api error", "status", resp.Status, "method", req.Method, "url", req.URL) + } + } + + return resp, err +} diff --git a/src/internal/connector/graph/service_helper.go b/src/internal/connector/graph/service_helper.go deleted file mode 100644 index 900919406..000000000 --- a/src/internal/connector/graph/service_helper.go +++ /dev/null @@ -1,125 +0,0 @@ -package graph - -import ( - "net/http" - "net/http/httputil" - "os" - "time" - - az "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - ka "github.com/microsoft/kiota-authentication-azure-go" - khttp "github.com/microsoft/kiota-http-go" - msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" - msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" - "github.com/pkg/errors" - - "github.com/alcionai/corso/src/pkg/logger" -) - -const ( - logGraphRequestsEnvKey = "LOG_GRAPH_REQUESTS" -) - -// CreateAdapter uses provided credentials to log into M365 using Kiota Azure Library -// with Azure identity package. An adapter object is a necessary to component -// to create *msgraphsdk.GraphServiceClient -func CreateAdapter(tenant, client, secret string) (*msgraphsdk.GraphRequestAdapter, error) { - // Client Provider: Uses Secret for access to tenant-level data - cred, err := az.NewClientSecretCredential(tenant, client, secret, nil) - if err != nil { - return nil, errors.Wrap(err, "creating m365 client secret credentials") - } - - auth, err := ka.NewAzureIdentityAuthenticationProviderWithScopes( - cred, - []string{"https://graph.microsoft.com/.default"}, - ) - if err != nil { - return nil, errors.Wrap(err, "creating new AzureIdentityAuthentication") - } - - httpClient := CreateHTTPClient() - - return msgraphsdk.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient( - auth, nil, nil, httpClient) -} - -// CreateHTTPClient creates the httpClient with middlewares and timeout configured -func CreateHTTPClient() *http.Client { - clientOptions := msgraphsdk.GetDefaultClientOptions() - middlewares := msgraphgocore.GetDefaultMiddlewaresWithOptions(&clientOptions) - middlewares = append(middlewares, &LoggingMiddleware{}) - httpClient := msgraphgocore.GetDefaultClient(&clientOptions, middlewares...) - httpClient.Timeout = time.Second * 90 - - return httpClient -} - -// LargeItemClient generates a client that's configured to handle -// large file downloads. This client isn't suitable for other queries -// due to loose restrictions on timeouts and such. -// -// Re-use of http clients is critical, or else we leak os resources -// and consume relatively unbound socket connections. It is important -// to centralize this client to be passed downstream where api calls -// can utilize it on a per-download basis. -// -// TODO: this should get owned by an API client layer, not the GC itself. -func LargeItemClient() *http.Client { - httpClient := CreateHTTPClient() - httpClient.Timeout = 0 // infinite timeout for pulling large files - - return httpClient -} - -// --------------------------------------------------------------------------- -// Logging Middleware -// --------------------------------------------------------------------------- - -// LoggingMiddleware can be used to log the http request sent by the graph client -type LoggingMiddleware struct{} - -func (handler *LoggingMiddleware) Intercept( - pipeline khttp.Pipeline, - middlewareIndex int, - req *http.Request, -) (*http.Response, error) { - var ( - ctx = req.Context() - resp, err = pipeline.Next(req, middlewareIndex) - ) - - if resp == nil { - return resp, err - } - - if (resp.StatusCode / 100) == 2 { - return resp, err - } - - // special case for supportability: log all throttling cases. - if resp.StatusCode == http.StatusTooManyRequests { - logger.Ctx(ctx).Infow("graph api throttling", "method", req.Method, "url", req.URL) - } - - if resp.StatusCode != http.StatusTooManyRequests && (resp.StatusCode/100) != 2 { - logger.Ctx(ctx).Infow("graph api error", "method", req.Method, "url", req.URL) - } - - if logger.DebugAPI || os.Getenv(logGraphRequestsEnvKey) != "" { - respDump, _ := httputil.DumpResponse(resp, true) - - metadata := []any{ - "method", req.Method, - "url", req.URL, - "requestLen", req.ContentLength, - "status", resp.Status, - "statusCode", resp.StatusCode, - "request", string(respDump), - } - - logger.Ctx(ctx).Errorw("non-2xx graph api response", metadata...) - } - - return resp, err -} diff --git a/src/internal/connector/graph/service_test.go b/src/internal/connector/graph/service_test.go index ee8c6bc29..14bdc9c36 100644 --- a/src/internal/connector/graph/service_test.go +++ b/src/internal/connector/graph/service_test.go @@ -1,14 +1,15 @@ -package graph_test +package graph import ( + "net/http" "testing" + "time" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" ) @@ -33,26 +34,54 @@ func (suite *GraphUnitSuite) SetupSuite() { func (suite *GraphUnitSuite) TestCreateAdapter() { t := suite.T() - adpt, err := graph.CreateAdapter( + adpt, err := CreateAdapter( suite.credentials.AzureTenantID, suite.credentials.AzureClientID, - suite.credentials.AzureClientSecret, - ) + suite.credentials.AzureClientSecret) assert.NoError(t, err) assert.NotNil(t, adpt) } +func (suite *GraphUnitSuite) TestHTTPClient() { + table := []struct { + name string + opts []option + check func(*testing.T, *http.Client) + }{ + { + name: "no options", + opts: []option{}, + check: func(t *testing.T, c *http.Client) { + assert.Equal(t, 90*time.Second, c.Timeout, "default timeout") + }, + }, + { + name: "no timeout", + opts: []option{NoTimeout()}, + check: func(t *testing.T, c *http.Client) { + assert.Equal(t, 0, int(c.Timeout), "unlimited timeout") + }, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + cli := HTTPClient(test.opts...) + assert.NotNil(t, cli) + test.check(t, cli) + }) + } +} + func (suite *GraphUnitSuite) TestSerializationEndPoint() { t := suite.T() - adpt, err := graph.CreateAdapter( + adpt, err := CreateAdapter( suite.credentials.AzureTenantID, suite.credentials.AzureClientID, - suite.credentials.AzureClientSecret, - ) + suite.credentials.AzureClientSecret) require.NoError(t, err) - serv := graph.NewService(adpt) + serv := NewService(adpt) email := models.NewMessage() subject := "TestSerializationEndPoint" email.SetSubject(&subject) diff --git a/src/internal/connector/graph_connector_disconnected_test.go b/src/internal/connector/graph_connector_disconnected_test.go index 711e55ff8..2f17ae026 100644 --- a/src/internal/connector/graph_connector_disconnected_test.go +++ b/src/internal/connector/graph_connector_disconnected_test.go @@ -66,7 +66,7 @@ func (suite *DisconnectedGraphConnectorSuite) TestBadConnection() { for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - gc, err := NewGraphConnector(ctx, graph.LargeItemClient(), test.acct(t), Users) + gc, err := NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), test.acct(t), Users) assert.Nil(t, gc, test.name+" failed") assert.NotNil(t, err, test.name+"failed") }) diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index 85ad3f45f..be1439c35 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -156,7 +156,7 @@ func (suite *GraphConnectorIntegrationSuite) SetupSuite() { tester.MustGetEnvSets(suite.T(), tester.M365AcctCredEnvs) - suite.connector = loadConnector(ctx, suite.T(), graph.LargeItemClient(), Users) + suite.connector = loadConnector(ctx, suite.T(), graph.HTTPClient(graph.NoTimeout()), Users) suite.user = tester.M365UserID(suite.T()) suite.acct = tester.NewM365Account(suite.T()) @@ -375,12 +375,11 @@ func runRestoreBackupTest( t.Logf( "Restoring collections to %s for resourceOwners(s) %v\n", dest.ContainerName, - resourceOwners, - ) + resourceOwners) start := time.Now() - restoreGC := loadConnector(ctx, t, graph.LargeItemClient(), test.resource) + restoreGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) restoreSel := getSelectorWith(t, test.service, resourceOwners, true) deets, err := restoreGC.RestoreDataCollections( ctx, @@ -394,8 +393,10 @@ func runRestoreBackupTest( status := restoreGC.AwaitStatus() runTime := time.Since(start) - assert.Equal(t, totalItems, status.ObjectCount, "status.ObjectCount") - assert.Equal(t, totalItems, status.Successful, "status.Successful") + assert.NoError(t, status.Err, "restored status.Err") + assert.Zero(t, status.ErrorCount, "restored status.ErrorCount") + assert.Equal(t, totalItems, status.ObjectCount, "restored status.ObjectCount") + assert.Equal(t, totalItems, status.Successful, "restored status.Successful") assert.Len( t, deets.Entries, @@ -419,13 +420,15 @@ func runRestoreBackupTest( }) } - backupGC := loadConnector(ctx, t, graph.LargeItemClient(), test.resource) + backupGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) backupSel := backupSelectorForExpected(t, test.service, expectedDests) t.Logf("Selective backup of %s\n", backupSel) start = time.Now() - dcs, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{}) + dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{}) require.NoError(t, err) + // No excludes yet because this isn't an incremental backup. + assert.Empty(t, excludes) t.Logf("Backup enumeration complete in %v\n", time.Since(start)) @@ -434,8 +437,13 @@ func runRestoreBackupTest( skipped := checkCollections(t, totalItems, expectedData, dcs) status = backupGC.AwaitStatus() - assert.Equal(t, totalItems+skipped, status.ObjectCount, "status.ObjectCount") - assert.Equal(t, totalItems+skipped, status.Successful, "status.Successful") + + assert.NoError(t, status.Err, "backup status.Err") + assert.Zero(t, status.ErrorCount, "backup status.ErrorCount") + assert.Equalf(t, totalItems+skipped, status.ObjectCount, + "backup status.ObjectCount; wanted %d items + %d skipped", totalItems, skipped) + assert.Equalf(t, totalItems+skipped, status.Successful, + "backup status.Successful; wanted %d items + %d skipped", totalItems, skipped) } func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup() { @@ -870,7 +878,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames dest.ContainerName, ) - restoreGC := loadConnector(ctx, t, graph.LargeItemClient(), test.resource) + restoreGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) deets, err := restoreGC.RestoreDataCollections(ctx, suite.acct, restoreSel, dest, collections) require.NoError(t, err) require.NotNil(t, deets) @@ -888,12 +896,14 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames // Run a backup and compare its output with what we put in. - backupGC := loadConnector(ctx, t, graph.LargeItemClient(), test.resource) + backupGC := loadConnector(ctx, t, graph.HTTPClient(graph.NoTimeout()), test.resource) backupSel := backupSelectorForExpected(t, test.service, expectedDests) t.Log("Selective backup of", backupSel) - dcs, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{}) + dcs, excludes, err := backupGC.DataCollections(ctx, backupSel, nil, control.Options{}) require.NoError(t, err) + // No excludes yet because this isn't an incremental backup. + assert.Empty(t, excludes) t.Log("Backup enumeration complete") @@ -907,3 +917,30 @@ func (suite *GraphConnectorIntegrationSuite) TestMultiFolderBackupDifferentNames }) } } + +// TODO: this should only be run during smoke tests, not part of the standard CI. +// That's why it's set aside instead of being included in the other test set. +func (suite *GraphConnectorIntegrationSuite) TestRestoreAndBackup_largeMailAttachment() { + subjectText := "Test message for restore with large attachment" + + test := restoreBackupInfo{ + name: "EmailsWithLargeAttachments", + service: path.ExchangeService, + resource: Users, + collections: []colInfo{ + { + pathElements: []string{"Inbox"}, + category: path.EmailCategory, + items: []itemInfo{ + { + name: "35mbAttachment", + data: mockconnector.GetMockMessageWithSizedAttachment(subjectText, 35), + lookupKey: subjectText, + }, + }, + }, + }, + } + + runRestoreBackupTest(suite.T(), suite.acct, test, suite.connector.tenant, []string{suite.user}) +} diff --git a/src/internal/connector/mockconnector/mock_data_message.go b/src/internal/connector/mockconnector/mock_data_message.go index 4c63806c9..597447492 100644 --- a/src/internal/connector/mockconnector/mock_data_message.go +++ b/src/internal/connector/mockconnector/mock_data_message.go @@ -155,6 +155,41 @@ func GetMockMessageWith( return []byte(message) } +// GetMockMessageWithDirectAttachment returns a message an attachment that contains n MB of data. +// Max limit on N is 35 (imposed by exchange) . +// Serialized with: kiota-serialization-json-go v0.7.1 +func GetMockMessageWithSizedAttachment(subject string, n int) []byte { + // I know we said 35, but after base64encoding, 24mb of base content + // bloats up to 34mb (35 baloons to 49). So we have to restrict n + // appropriately. + if n > 24 { + n = 24 + } + + //nolint:lll + messageFmt := "{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAADSEBNbUIB9RL6ePDeF3FIYAAB4moqeAAA=\"," + + "\"@odata.type\":\"#microsoft.graph.message\",\"@odata.etag\":\"W/\\\"CQAAABYAAADSEBNbUIB9RL6ePDeF3FIYAAB3maFQ\\\"\",\"@odata.context\":\"https://graph.microsoft.com/v1.0/$metadata#users('a4a472f8-ccb0-43ec-bf52-3697a91b926c')/messages/$entity\",\"categories\":[]," + + "\"changeKey\":\"CQAAABYAAADSEBNbUIB9RL6ePDeF3FIYAAB3maFQ\",\"createdDateTime\":\"2022-09-29T17:39:06Z\",\"lastModifiedDateTime\":\"2022-09-29T17:39:08Z\"," + + "\"attachments\":[{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAADSEBNbUIB9RL6ePDeF3FIYAAB4moqeAAABEgAQANMmZLFhjWJJj4X9mj8piqg=\",\"@odata.type\":\"#microsoft.graph.fileAttachment\",\"@odata.mediaContentType\":\"application/octet-stream\"," + + "\"contentType\":\"application/octet-stream\",\"isInline\":false,\"lastModifiedDateTime\":\"2022-09-29T17:39:06Z\",\"name\":\"database.db\",\"size\":%d," + + "\"contentBytes\":\"%s\"}]," + + "\"bccRecipients\":[],\"body\":{\"content\":\"\\r\\n
Lidia,

I hope this message finds you well. I am researching a database construct for next quarter's review. SkyNet will not be able to match our database process speeds if we utilize the formulae that are included. 

Please give me your thoughts on the implementation.

Best,

Dustin
\",\"contentType\":\"html\",\"@odata.type\":\"#microsoft.graph.itemBody\"}," + + "\"bodyPreview\":\"Lidia,\\r\\n\\r\\nI hope this message finds you well. I am researching a database construct for next quarter's review. SkyNet will not be able to match our database process speeds if we utilize the formulae that are included.\\r\\n\\r\\nPlease give me your thoughts on th\",\"ccRecipients\":[]," + + "\"conversationId\":\"AAQkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAQANPFOcy_BapBghezTzIIldI=\",\"conversationIndex\":\"AQHY1Cpb08U5zL4FqkGCF7NPMgiV0g==\",\"flag\":{\"flagStatus\":\"notFlagged\",\"@odata.type\":\"#microsoft.graph.followupFlag\"}," + + "\"from\":{\"emailAddress\":{\"address\":\"dustina@8qzvrj.onmicrosoft.com\",\"name\":\"Dustin Abbot\",\"@odata.type\":\"#microsoft.graph.emailAddress\"},\"@odata.type\":\"#microsoft.graph.recipient\"},\"hasAttachments\":true,\"importance\":\"normal\",\"inferenceClassification\":\"focused\"," + + "\"internetMessageId\":\"\",\"isDeliveryReceiptRequested\":false,\"isDraft\":false,\"isRead\":false,\"isReadReceiptRequested\":false," + + "\"parentFolderId\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAAA=\",\"receivedDateTime\":\"2022-09-29T17:39:07Z\",\"replyTo\":[],\"sender\":{\"emailAddress\":{\"address\":\"dustina@8qzvrj.onmicrosoft.com\",\"name\":\"Dustin Abbot\"," + + "\"@odata.type\":\"#microsoft.graph.emailAddress\"},\"@odata.type\":\"#microsoft.graph.recipient\"},\"sentDateTime\":\"2022-09-29T17:39:02Z\"," + + "\"subject\":\"" + subject + "\",\"toRecipients\":[{\"emailAddress\":{\"address\":\"LidiaH@8qzvrj.onmicrosoft.com\",\"name\":\"Lidia Holloway\",\"@odata.type\":\"#microsoft.graph.emailAddress\"},\"@odata.type\":\"#microsoft.graph.recipient\"}]," + + "\"webLink\":\"https://outlook.office365.com/owa/?ItemID=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAADSEBNbUIB9RL6ePDeF3FIYAAB4moqeAAA%3D&exvsurl=1&viewmodel=ReadMessageItem\"}" + + attachmentSize := n * 1024 * 1024 // n MB + attachmentBytes := make([]byte, attachmentSize) + + // Attachment content bytes are base64 encoded + return []byte(fmt.Sprintf(messageFmt, attachmentSize, base64.StdEncoding.EncodeToString([]byte(attachmentBytes)))) +} + // GetMockMessageWithDirectAttachment returns a message with inline attachment // Serialized with: kiota-serialization-json-go v0.7.1 func GetMockMessageWithDirectAttachment(subject string) []byte { @@ -228,28 +263,7 @@ func GetMockMessageWithDirectAttachment(subject string) []byte { // used in GetMockMessageWithDirectAttachment // Serialized with: kiota-serialization-json-go v0.7.1 func GetMockMessageWithLargeAttachment(subject string) []byte { - //nolint:lll - messageFmt := "{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAADSEBNbUIB9RL6ePDeF3FIYAAB4moqeAAA=\"," + - "\"@odata.type\":\"#microsoft.graph.message\",\"@odata.etag\":\"W/\\\"CQAAABYAAADSEBNbUIB9RL6ePDeF3FIYAAB3maFQ\\\"\",\"@odata.context\":\"https://graph.microsoft.com/v1.0/$metadata#users('a4a472f8-ccb0-43ec-bf52-3697a91b926c')/messages/$entity\",\"categories\":[]," + - "\"changeKey\":\"CQAAABYAAADSEBNbUIB9RL6ePDeF3FIYAAB3maFQ\",\"createdDateTime\":\"2022-09-29T17:39:06Z\",\"lastModifiedDateTime\":\"2022-09-29T17:39:08Z\"," + - "\"attachments\":[{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAADSEBNbUIB9RL6ePDeF3FIYAAB4moqeAAABEgAQANMmZLFhjWJJj4X9mj8piqg=\",\"@odata.type\":\"#microsoft.graph.fileAttachment\",\"@odata.mediaContentType\":\"application/octet-stream\"," + - "\"contentType\":\"application/octet-stream\",\"isInline\":false,\"lastModifiedDateTime\":\"2022-09-29T17:39:06Z\",\"name\":\"database.db\",\"size\":%d," + - "\"contentBytes\":\"%s\"}]," + - "\"bccRecipients\":[],\"body\":{\"content\":\"\\r\\n
Lidia,

I hope this message finds you well. I am researching a database construct for next quarter's review. SkyNet will not be able to match our database process speeds if we utilize the formulae that are included. 

Please give me your thoughts on the implementation.

Best,

Dustin
\",\"contentType\":\"html\",\"@odata.type\":\"#microsoft.graph.itemBody\"}," + - "\"bodyPreview\":\"Lidia,\\r\\n\\r\\nI hope this message finds you well. I am researching a database construct for next quarter's review. SkyNet will not be able to match our database process speeds if we utilize the formulae that are included.\\r\\n\\r\\nPlease give me your thoughts on th\",\"ccRecipients\":[]," + - "\"conversationId\":\"AAQkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAQANPFOcy_BapBghezTzIIldI=\",\"conversationIndex\":\"AQHY1Cpb08U5zL4FqkGCF7NPMgiV0g==\",\"flag\":{\"flagStatus\":\"notFlagged\",\"@odata.type\":\"#microsoft.graph.followupFlag\"}," + - "\"from\":{\"emailAddress\":{\"address\":\"dustina@8qzvrj.onmicrosoft.com\",\"name\":\"Dustin Abbot\",\"@odata.type\":\"#microsoft.graph.emailAddress\"},\"@odata.type\":\"#microsoft.graph.recipient\"},\"hasAttachments\":true,\"importance\":\"normal\",\"inferenceClassification\":\"focused\"," + - "\"internetMessageId\":\"\",\"isDeliveryReceiptRequested\":false,\"isDraft\":false,\"isRead\":false,\"isReadReceiptRequested\":false," + - "\"parentFolderId\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAAA=\",\"receivedDateTime\":\"2022-09-29T17:39:07Z\",\"replyTo\":[],\"sender\":{\"emailAddress\":{\"address\":\"dustina@8qzvrj.onmicrosoft.com\",\"name\":\"Dustin Abbot\"," + - "\"@odata.type\":\"#microsoft.graph.emailAddress\"},\"@odata.type\":\"#microsoft.graph.recipient\"},\"sentDateTime\":\"2022-09-29T17:39:02Z\"," + - "\"subject\":\"" + subject + "\",\"toRecipients\":[{\"emailAddress\":{\"address\":\"LidiaH@8qzvrj.onmicrosoft.com\",\"name\":\"Lidia Holloway\",\"@odata.type\":\"#microsoft.graph.emailAddress\"},\"@odata.type\":\"#microsoft.graph.recipient\"}]," + - "\"webLink\":\"https://outlook.office365.com/owa/?ItemID=AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAEMAADSEBNbUIB9RL6ePDeF3FIYAAB4moqeAAA%3D&exvsurl=1&viewmodel=ReadMessageItem\"}" - - attachmentSize := 3 * 1024 * 1024 // 3 MB - attachmentBytes := make([]byte, attachmentSize) - - // Attachment content bytes are base64 encoded - return []byte(fmt.Sprintf(messageFmt, attachmentSize, base64.StdEncoding.EncodeToString([]byte(attachmentBytes)))) + return GetMockMessageWithSizedAttachment(subject, 3) } // GetMessageWithOneDriveAttachment returns a message with an OneDrive attachment represented in bytes diff --git a/src/internal/connector/onedrive/api/drive.go b/src/internal/connector/onedrive/api/drive.go new file mode 100644 index 000000000..6dd7d46a1 --- /dev/null +++ b/src/internal/connector/onedrive/api/drive.go @@ -0,0 +1,105 @@ +package api + +import ( + "context" + + "github.com/microsoftgraph/msgraph-sdk-go/models" + mssites "github.com/microsoftgraph/msgraph-sdk-go/sites" + msusers "github.com/microsoftgraph/msgraph-sdk-go/users" + "github.com/pkg/errors" + + "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/api" +) + +type userDrivePager struct { + gs graph.Servicer + builder *msusers.ItemDrivesRequestBuilder + options *msusers.ItemDrivesRequestBuilderGetRequestConfiguration +} + +func NewUserDrivePager( + gs graph.Servicer, + userID string, + fields []string, +) *userDrivePager { + requestConfig := &msusers.ItemDrivesRequestBuilderGetRequestConfiguration{ + QueryParameters: &msusers.ItemDrivesRequestBuilderGetQueryParameters{ + Select: fields, + }, + } + + res := &userDrivePager{ + gs: gs, + options: requestConfig, + builder: gs.Client().UsersById(userID).Drives(), + } + + return res +} + +func (p *userDrivePager) GetPage(ctx context.Context) (api.PageLinker, error) { + return p.builder.Get(ctx, p.options) +} + +func (p *userDrivePager) SetNext(link string) { + p.builder = msusers.NewItemDrivesRequestBuilder(link, p.gs.Adapter()) +} + +func (p *userDrivePager) ValuesIn(l api.PageLinker) ([]models.Driveable, error) { + page, ok := l.(interface{ GetValue() []models.Driveable }) + if !ok { + return nil, errors.Errorf( + "response of type [%T] does not comply with GetValue() interface", + l, + ) + } + + return page.GetValue(), nil +} + +type siteDrivePager struct { + gs graph.Servicer + builder *mssites.ItemDrivesRequestBuilder + options *mssites.ItemDrivesRequestBuilderGetRequestConfiguration +} + +func NewSiteDrivePager( + gs graph.Servicer, + siteID string, + fields []string, +) *siteDrivePager { + requestConfig := &mssites.ItemDrivesRequestBuilderGetRequestConfiguration{ + QueryParameters: &mssites.ItemDrivesRequestBuilderGetQueryParameters{ + Select: fields, + }, + } + + res := &siteDrivePager{ + gs: gs, + options: requestConfig, + builder: gs.Client().SitesById(siteID).Drives(), + } + + return res +} + +func (p *siteDrivePager) GetPage(ctx context.Context) (api.PageLinker, error) { + return p.builder.Get(ctx, p.options) +} + +func (p *siteDrivePager) SetNext(link string) { + p.builder = mssites.NewItemDrivesRequestBuilder(link, p.gs.Adapter()) +} + +func (p *siteDrivePager) ValuesIn(l api.PageLinker) ([]models.Driveable, error) { + page, ok := l.(interface{ GetValue() []models.Driveable }) + if !ok { + return nil, errors.Errorf( + "response of type [%T] does not comply with GetValue() interface", + l, + ) + } + + return page.GetValue(), nil +} diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index ac0aa9fb3..a786de0ab 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -191,7 +191,7 @@ func (oc *Collection) populateItems(ctx context.Context) { folderProgress, colCloser := observe.ProgressWithCount( ctx, observe.ItemQueueMsg, - "/"+parentPathString, + observe.PII("/"+parentPathString), int64(len(oc.driveItems))) defer colCloser() defer close(folderProgress) @@ -223,52 +223,89 @@ func (oc *Collection) populateItems(ctx context.Context) { defer wg.Done() defer func() { <-semaphoreCh }() - // Read the item var ( + itemID = *item.GetId() + itemName = *item.GetName() + itemSize = *item.GetSize() itemInfo details.ItemInfo - itemData io.ReadCloser - err error - ) - - for i := 1; i <= maxRetries; i++ { - itemInfo, itemData, err = oc.itemReader(oc.itemClient, item) - if err == nil || graph.IsErrTimeout(err) == nil { - // retry on Timeout type errors, break otherwise. - break - } - - if i < maxRetries { - time.Sleep(1 * time.Second) - } - } - - if err != nil { - errUpdater(*item.GetId(), err) - return - } - - var ( - itemName string - itemSize int64 ) switch oc.source { case SharePointSource: + itemInfo.SharePoint = sharePointItemInfo(item, itemSize) itemInfo.SharePoint.ParentPath = parentPathString - itemName = itemInfo.SharePoint.ItemName - itemSize = itemInfo.SharePoint.Size default: + itemInfo.OneDrive = oneDriveItemInfo(item, itemSize) itemInfo.OneDrive.ParentPath = parentPathString - itemName = itemInfo.OneDrive.ItemName - itemSize = itemInfo.OneDrive.Size } + // Construct a new lazy readCloser to feed to the collection consumer. + // This ensures that downloads won't be attempted unless that consumer + // attempts to read bytes. Assumption is that kopia will check things + // like file modtimes before attempting to read. itemReader := lazy.NewLazyReadCloser(func() (io.ReadCloser, error) { - progReader, closer := observe.ItemProgress(ctx, itemData, observe.ItemBackupMsg, itemName, itemSize) + // Read the item + var ( + itemData io.ReadCloser + err error + ) + + for i := 1; i <= maxRetries; i++ { + _, itemData, err = oc.itemReader(oc.itemClient, item) + if err == nil { + break + } + + if graph.IsErrUnauthorized(err) { + // assume unauthorized requests are a sign of an expired + // jwt token, and that we've overrun the available window + // to download the actual file. Re-downloading the item + // will refresh that download url. + di, diErr := getDriveItem(ctx, oc.service, oc.driveID, itemID) + if diErr != nil { + err = errors.Wrap(diErr, "retrieving expired item") + break + } + + item = di + + continue + + } else if !graph.IsErrTimeout(err) && !graph.IsErrThrottled(err) && !graph.IsSericeUnavailable(err) { + // TODO: graphAPI will provides headers that state the duration to wait + // in order to succeed again. The one second sleep won't cut it here. + // + // for all non-timeout, non-unauth, non-throttling errors, do not retry + break + } + + if i < maxRetries { + time.Sleep(1 * time.Second) + } + } + + // check for errors following retries + if err != nil { + errUpdater(itemID, err) + return nil, err + } + + // display/log the item download + progReader, closer := observe.ItemProgress(ctx, itemData, observe.ItemBackupMsg, observe.PII(itemName), itemSize) go closer() + return progReader, nil }) + // This can cause inaccurate counts. Right now it counts all the items + // we intend to read. Errors within the lazy readCloser will create a + // conflict: an item is both successful and erroneous. But the async + // control to fix that is more error-prone than helpful. + // + // TODO: transform this into a stats bus so that async control of stats + // aggregation is handled at the backup level, not at the item iteration + // level. + // // Item read successfully, add to collection atomic.AddInt64(&itemsRead, 1) // byteCount iteration diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index a36db58c9..b608e9068 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -62,17 +62,25 @@ func (suite *CollectionUnitTestSuite) TestCollection() { now = time.Now() ) + type nst struct { + name string + size int64 + time time.Time + } + table := []struct { name string numInstances int source driveSource itemReader itemReaderFunc + itemDeets nst infoFrom func(*testing.T, details.ItemInfo) (string, string) }{ { name: "oneDrive, no duplicates", numInstances: 1, source: OneDriveSource, + itemDeets: nst{testItemName, 42, now}, itemReader: func(*http.Client, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), @@ -87,6 +95,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { name: "oneDrive, duplicates", numInstances: 3, source: OneDriveSource, + itemDeets: nst{testItemName, 42, now}, itemReader: func(*http.Client, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), @@ -101,6 +110,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { name: "sharePoint, no duplicates", numInstances: 1, source: SharePointSource, + itemDeets: nst{testItemName, 42, now}, itemReader: func(*http.Client, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), @@ -115,6 +125,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { name: "sharePoint, duplicates", numInstances: 3, source: SharePointSource, + itemDeets: nst{testItemName, 42, now}, itemReader: func(*http.Client, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), @@ -140,7 +151,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { require.NoError(t, err) coll := NewCollection( - graph.LargeItemClient(), + graph.HTTPClient(graph.NoTimeout()), folderPath, "drive-id", suite, @@ -153,6 +164,10 @@ func (suite *CollectionUnitTestSuite) TestCollection() { // Set a item reader, add an item and validate we get the item back mockItem := models.NewDriveItem() mockItem.SetId(&testItemID) + mockItem.SetName(&test.itemDeets.name) + mockItem.SetSize(&test.itemDeets.size) + mockItem.SetCreatedDateTime(&test.itemDeets.time) + mockItem.SetLastModifiedDateTime(&test.itemDeets.time) for i := 0; i < test.numInstances; i++ { coll.Add(mockItem) @@ -169,27 +184,26 @@ func (suite *CollectionUnitTestSuite) TestCollection() { wg.Wait() - // Expect only 1 item - require.Len(t, readItems, 1) - require.Equal(t, 1, collStatus.ObjectCount) - require.Equal(t, 1, collStatus.Successful) - // Validate item info and data readItem := readItems[0] readItemInfo := readItem.(data.StreamInfo) + readData, err := io.ReadAll(readItem.ToReader()) + require.NoError(t, err) + assert.Equal(t, testItemData, readData) + + // Expect only 1 item + require.Len(t, readItems, 1) + require.Equal(t, 1, collStatus.ObjectCount, "items iterated") + require.Equal(t, 1, collStatus.Successful, "items successful") + assert.Equal(t, testItemName, readItem.UUID()) require.Implements(t, (*data.StreamModTime)(nil), readItem) mt := readItem.(data.StreamModTime) assert.Equal(t, now, mt.ModTime()) - readData, err := io.ReadAll(readItem.ToReader()) - require.NoError(t, err) - name, parentPath := test.infoFrom(t, readItemInfo.Info()) - - assert.Equal(t, testItemData, readData) assert.Equal(t, testItemName, name) assert.Equal(t, driveFolderPath, parentPath) }) @@ -197,6 +211,12 @@ func (suite *CollectionUnitTestSuite) TestCollection() { } func (suite *CollectionUnitTestSuite) TestCollectionReadError() { + var ( + name = "name" + size int64 = 42 + now = time.Now() + ) + table := []struct { name string source driveSource @@ -225,7 +245,7 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() { require.NoError(t, err) coll := NewCollection( - graph.LargeItemClient(), + graph.HTTPClient(graph.NoTimeout()), folderPath, "fakeDriveID", suite, @@ -235,18 +255,27 @@ func (suite *CollectionUnitTestSuite) TestCollectionReadError() { mockItem := models.NewDriveItem() mockItem.SetId(&testItemID) + mockItem.SetName(&name) + mockItem.SetSize(&size) + mockItem.SetCreatedDateTime(&now) + mockItem.SetLastModifiedDateTime(&now) coll.Add(mockItem) coll.itemReader = func(*http.Client, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{}, nil, assert.AnError } - coll.Items() + collItem, ok := <-coll.Items() + assert.True(t, ok) + + _, err = io.ReadAll(collItem.ToReader()) + assert.Error(t, err) + wg.Wait() // Expect no items - require.Equal(t, 1, collStatus.ObjectCount) - require.Equal(t, 0, collStatus.Successful) + require.Equal(t, 1, collStatus.ObjectCount, "only one object should be counted") + require.Equal(t, 1, collStatus.Successful, "TODO: should be 0, but allowing 1 to reduce async management") }) } } diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index f446aa246..f83ce342a 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -92,12 +92,20 @@ func NewCollections( } } -// Retrieves drive data as set of `data.Collections` -func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { +// Retrieves drive data as set of `data.Collections` and a set of item names to +// be excluded from the upcoming backup. +func (c *Collections) Get(ctx context.Context) ([]data.Collection, map[string]struct{}, error) { // Enumerate drives for the specified resourceOwner - drives, err := drives(ctx, c.service, c.resourceOwner, c.source) + pager, err := PagerForSource(c.source, c.service, c.resourceOwner, nil) if err != nil { - return nil, err + return nil, nil, err + } + + retry := c.source == OneDriveSource + + drives, err := drives(ctx, pager, retry) + if err != nil { + return nil, nil, err } var ( @@ -126,7 +134,7 @@ func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { c.UpdateCollections, ) if err != nil { - return nil, err + return nil, nil, err } if len(delta) > 0 { @@ -144,7 +152,7 @@ func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { maps.Copy(excludedItems, excluded) } - observe.Message(ctx, fmt.Sprintf("Discovered %d items to backup", c.NumItems)) + observe.Message(ctx, observe.Safe(fmt.Sprintf("Discovered %d items to backup", c.NumItems))) // Add an extra for the metadata collection. collections := make([]data.Collection, 0, len(c.CollectionMap)+1) @@ -178,7 +186,8 @@ func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { collections = append(collections, metadata) } - return collections, nil + // TODO(ashmrtn): Track and return the set of items to exclude. + return collections, nil, nil } // UpdateCollections initializes and adds the provided drive items to Collections diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index 21ca061a9..b69253918 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -588,7 +588,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { outputFolderMap := map[string]string{} maps.Copy(outputFolderMap, tt.inputFolderMap) c := NewCollections( - graph.LargeItemClient(), + graph.HTTPClient(graph.NoTimeout()), tenant, user, OneDriveSource, diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index c765e3719..6270ec08a 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -10,11 +10,12 @@ import ( msdrives "github.com/microsoftgraph/msgraph-sdk-go/drives" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" - "github.com/microsoftgraph/msgraph-sdk-go/sites" "github.com/pkg/errors" "golang.org/x/exp/maps" "github.com/alcionai/corso/src/internal/connector/graph" + gapi "github.com/alcionai/corso/src/internal/connector/graph/api" + "github.com/alcionai/corso/src/internal/connector/onedrive/api" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/pkg/logger" ) @@ -22,86 +23,106 @@ import ( var errFolderNotFound = errors.New("folder not found") const ( + getDrivesRetries = 3 + // nextLinkKey is used to find the next link in a paged // graph response - nextLinkKey = "@odata.nextLink" - itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children" - itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s" - itemNotFoundErrorCode = "itemNotFound" - userMysiteURLNotFound = "BadRequest Unable to retrieve user's mysite URL" - userMysiteNotFound = "ResourceNotFound User's mysite not found" + nextLinkKey = "@odata.nextLink" + itemChildrenRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s/children" + itemByPathRawURLFmt = "https://graph.microsoft.com/v1.0/drives/%s/items/%s:/%s" + itemNotFoundErrorCode = "itemNotFound" + userMysiteURLNotFound = "BadRequest Unable to retrieve user's mysite URL" + userMysiteNotFound = "ResourceNotFound User's mysite not found" + contextDeadlineExceeded = "context deadline exceeded" ) -// Enumerates the drives for the specified user -func drives( - ctx context.Context, - service graph.Servicer, - resourceOwner string, +type drivePager interface { + GetPage(context.Context) (gapi.PageLinker, error) + SetNext(nextLink string) + ValuesIn(gapi.PageLinker) ([]models.Driveable, error) +} + +func PagerForSource( source driveSource, -) ([]models.Driveable, error) { + servicer graph.Servicer, + resourceOwner string, + fields []string, +) (drivePager, error) { switch source { case OneDriveSource: - return userDrives(ctx, service, resourceOwner) + return api.NewUserDrivePager(servicer, resourceOwner, fields), nil case SharePointSource: - return siteDrives(ctx, service, resourceOwner) + return api.NewSiteDrivePager(servicer, resourceOwner, fields), nil default: return nil, errors.Errorf("unrecognized drive data source") } } -func siteDrives(ctx context.Context, service graph.Servicer, site string) ([]models.Driveable, error) { - options := &sites.ItemDrivesRequestBuilderGetRequestConfiguration{ - QueryParameters: &sites.ItemDrivesRequestBuilderGetQueryParameters{ - Select: []string{"id", "name", "weburl", "system"}, - }, - } - - r, err := service.Client().SitesById(site).Drives().Get(ctx, options) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve site drives. site: %s, details: %s", - site, support.ConnectorStackErrorTrace(err)) - } - - return r.GetValue(), nil -} - -func userDrives(ctx context.Context, service graph.Servicer, user string) ([]models.Driveable, error) { +func drives( + ctx context.Context, + pager drivePager, + retry bool, +) ([]models.Driveable, error) { var ( - numberOfRetries = 3 - r models.DriveCollectionResponseable err error + page gapi.PageLinker + numberOfRetries = getDrivesRetries + drives = []models.Driveable{} ) - // Retry Loop for Drive retrieval. Request can timeout - for i := 0; i <= numberOfRetries; i++ { - r, err = service.Client().UsersById(user).Drives().Get(ctx, nil) - if err != nil { - detailedError := support.ConnectorStackErrorTrace(err) - if strings.Contains(detailedError, userMysiteURLNotFound) || - strings.Contains(detailedError, userMysiteNotFound) { - logger.Ctx(ctx).Infof("User %s does not have a drive", user) - return make([]models.Driveable, 0), nil // no license - } - - if strings.Contains(detailedError, "context deadline exceeded") && i < numberOfRetries { - time.Sleep(time.Duration(3*(i+1)) * time.Second) - continue - } - - return nil, errors.Wrapf( - err, - "failed to retrieve user drives. user: %s, details: %s", - user, - detailedError, - ) - } - - break + if !retry { + numberOfRetries = 0 } - logger.Ctx(ctx).Debugf("Found %d drives for user %s", len(r.GetValue()), user) + // Loop through all pages returned by Graph API. + for { + // Retry Loop for Drive retrieval. Request can timeout + for i := 0; i <= numberOfRetries; i++ { + page, err = pager.GetPage(ctx) + if err != nil { + // Various error handling. May return an error or perform a retry. + detailedError := support.ConnectorStackErrorTrace(err) + if strings.Contains(detailedError, userMysiteURLNotFound) || + strings.Contains(detailedError, userMysiteNotFound) { + logger.Ctx(ctx).Infof("resource owner does not have a drive") + return make([]models.Driveable, 0), nil // no license or drives. + } - return r.GetValue(), nil + if strings.Contains(detailedError, contextDeadlineExceeded) && i < numberOfRetries { + time.Sleep(time.Duration(3*(i+1)) * time.Second) + continue + } + + return nil, errors.Wrapf( + err, + "failed to retrieve drives. details: %s", + detailedError, + ) + } + + // No error encountered, break the retry loop so we can extract results + // and see if there's another page to fetch. + break + } + + tmp, err := pager.ValuesIn(page) + if err != nil { + return nil, errors.Wrap(err, "extracting drives from response") + } + + drives = append(drives, tmp...) + + nextLink := gapi.NextLink(page) + if len(nextLink) == 0 { + break + } + + pager.SetNext(nextLink) + } + + logger.Ctx(ctx).Debugf("Found %d drives", len(drives)) + + return drives, nil } // itemCollector functions collect the items found in a drive @@ -284,10 +305,10 @@ func (op *Displayable) GetDisplayName() *string { func GetAllFolders( ctx context.Context, gs graph.Servicer, - userID string, + pager drivePager, prefix string, ) ([]*Displayable, error) { - drives, err := drives(ctx, gs, userID, OneDriveSource) + drives, err := drives(ctx, pager, true) if err != nil { return nil, errors.Wrap(err, "getting OneDrive folders") } diff --git a/src/internal/connector/onedrive/drive_test.go b/src/internal/connector/onedrive/drive_test.go index 755b7293b..36fef30ab 100644 --- a/src/internal/connector/onedrive/drive_test.go +++ b/src/internal/connector/onedrive/drive_test.go @@ -1,21 +1,323 @@ package onedrive import ( + "context" "strings" "testing" + "github.com/google/uuid" + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/graph/api" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/selectors" ) +type mockPageLinker struct { + link *string +} + +func (pl *mockPageLinker) GetOdataNextLink() *string { + return pl.link +} + +type pagerResult struct { + drives []models.Driveable + nextLink *string + err error +} + +type mockDrivePager struct { + toReturn []pagerResult + getIdx int +} + +func (p *mockDrivePager) GetPage(context.Context) (api.PageLinker, error) { + if len(p.toReturn) <= p.getIdx { + return nil, assert.AnError + } + + idx := p.getIdx + p.getIdx++ + + return &mockPageLinker{p.toReturn[idx].nextLink}, p.toReturn[idx].err +} + +func (p *mockDrivePager) SetNext(string) {} + +func (p *mockDrivePager) ValuesIn(api.PageLinker) ([]models.Driveable, error) { + idx := p.getIdx + if idx > 0 { + // Return values lag by one since we increment in GetPage(). + idx-- + } + + if len(p.toReturn) <= idx { + return nil, assert.AnError + } + + return p.toReturn[idx].drives, nil +} + +// Unit tests +type OneDriveUnitSuite struct { + suite.Suite +} + +func TestOneDriveUnitSuite(t *testing.T) { + suite.Run(t, new(OneDriveUnitSuite)) +} + +func (suite *OneDriveUnitSuite) TestDrives() { + numDriveResults := 4 + emptyLink := "" + link := "foo" + + // These errors won't be the "correct" format when compared to what graph + // returns, but they're close enough to have the same info when the inner + // details are extracted via support package. + tmp := userMysiteURLNotFound + tmpMySiteURLNotFound := odataerrors.NewMainError() + tmpMySiteURLNotFound.SetMessage(&tmp) + + mySiteURLNotFound := odataerrors.NewODataError() + mySiteURLNotFound.SetError(tmpMySiteURLNotFound) + + tmp2 := userMysiteNotFound + tmpMySiteNotFound := odataerrors.NewMainError() + tmpMySiteNotFound.SetMessage(&tmp2) + + mySiteNotFound := odataerrors.NewODataError() + mySiteNotFound.SetError(tmpMySiteNotFound) + + tmp3 := contextDeadlineExceeded + tmpDeadlineExceeded := odataerrors.NewMainError() + tmpDeadlineExceeded.SetMessage(&tmp3) + + deadlineExceeded := odataerrors.NewODataError() + deadlineExceeded.SetError(tmpDeadlineExceeded) + + resultDrives := make([]models.Driveable, 0, numDriveResults) + + for i := 0; i < numDriveResults; i++ { + d := models.NewDrive() + id := uuid.NewString() + d.SetId(&id) + + resultDrives = append(resultDrives, d) + } + + tooManyRetries := make([]pagerResult, 0, getDrivesRetries+1) + + for i := 0; i < getDrivesRetries+1; i++ { + tooManyRetries = append(tooManyRetries, pagerResult{ + err: deadlineExceeded, + }) + } + + table := []struct { + name string + pagerResults []pagerResult + retry bool + expectedErr assert.ErrorAssertionFunc + expectedResults []models.Driveable + }{ + { + name: "AllOneResultNilNextLink", + pagerResults: []pagerResult{ + { + drives: resultDrives, + nextLink: nil, + err: nil, + }, + }, + retry: false, + expectedErr: assert.NoError, + expectedResults: resultDrives, + }, + { + name: "AllOneResultEmptyNextLink", + pagerResults: []pagerResult{ + { + drives: resultDrives, + nextLink: &emptyLink, + err: nil, + }, + }, + retry: false, + expectedErr: assert.NoError, + expectedResults: resultDrives, + }, + { + name: "SplitResultsNilNextLink", + pagerResults: []pagerResult{ + { + drives: resultDrives[:numDriveResults/2], + nextLink: &link, + err: nil, + }, + { + drives: resultDrives[numDriveResults/2:], + nextLink: nil, + err: nil, + }, + }, + retry: false, + expectedErr: assert.NoError, + expectedResults: resultDrives, + }, + { + name: "SplitResultsEmptyNextLink", + pagerResults: []pagerResult{ + { + drives: resultDrives[:numDriveResults/2], + nextLink: &link, + err: nil, + }, + { + drives: resultDrives[numDriveResults/2:], + nextLink: &emptyLink, + err: nil, + }, + }, + retry: false, + expectedErr: assert.NoError, + expectedResults: resultDrives, + }, + { + name: "NonRetryableError", + pagerResults: []pagerResult{ + { + drives: resultDrives, + nextLink: &link, + err: nil, + }, + { + drives: nil, + nextLink: nil, + err: assert.AnError, + }, + }, + retry: true, + expectedErr: assert.Error, + expectedResults: nil, + }, + { + name: "SiteURLNotFound", + pagerResults: []pagerResult{ + { + drives: nil, + nextLink: nil, + err: mySiteURLNotFound, + }, + }, + retry: true, + expectedErr: assert.NoError, + expectedResults: nil, + }, + { + name: "SiteNotFound", + pagerResults: []pagerResult{ + { + drives: nil, + nextLink: nil, + err: mySiteNotFound, + }, + }, + retry: true, + expectedErr: assert.NoError, + expectedResults: nil, + }, + { + name: "SplitResultsContextTimeoutWithRetries", + pagerResults: []pagerResult{ + { + drives: resultDrives[:numDriveResults/2], + nextLink: &link, + err: nil, + }, + { + drives: nil, + nextLink: nil, + err: deadlineExceeded, + }, + { + drives: resultDrives[numDriveResults/2:], + nextLink: &emptyLink, + err: nil, + }, + }, + retry: true, + expectedErr: assert.NoError, + expectedResults: resultDrives, + }, + { + name: "SplitResultsContextTimeoutNoRetries", + pagerResults: []pagerResult{ + { + drives: resultDrives[:numDriveResults/2], + nextLink: &link, + err: nil, + }, + { + drives: nil, + nextLink: nil, + err: deadlineExceeded, + }, + { + drives: resultDrives[numDriveResults/2:], + nextLink: &emptyLink, + err: nil, + }, + }, + retry: false, + expectedErr: assert.Error, + expectedResults: nil, + }, + { + name: "TooManyRetries", + pagerResults: append( + []pagerResult{ + { + drives: resultDrives[:numDriveResults/2], + nextLink: &link, + err: nil, + }, + }, + tooManyRetries..., + ), + retry: true, + expectedErr: assert.Error, + expectedResults: nil, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + pager := &mockDrivePager{ + toReturn: test.pagerResults, + } + + drives, err := drives(ctx, pager, test.retry) + test.expectedErr(t, err) + + assert.ElementsMatch(t, test.expectedResults, drives) + }) + } +} + +// Integration tests + type OneDriveSuite struct { suite.Suite userID string @@ -44,7 +346,10 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() { folderElements := []string{folderName1} gs := loadTestService(t) - drives, err := drives(ctx, gs, suite.userID, OneDriveSource) + pager, err := PagerForSource(OneDriveSource, gs, suite.userID, nil) + require.NoError(t, err) + + drives, err := drives(ctx, pager, true) require.NoError(t, err) require.NotEmpty(t, drives) @@ -89,7 +394,10 @@ func (suite *OneDriveSuite) TestCreateGetDeleteFolder() { for _, test := range table { suite.T().Run(test.name, func(t *testing.T) { - allFolders, err := GetAllFolders(ctx, gs, suite.userID, test.prefix) + pager, err := PagerForSource(OneDriveSource, gs, suite.userID, nil) + require.NoError(t, err) + + allFolders, err := GetAllFolders(ctx, gs, pager, test.prefix) require.NoError(t, err) foundFolderIDs := []string{} @@ -146,8 +454,8 @@ func (suite *OneDriveSuite) TestOneDriveNewCollections() { scope := selectors. NewOneDriveBackup([]string{test.user}). AllData()[0] - odcs, err := NewCollections( - graph.LargeItemClient(), + odcs, excludes, err := NewCollections( + graph.HTTPClient(graph.NoTimeout()), creds.AzureTenantID, test.user, OneDriveSource, @@ -157,6 +465,8 @@ func (suite *OneDriveSuite) TestOneDriveNewCollections() { control.Options{}, ).Get(ctx) assert.NoError(t, err) + // Don't expect excludes as this isn't an incremental backup. + assert.Empty(t, excludes) for _, entry := range odcs { assert.NotEmpty(t, entry.FullPath()) diff --git a/src/internal/connector/onedrive/item.go b/src/internal/connector/onedrive/item.go index 3e4e9e516..c4fd1b380 100644 --- a/src/internal/connector/onedrive/item.go +++ b/src/internal/connector/onedrive/item.go @@ -25,6 +25,15 @@ const ( downloadURLKey = "@microsoft.graph.downloadUrl" ) +// generic drive item getter +func getDriveItem( + ctx context.Context, + srv graph.Servicer, + driveID, itemID string, +) (models.DriveItemable, error) { + return srv.Client().DrivesById(driveID).ItemsById(itemID).Get(ctx, nil) +} + // sharePointItemReader will return a io.ReadCloser for the specified item // It crafts this by querying M365 for a download URL for the item // and using a http client to initialize a reader @@ -32,14 +41,9 @@ func sharePointItemReader( hc *http.Client, item models.DriveItemable, ) (details.ItemInfo, io.ReadCloser, error) { - url, ok := item.GetAdditionalData()[downloadURLKey].(*string) - if !ok { - return details.ItemInfo{}, nil, fmt.Errorf("failed to get url for %s", *item.GetName()) - } - - resp, err := hc.Get(*url) + resp, err := downloadItem(hc, item) if err != nil { - return details.ItemInfo{}, nil, err + return details.ItemInfo{}, nil, errors.Wrap(err, "downloading item") } dii := details.ItemInfo{ @@ -56,24 +60,9 @@ func oneDriveItemReader( hc *http.Client, item models.DriveItemable, ) (details.ItemInfo, io.ReadCloser, error) { - url, ok := item.GetAdditionalData()[downloadURLKey].(*string) - if !ok { - return details.ItemInfo{}, nil, fmt.Errorf("failed to get url for %s", *item.GetName()) - } - - req, err := http.NewRequest(http.MethodGet, *url, nil) + resp, err := downloadItem(hc, item) if err != nil { - return details.ItemInfo{}, nil, err - } - - // Decorate the traffic - //nolint:lll - // See https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online#how-to-decorate-your-http-traffic - req.Header.Set("User-Agent", "ISV|Alcion|Corso/"+version.Version) - - resp, err := hc.Do(req) - if err != nil { - return details.ItemInfo{}, nil, err + return details.ItemInfo{}, nil, errors.Wrap(err, "downloading item") } dii := details.ItemInfo{ @@ -83,6 +72,46 @@ func oneDriveItemReader( return dii, resp.Body, nil } +func downloadItem(hc *http.Client, item models.DriveItemable) (*http.Response, error) { + url, ok := item.GetAdditionalData()[downloadURLKey].(*string) + if !ok { + return nil, fmt.Errorf("extracting file url: file %s", *item.GetId()) + } + + req, err := http.NewRequest(http.MethodGet, *url, nil) + if err != nil { + return nil, errors.Wrap(err, "new request") + } + + //nolint:lll + // Decorate the traffic + // See https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online#how-to-decorate-your-http-traffic + req.Header.Set("User-Agent", "ISV|Alcion|Corso/"+version.Version) + + resp, err := hc.Do(req) + if err != nil { + return nil, err + } + + if (resp.StatusCode / 100) == 2 { + return resp, nil + } + + if resp.StatusCode == http.StatusTooManyRequests { + return resp, graph.Err429TooManyRequests + } + + if resp.StatusCode == http.StatusUnauthorized { + return resp, graph.Err401Unauthorized + } + + if resp.StatusCode == http.StatusServiceUnavailable { + return resp, graph.Err503ServiceUnavailable + } + + return resp, errors.New("non-2xx http response: " + resp.Status) +} + // oneDriveItemInfo will populate a details.OneDriveInfo struct // with properties from the drive item. ItemSize is specified // separately for restore processes because the local itemable diff --git a/src/internal/connector/onedrive/item_test.go b/src/internal/connector/onedrive/item_test.go index 2b28f0910..938748ca2 100644 --- a/src/internal/connector/onedrive/item_test.go +++ b/src/internal/connector/onedrive/item_test.go @@ -75,7 +75,10 @@ func (suite *ItemIntegrationSuite) SetupSuite() { suite.user = tester.SecondaryM365UserID(t) - odDrives, err := drives(ctx, suite, suite.user, OneDriveSource) + pager, err := PagerForSource(OneDriveSource, suite, suite.user, nil) + require.NoError(t, err) + + odDrives, err := drives(ctx, pager, true) require.NoError(t, err) // Test Requirement 1: Need a drive require.Greaterf(t, len(odDrives), 0, "user %s does not have a drive", suite.user) @@ -126,7 +129,7 @@ func (suite *ItemIntegrationSuite) TestItemReader_oneDrive() { // Read data for the file - itemInfo, itemData, err := oneDriveItemReader(graph.LargeItemClient(), driveItem) + itemInfo, itemData, err := oneDriveItemReader(graph.HTTPClient(graph.NoTimeout()), driveItem) require.NoError(suite.T(), err) require.NotNil(suite.T(), itemInfo.OneDrive) require.NotEmpty(suite.T(), itemInfo.OneDrive.ItemName) diff --git a/src/internal/connector/onedrive/restore.go b/src/internal/connector/onedrive/restore.go index 31149c7aa..00ed855b7 100644 --- a/src/internal/connector/onedrive/restore.go +++ b/src/internal/connector/onedrive/restore.go @@ -243,7 +243,7 @@ func restoreItem( } iReader := itemData.ToReader() - progReader, closer := observe.ItemProgress(ctx, iReader, observe.ItemRestoreMsg, itemName, ss.Size()) + progReader, closer := observe.ItemProgress(ctx, iReader, observe.ItemRestoreMsg, observe.PII(itemName), ss.Size()) go closer() diff --git a/src/internal/connector/sharepoint/collection.go b/src/internal/connector/sharepoint/collection.go index ff6af4132..c34d2a2d1 100644 --- a/src/internal/connector/sharepoint/collection.go +++ b/src/internal/connector/sharepoint/collection.go @@ -158,9 +158,9 @@ func (sc *Collection) populate(ctx context.Context) { // TODO: Insert correct ID for CollectionProgress colProgress, closer := observe.CollectionProgress( ctx, - "name", sc.fullPath.Category().String(), - sc.fullPath.Folder()) + observe.Safe("name"), + observe.PII(sc.fullPath.Folder())) go closer() defer func() { diff --git a/src/internal/connector/sharepoint/data_collections.go b/src/internal/connector/sharepoint/data_collections.go index 5950daede..6011c32a0 100644 --- a/src/internal/connector/sharepoint/data_collections.go +++ b/src/internal/connector/sharepoint/data_collections.go @@ -31,10 +31,10 @@ func DataCollections( serv graph.Servicer, su statusUpdater, ctrlOpts control.Options, -) ([]data.Collection, error) { +) ([]data.Collection, map[string]struct{}, error) { b, err := selector.ToSharePointBackup() if err != nil { - return nil, errors.Wrap(err, "sharePointDataCollection: parsing selector") + return nil, nil, errors.Wrap(err, "sharePointDataCollection: parsing selector") } var ( @@ -46,7 +46,8 @@ func DataCollections( for _, scope := range b.Scopes() { foldersComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf( "%s - %s", - scope.Category().PathType(), site)) + observe.Safe(scope.Category().PathType().String()), + observe.PII(site))) defer closer() defer close(foldersComplete) @@ -62,11 +63,11 @@ func DataCollections( su, ctrlOpts) if err != nil { - return nil, support.WrapAndAppend(site, err, errs) + return nil, nil, support.WrapAndAppend(site, err, errs) } case path.LibrariesCategory: - spcs, err = collectLibraries( + spcs, _, err = collectLibraries( ctx, itemClient, serv, @@ -76,7 +77,7 @@ func DataCollections( su, ctrlOpts) if err != nil { - return nil, support.WrapAndAppend(site, err, errs) + return nil, nil, support.WrapAndAppend(site, err, errs) } } @@ -84,7 +85,7 @@ func DataCollections( foldersComplete <- struct{}{} } - return collections, errs + return collections, nil, errs } func collectLists( @@ -133,7 +134,7 @@ func collectLibraries( scope selectors.SharePointScope, updater statusUpdater, ctrlOpts control.Options, -) ([]data.Collection, error) { +) ([]data.Collection, map[string]struct{}, error) { var ( collections = []data.Collection{} errs error @@ -151,12 +152,12 @@ func collectLibraries( updater.UpdateStatus, ctrlOpts) - odcs, err := colls.Get(ctx) + odcs, excludes, err := colls.Get(ctx) if err != nil { - return nil, support.WrapAndAppend(siteID, err, errs) + return nil, nil, support.WrapAndAppend(siteID, err, errs) } - return append(collections, odcs...), errs + return append(collections, odcs...), excludes, errs } type folderMatcher struct { diff --git a/src/internal/connector/sharepoint/data_collections_test.go b/src/internal/connector/sharepoint/data_collections_test.go index 4daca0877..87aaa5c84 100644 --- a/src/internal/connector/sharepoint/data_collections_test.go +++ b/src/internal/connector/sharepoint/data_collections_test.go @@ -92,7 +92,7 @@ func (suite *SharePointLibrariesSuite) TestUpdateCollections() { newPaths := map[string]string{} excluded := map[string]struct{}{} c := onedrive.NewCollections( - graph.LargeItemClient(), + graph.HTTPClient(graph.NoTimeout()), tenant, site, onedrive.SharePointSource, diff --git a/src/internal/connector/support/m365Support.go b/src/internal/connector/support/m365Support.go index 99cb95577..d7e51e513 100644 --- a/src/internal/connector/support/m365Support.go +++ b/src/internal/connector/support/m365Support.go @@ -1,8 +1,6 @@ package support import ( - "strings" - absser "github.com/microsoft/kiota-abstractions-go/serialization" js "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models" @@ -73,14 +71,3 @@ func CreateListFromBytes(bytes []byte) (models.Listable, error) { return list, nil } - -func HasAttachments(body models.ItemBodyable) bool { - if body.GetContent() == nil || body.GetContentType() == nil || - *body.GetContentType() == models.TEXT_BODYTYPE || len(*body.GetContent()) == 0 { - return false - } - - content := *body.GetContent() - - return strings.Contains(content, "src=\"cid:") -} diff --git a/src/internal/connector/support/m365Support_test.go b/src/internal/connector/support/m365Support_test.go index dedde3536..c04c74604 100644 --- a/src/internal/connector/support/m365Support_test.go +++ b/src/internal/connector/support/m365Support_test.go @@ -3,7 +3,6 @@ package support import ( "testing" - "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -160,56 +159,3 @@ func (suite *DataSupportSuite) TestCreateListFromBytes() { }) } } - -func (suite *DataSupportSuite) TestHasAttachments() { - tests := []struct { - name string - hasAttachment assert.BoolAssertionFunc - getBodyable func(t *testing.T) models.ItemBodyable - }{ - { - name: "Mock w/out attachment", - hasAttachment: assert.False, - getBodyable: func(t *testing.T) models.ItemBodyable { - byteArray := mockconnector.GetMockMessageWithBodyBytes( - "Test", - "This is testing", - "This is testing", - ) - message, err := CreateMessageFromBytes(byteArray) - require.NoError(t, err) - return message.GetBody() - }, - }, - { - name: "Mock w/ inline attachment", - hasAttachment: assert.True, - getBodyable: func(t *testing.T) models.ItemBodyable { - byteArray := mockconnector.GetMessageWithOneDriveAttachment("Test legacy") - message, err := CreateMessageFromBytes(byteArray) - require.NoError(t, err) - return message.GetBody() - }, - }, - { - name: "Edge Case", - hasAttachment: assert.True, - getBodyable: func(t *testing.T) models.ItemBodyable { - //nolint:lll - content := "\r\n
Happy New Year,

In accordance with TPS report guidelines, there have been questions about how to address our activities SharePoint Cover page. Do you believe this is the best picture? 



Let me know if this meets our culture requirements.

Warm Regards,

Dustin
" - body := models.NewItemBody() - body.SetContent(&content) - cat := models.HTML_BODYTYPE - body.SetContentType(&cat) - return body - }, - }, - } - - for _, test := range tests { - suite.T().Run(test.name, func(t *testing.T) { - found := HasAttachments(test.getBodyable(t)) - test.hasAttachment(t, found) - }) - } -} diff --git a/src/internal/kopia/upload.go b/src/internal/kopia/upload.go index 5301e6872..8ddb46978 100644 --- a/src/internal/kopia/upload.go +++ b/src/internal/kopia/upload.go @@ -3,10 +3,13 @@ package kopia import ( "bytes" "context" + "encoding/base64" "encoding/binary" + "fmt" "io" "os" "runtime/trace" + "strings" "sync" "sync/atomic" "time" @@ -204,6 +207,19 @@ func (cp *corsoProgress) FinishedHashingFile(fname string, bs int64) { // Pass the call through as well so we don't break expected functionality. defer cp.UploadProgress.FinishedHashingFile(fname, bs) + sl := strings.Split(fname, "/") + + for i := range sl { + rdt, err := base64.StdEncoding.DecodeString(sl[i]) + if err != nil { + fmt.Println("f did not decode") + } + + sl[i] = string(rdt) + } + + logger.Ctx(context.Background()).Debugw("finished hashing file", "path", sl[2:]) + atomic.AddInt64(&cp.totalBytes, bs) } diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index 13db75ac4..8c1aaeec7 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -119,6 +119,7 @@ func (w Wrapper) BackupCollections( ctx context.Context, previousSnapshots []IncrementalBase, collections []data.Collection, + globalExcludeSet map[string]struct{}, tags map[string]string, buildTreeWithBase bool, ) (*BackupStats, *details.Builder, map[string]path.Path, error) { @@ -129,10 +130,6 @@ func (w Wrapper) BackupCollections( ctx, end := D.Span(ctx, "kopia:backupCollections") defer end() - // TODO(ashmrtn): Make this a parameter when actually enabling the global - // exclude set. - var globalExcludeSet map[string]struct{} - if len(collections) == 0 && len(globalExcludeSet) == 0 { return &BackupStats{}, &details.Builder{}, nil, nil } diff --git a/src/internal/kopia/wrapper_test.go b/src/internal/kopia/wrapper_test.go index 3654d8846..54bbb4c8e 100644 --- a/src/internal/kopia/wrapper_test.go +++ b/src/internal/kopia/wrapper_test.go @@ -266,6 +266,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections() { suite.ctx, prevSnaps, collections, + nil, tags, true, ) @@ -353,6 +354,7 @@ func (suite *KopiaIntegrationSuite) TestRestoreAfterCompressionChange() { ctx, nil, []data.Collection{dc1, dc2}, + nil, tags, true, ) @@ -435,6 +437,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() { suite.ctx, nil, collections, + nil, tags, true, ) @@ -447,6 +450,22 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections_ReaderError() { assert.False(t, stats.Incomplete) // 5 file and 6 folder entries. assert.Len(t, deets.Details().Entries, 5+6) + + failedPath, err := suite.testPath2.Append(testFileName4, true) + require.NoError(t, err) + + ic := i64counter{} + + _, err = suite.w.RestoreMultipleItems( + suite.ctx, + string(stats.SnapshotID), + []path.Path{failedPath}, + &ic, + ) + // Files that had an error shouldn't make a dir entry in kopia. If they do we + // may run into kopia-assisted incrementals issues because only mod time and + // not file size is checked for StreamingFiles. + assert.ErrorIs(t, err, ErrNotFound, "errored file is restorable") } type backedupFile struct { @@ -480,6 +499,7 @@ func (suite *KopiaIntegrationSuite) TestBackupCollectionsHandlesNoCollections() nil, test.collections, nil, + nil, true, ) require.NoError(t, err) @@ -637,6 +657,7 @@ func (suite *KopiaSimpleRepoIntegrationSuite) SetupTest() { suite.ctx, nil, collections, + nil, tags, false, ) @@ -665,6 +686,136 @@ func (c *i64counter) Count(i int64) { c.i += i } +func (suite *KopiaSimpleRepoIntegrationSuite) TestBackupExcludeItem() { + reason := Reason{ + ResourceOwner: testUser, + Service: path.ExchangeService, + Category: path.EmailCategory, + } + + subtreePathTmp, err := path.Builder{}.Append("tmp").ToDataLayerExchangePathForCategory( + testTenant, + testUser, + path.EmailCategory, + false, + ) + require.NoError(suite.T(), err) + + subtreePath := subtreePathTmp.ToBuilder().Dir() + + manifests, err := suite.w.FetchPrevSnapshotManifests( + suite.ctx, + []Reason{reason}, + nil, + ) + require.NoError(suite.T(), err) + require.Len(suite.T(), manifests, 1) + require.Equal(suite.T(), suite.snapshotID, manifests[0].ID) + + tags := map[string]string{} + + for _, k := range reason.TagKeys() { + tags[k] = "" + } + + table := []struct { + name string + excludeItem bool + expectedCachedItems int + expectedUncachedItems int + cols func() []data.Collection + backupIDCheck require.ValueAssertionFunc + restoreCheck assert.ErrorAssertionFunc + }{ + { + name: "ExcludeItem", + excludeItem: true, + expectedCachedItems: len(suite.filesByPath) - 1, + expectedUncachedItems: 0, + cols: func() []data.Collection { + return nil + }, + backupIDCheck: require.NotEmpty, + restoreCheck: assert.Error, + }, + { + name: "NoExcludeItemNoChanges", + // No snapshot should be made since there were no changes. + expectedCachedItems: 0, + expectedUncachedItems: 0, + cols: func() []data.Collection { + return nil + }, + // Backup doesn't run. + backupIDCheck: require.Empty, + }, + { + name: "NoExcludeItemWithChanges", + expectedCachedItems: len(suite.filesByPath), + expectedUncachedItems: 1, + cols: func() []data.Collection { + c := mockconnector.NewMockExchangeCollection( + suite.testPath1, + 1, + ) + c.ColState = data.NotMovedState + + return []data.Collection{c} + }, + backupIDCheck: require.NotEmpty, + restoreCheck: assert.NoError, + }, + } + + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + var excluded map[string]struct{} + if test.excludeItem { + excluded = map[string]struct{}{ + suite.files[suite.testPath1.String()][0].itemPath.Item(): {}, + } + } + + stats, _, _, err := suite.w.BackupCollections( + suite.ctx, + []IncrementalBase{ + { + Manifest: manifests[0].Manifest, + SubtreePaths: []*path.Builder{ + subtreePath, + }, + }, + }, + test.cols(), + excluded, + tags, + true, + ) + require.NoError(t, err) + assert.Equal(t, test.expectedCachedItems, stats.CachedFileCount) + assert.Equal(t, test.expectedUncachedItems, stats.UncachedFileCount) + + test.backupIDCheck(t, stats.SnapshotID) + + if len(stats.SnapshotID) == 0 { + return + } + + ic := i64counter{} + + _, err = suite.w.RestoreMultipleItems( + suite.ctx, + string(stats.SnapshotID), + []path.Path{ + suite.files[suite.testPath1.String()][0].itemPath, + }, + &ic, + ) + test.restoreCheck(t, err) + }) + } +} + func (suite *KopiaSimpleRepoIntegrationSuite) TestRestoreMultipleItems() { doesntExist, err := path.Builder{}.Append("subdir", "foo").ToDataLayerExchangePathForCategory( testTenant, diff --git a/src/internal/observe/observe.go b/src/internal/observe/observe.go index 6d648be5f..d8492109d 100644 --- a/src/internal/observe/observe.go +++ b/src/internal/observe/observe.go @@ -138,8 +138,9 @@ const ( // Progress Updates // Message is used to display a progress message -func Message(ctx context.Context, message string) { - logger.Ctx(ctx).Info(message) +func Message(ctx context.Context, msg cleanable) { + logger.Ctx(ctx).Info(msg.clean()) + message := msg.String() if cfg.hidden() { return @@ -163,9 +164,15 @@ func Message(ctx context.Context, message string) { // MessageWithCompletion is used to display progress with a spinner // that switches to "done" when the completion channel is signalled -func MessageWithCompletion(ctx context.Context, message string) (chan<- struct{}, func()) { +func MessageWithCompletion( + ctx context.Context, + msg cleanable, +) (chan<- struct{}, func()) { + clean := msg.clean() + message := msg.String() + log := logger.Ctx(ctx) - log.Info(message) + log.Info(clean) completionCh := make(chan struct{}, 1) @@ -201,7 +208,7 @@ func MessageWithCompletion(ctx context.Context, message string) (chan<- struct{} }(completionCh) wacb := waitAndCloseBar(bar, func() { - log.Info("done - " + message) + log.Info("done - " + clean) }) return completionCh, wacb @@ -217,10 +224,11 @@ func MessageWithCompletion(ctx context.Context, message string) (chan<- struct{} func ItemProgress( ctx context.Context, rc io.ReadCloser, - header, iname string, + header string, + iname cleanable, totalBytes int64, ) (io.ReadCloser, func()) { - log := logger.Ctx(ctx).With("item", iname, "size", humanize.Bytes(uint64(totalBytes))) + log := logger.Ctx(ctx).With("item", iname.clean(), "size", humanize.Bytes(uint64(totalBytes))) log.Debug(header) if cfg.hidden() || rc == nil || totalBytes == 0 { @@ -232,7 +240,7 @@ func ItemProgress( barOpts := []mpb.BarOption{ mpb.PrependDecorators( decor.Name(header, decor.WCSyncSpaceR), - decor.Name(iname, decor.WCSyncSpaceR), + decor.Name(iname.String(), decor.WCSyncSpaceR), decor.CountersKibiByte(" %.1f/%.1f ", decor.WC{W: 8}), decor.NewPercentage("%d ", decor.WC{W: 4}), ), @@ -256,9 +264,14 @@ func ItemProgress( // of the specified count. // Each write to the provided channel counts as a single increment. // The caller is expected to close the channel. -func ProgressWithCount(ctx context.Context, header, message string, count int64) (chan<- struct{}, func()) { +func ProgressWithCount( + ctx context.Context, + header string, + message cleanable, + count int64, +) (chan<- struct{}, func()) { log := logger.Ctx(ctx) - lmsg := fmt.Sprintf("%s %s - %d", header, message, count) + lmsg := fmt.Sprintf("%s %s - %d", header, message.clean(), count) log.Info(lmsg) progressCh := make(chan struct{}) @@ -281,7 +294,7 @@ func ProgressWithCount(ctx context.Context, header, message string, count int64) barOpts := []mpb.BarOption{ mpb.PrependDecorators( decor.Name(header, decor.WCSyncSpaceR), - decor.Name(message), + decor.Name(message.String()), decor.Counters(0, " %d/%d "), ), } @@ -355,13 +368,17 @@ func makeSpinFrames(barWidth int) { // counts as a single increment. The caller is expected to close the channel. func CollectionProgress( ctx context.Context, - user, category, dirName string, + category string, + user, dirName cleanable, ) (chan<- struct{}, func()) { - log := logger.Ctx(ctx).With("user", user, "category", category, "dir", dirName) - message := "Collecting " + dirName + log := logger.Ctx(ctx).With( + "user", user.clean(), + "category", category, + "dir", dirName.clean()) + message := "Collecting Directory" log.Info(message) - if cfg.hidden() || len(user) == 0 || len(dirName) == 0 { + if cfg.hidden() || len(user.String()) == 0 || len(dirName.String()) == 0 { ch := make(chan struct{}) go func(ci <-chan struct{}) { @@ -379,7 +396,7 @@ func CollectionProgress( wg.Add(1) barOpts := []mpb.BarOption{ - mpb.PrependDecorators(decor.Name(category)), + mpb.PrependDecorators(decor.Name(string(category))), mpb.AppendDecorators( decor.CurrentNoUnit("%d - ", decor.WCSyncSpace), decor.Name(fmt.Sprintf("%s - %s", user, dirName)), @@ -439,8 +456,65 @@ func waitAndCloseBar(bar *mpb.Bar, log func()) func() { // other funcs // --------------------------------------------------------------------------- -// Bulletf prepends the message with "∙ ", and formats it. -// Ex: Bulletf("%s", "foo") => "∙ foo" -func Bulletf(template string, vs ...any) string { - return fmt.Sprintf("∙ "+template, vs...) +const Bullet = "∙" + +// --------------------------------------------------------------------------- +// PII redaction +// --------------------------------------------------------------------------- + +type cleanable interface { + clean() string + String() string +} + +type PII string + +func (p PII) clean() string { + return "***" +} + +func (p PII) String() string { + return string(p) +} + +type Safe string + +func (s Safe) clean() string { + return string(s) +} + +func (s Safe) String() string { + return string(s) +} + +type bulletPII struct { + tmpl string + vars []cleanable +} + +func Bulletf(template string, vs ...cleanable) bulletPII { + return bulletPII{ + tmpl: "∙ " + template, + vars: vs, + } +} + +func (b bulletPII) clean() string { + vs := make([]any, 0, len(b.vars)) + + for _, v := range b.vars { + vs = append(vs, v.clean()) + } + + return fmt.Sprintf(b.tmpl, vs...) +} + +func (b bulletPII) String() string { + vs := make([]any, 0, len(b.vars)) + + for _, v := range b.vars { + vs = append(vs, v.String()) + } + + return fmt.Sprintf(b.tmpl, vs...) } diff --git a/src/internal/observe/observe_test.go b/src/internal/observe/observe_test.go index 681cbeaf5..38a95bec3 100644 --- a/src/internal/observe/observe_test.go +++ b/src/internal/observe/observe_test.go @@ -26,6 +26,12 @@ func TestObserveProgressUnitSuite(t *testing.T) { suite.Run(t, new(ObserveProgressUnitSuite)) } +var ( + tst = observe.Safe("test") + testcat = observe.Safe("testcat") + testertons = observe.Safe("testertons") +) + func (suite *ObserveProgressUnitSuite) TestItemProgress() { ctx, flush := tester.NewContext() defer flush() @@ -47,7 +53,7 @@ func (suite *ObserveProgressUnitSuite) TestItemProgress() { ctx, io.NopCloser(bytes.NewReader(from)), "folder", - "test", + tst, 100) require.NotNil(t, prog) require.NotNil(t, closer) @@ -97,7 +103,7 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnCtxCancel observe.SeedWriter(context.Background(), nil, nil) }() - progCh, closer := observe.CollectionProgress(ctx, "test", "testcat", "testertons") + progCh, closer := observe.CollectionProgress(ctx, "test", testcat, testertons) require.NotNil(t, progCh) require.NotNil(t, closer) @@ -132,7 +138,7 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnChannelCl observe.SeedWriter(context.Background(), nil, nil) }() - progCh, closer := observe.CollectionProgress(ctx, "test", "testcat", "testertons") + progCh, closer := observe.CollectionProgress(ctx, "test", testcat, testertons) require.NotNil(t, progCh) require.NotNil(t, closer) @@ -164,7 +170,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgress() { message := "Test Message" - observe.Message(ctx, message) + observe.Message(ctx, observe.Safe(message)) observe.Complete() require.NotEmpty(suite.T(), recorder.String()) require.Contains(suite.T(), recorder.String(), message) @@ -185,7 +191,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithCompletion() { message := "Test Message" - ch, closer := observe.MessageWithCompletion(ctx, message) + ch, closer := observe.MessageWithCompletion(ctx, observe.Safe(message)) // Trigger completion ch <- struct{}{} @@ -215,7 +221,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithChannelClosed() { message := "Test Message" - ch, closer := observe.MessageWithCompletion(ctx, message) + ch, closer := observe.MessageWithCompletion(ctx, observe.Safe(message)) // Close channel without completing close(ch) @@ -247,7 +253,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithContextCancelled() message := "Test Message" - _, closer := observe.MessageWithCompletion(ctx, message) + _, closer := observe.MessageWithCompletion(ctx, observe.Safe(message)) // cancel context cancel() @@ -278,7 +284,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithCount() { message := "Test Message" count := 3 - ch, closer := observe.ProgressWithCount(ctx, header, message, int64(count)) + ch, closer := observe.ProgressWithCount(ctx, header, observe.Safe(message), int64(count)) for i := 0; i < count; i++ { ch <- struct{}{} @@ -311,7 +317,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithCountChannelClosed message := "Test Message" count := 3 - ch, closer := observe.ProgressWithCount(ctx, header, message, int64(count)) + ch, closer := observe.ProgressWithCount(ctx, header, observe.Safe(message), int64(count)) close(ch) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index a5a955150..b2a0c552c 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -44,7 +44,7 @@ type BackupOperation struct { // BackupResults aggregate the details of the result of the operation. type BackupResults struct { - stats.Errs + stats.Errs // deprecated in place of fault.Errors in the base operation. stats.ReadWrites stats.StartAndEndTime BackupID model.StableID `json:"backupID"` @@ -90,7 +90,6 @@ type backupStats struct { k *kopia.BackupStats gc *support.ConnectorOperationStatus resourceCount int - started bool readErr, writeErr error } @@ -228,7 +227,6 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { // should always be 1, since backups are 1:1 with resourceOwners. opStats.resourceCount = 1 - opStats.started = true return err } @@ -256,14 +254,18 @@ func produceBackupDataCollections( metadata []data.Collection, ctrlOpts control.Options, ) ([]data.Collection, error) { - complete, closer := observe.MessageWithCompletion(ctx, "Discovering items to backup") + complete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Discovering items to backup")) defer func() { complete <- struct{}{} close(complete) closer() }() - return gc.DataCollections(ctx, sel, metadata, ctrlOpts) + // TODO(ashmrtn): When we're ready to wire up the global exclude list return + // all values. + cols, _, errs := gc.DataCollections(ctx, sel, metadata, ctrlOpts) + + return cols, errs } // --------------------------------------------------------------------------- @@ -275,6 +277,7 @@ type backuper interface { ctx context.Context, bases []kopia.IncrementalBase, cs []data.Collection, + excluded map[string]struct{}, tags map[string]string, buildTreeWithBase bool, ) (*kopia.BackupStats, *details.Builder, map[string]path.Path, error) @@ -338,7 +341,7 @@ func consumeBackupDataCollections( backupID model.StableID, isIncremental bool, ) (*kopia.BackupStats, *details.Builder, map[string]path.Path, error) { - complete, closer := observe.MessageWithCompletion(ctx, "Backing up data") + complete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Backing up data")) defer func() { complete <- struct{}{} close(complete) @@ -400,7 +403,33 @@ func consumeBackupDataCollections( ) } - return bu.BackupCollections(ctx, bases, cs, tags, isIncremental) + kopiaStats, deets, itemsSourcedFromBase, err := bu.BackupCollections( + ctx, + bases, + cs, + nil, + tags, + isIncremental, + ) + + if kopiaStats.ErrorCount > 0 || kopiaStats.IgnoredErrorCount > 0 { + if err != nil { + err = errors.Wrapf( + err, + "kopia snapshot failed with %v catastrophic errors and %v ignored errors", + kopiaStats.ErrorCount, + kopiaStats.IgnoredErrorCount, + ) + } else { + err = errors.Errorf( + "kopia snapshot failed with %v catastrophic errors and %v ignored errors", + kopiaStats.ErrorCount, + kopiaStats.IgnoredErrorCount, + ) + } + } + + return kopiaStats, deets, itemsSourcedFromBase, err } func matchesReason(reasons []kopia.Reason, p path.Path) bool { @@ -531,9 +560,12 @@ func (op *BackupOperation) persistResults( ) error { op.Results.StartedAt = started op.Results.CompletedAt = time.Now() + op.Results.ReadErrors = opStats.readErr + op.Results.WriteErrors = opStats.writeErr op.Status = Completed - if !opStats.started { + + if opStats.readErr != nil || opStats.writeErr != nil { op.Status = Failed return multierror.Append( @@ -546,9 +578,6 @@ func (op *BackupOperation) persistResults( op.Status = NoData } - op.Results.ReadErrors = opStats.readErr - op.Results.WriteErrors = opStats.writeErr - op.Results.BytesRead = opStats.k.TotalHashedBytes op.Results.BytesUploaded = opStats.k.TotalUploadedBytes op.Results.ItemsRead = opStats.gc.Successful @@ -580,6 +609,7 @@ func (op *BackupOperation) createBackupModels( op.Selectors, op.Results.ReadWrites, op.Results.StartAndEndTime, + op.Errors, ) err = op.store.Put(ctx, model.BackupSchema, b) diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 83748baa2..21d4009e0 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -153,6 +153,8 @@ func runAndCheckBackup( assert.Less(t, int64(0), bo.Results.BytesRead, "bytes read") assert.Less(t, int64(0), bo.Results.BytesUploaded, "bytes uploaded") assert.Equal(t, 1, bo.Results.ResourceOwners, "count of resource owners") + assert.NoError(t, bo.Errors.Err(), "incremental non-recoverable error") + assert.Empty(t, bo.Errors.Errs(), "incremental recoverable/iteration errors") assert.NoError(t, bo.Results.ReadErrors, "errors reading data") assert.NoError(t, bo.Results.WriteErrors, "errors writing data") assert.Equal(t, 1, mb.TimesCalled[events.BackupStart], "backup-start events") @@ -616,6 +618,8 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchange() { assert.Greater(t, bo.Results.BytesRead, incBO.Results.BytesRead, "incremental bytes read") assert.Greater(t, bo.Results.BytesUploaded, incBO.Results.BytesUploaded, "incremental bytes uploaded") assert.Equal(t, bo.Results.ResourceOwners, incBO.Results.ResourceOwners, "incremental backup resource owner") + assert.NoError(t, incBO.Errors.Err(), "incremental non-recoverable error") + assert.Empty(t, incBO.Errors.Errs(), "count incremental recoverable/iteration errors") assert.NoError(t, incBO.Results.ReadErrors, "incremental read errors") assert.NoError(t, incBO.Results.WriteErrors, "incremental write errors") assert.Equal(t, 1, incMB.TimesCalled[events.BackupStart], "incremental backup-start events") @@ -633,6 +637,8 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { ctx, flush := tester.NewContext() defer flush() + tester.LogTimeOfTest(suite.T()) + var ( t = suite.T() acct = tester.NewM365Account(t) @@ -655,7 +661,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { m365, err := acct.M365Config() require.NoError(t, err) - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), acct, connector.Users) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), acct, connector.Users) require.NoError(t, err) ac, err := api.NewClient(m365) @@ -803,7 +809,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { { name: "move an email folder to a subfolder", updateUserData: func(t *testing.T) { - // contacts cannot be sufoldered; this is an email-only change + // contacts and events cannot be sufoldered; this is an email-only change toContainer := dataset[path.EmailCategory].dests[container1].containerID fromContainer := dataset[path.EmailCategory].dests[container2].containerID @@ -826,23 +832,22 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { updateUserData: func(t *testing.T) { for category, d := range dataset { containerID := d.dests[container2].containerID - cli := gc.Service.Client().UsersById(suite.user) switch category { case path.EmailCategory: require.NoError( t, - cli.MailFoldersById(containerID).Delete(ctx, nil), + ac.Mail().DeleteContainer(ctx, suite.user, containerID), "deleting an email folder") case path.ContactsCategory: require.NoError( t, - cli.ContactFoldersById(containerID).Delete(ctx, nil), + ac.Contacts().DeleteContainer(ctx, suite.user, containerID), "deleting a contacts folder") case path.EventsCategory: require.NoError( t, - cli.CalendarsById(containerID).Delete(ctx, nil), + ac.Events().DeleteContainer(ctx, suite.user, containerID), "deleting a calendar") } } @@ -923,19 +928,19 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { require.NoError(t, err, "updating contact folder name") case path.EventsCategory: - ccf := cli.CalendarsById(containerID) + cbi := cli.CalendarsById(containerID) - body, err := ccf.Get(ctx, nil) + body, err := cbi.Get(ctx, nil) require.NoError(t, err, "getting calendar") body.SetName(&containerRename) - _, err = ccf.Patch(ctx, body, nil) + _, err = cbi.Patch(ctx, body, nil) require.NoError(t, err, "updating calendar name") } } }, - itemsRead: 0, - itemsWritten: 4, + itemsRead: 0, // containers are not counted as reads + itemsWritten: 4, // two items per category }, { name: "add a new item", @@ -1038,6 +1043,8 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { // +4 on read/writes to account for metadata: 1 delta and 1 path for each type. assert.Equal(t, test.itemsWritten+4, incBO.Results.ItemsWritten, "incremental items written") assert.Equal(t, test.itemsRead+4, incBO.Results.ItemsRead, "incremental items read") + assert.NoError(t, incBO.Errors.Err(), "incremental non-recoverable error") + assert.Empty(t, incBO.Errors.Errs(), "incremental recoverable/iteration errors") assert.NoError(t, incBO.Results.ReadErrors, "incremental read errors") assert.NoError(t, incBO.Results.WriteErrors, "incremental write errors") assert.Equal(t, 1, incMB.TimesCalled[events.BackupStart], "incremental backup-start events") diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index 32a6d1a4e..f867e2a11 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -95,6 +95,7 @@ func (mbu mockBackuper) BackupCollections( ctx context.Context, bases []kopia.IncrementalBase, cs []data.Collection, + excluded map[string]struct{}, tags map[string]string, buildTreeWithBase bool, ) (*kopia.BackupStats, *details.Builder, map[string]path.Path, error) { @@ -372,7 +373,6 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { expectStatus: Completed, expectErr: assert.NoError, stats: backupStats{ - started: true, resourceCount: 1, k: &kopia.BackupStats{ TotalFileCount: 1, @@ -388,7 +388,7 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { expectStatus: Failed, expectErr: assert.Error, stats: backupStats{ - started: false, + readErr: assert.AnError, k: &kopia.BackupStats{}, gc: &support.ConnectorOperationStatus{}, }, @@ -397,9 +397,8 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { expectStatus: NoData, expectErr: assert.NoError, stats: backupStats{ - started: true, - k: &kopia.BackupStats{}, - gc: &support.ConnectorOperationStatus{}, + k: &kopia.BackupStats{}, + gc: &support.ConnectorOperationStatus{}, }, }, } @@ -421,11 +420,11 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() { assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status") assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsRead, "items read") - assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors") assert.Equal(t, test.stats.k.TotalFileCount, op.Results.ItemsWritten, "items written") assert.Equal(t, test.stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read") assert.Equal(t, test.stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written") assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners") + assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors") assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors") assert.Equal(t, now, op.Results.StartedAt, "started at") assert.Less(t, now, op.Results.CompletedAt, "completed at") diff --git a/src/internal/operations/operation.go b/src/internal/operations/operation.go index e5382b022..32122d682 100644 --- a/src/internal/operations/operation.go +++ b/src/internal/operations/operation.go @@ -13,6 +13,7 @@ import ( "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/store" ) @@ -52,9 +53,11 @@ const ( // Specific processes (eg: backups, restores, etc) are expected to wrap operation // with process specific details. type operation struct { - CreatedAt time.Time `json:"createdAt"` // datetime of the operation's creation - Options control.Options `json:"options"` - Status opStatus `json:"status"` + CreatedAt time.Time `json:"createdAt"` + + Errors *fault.Errors `json:"errors"` + Options control.Options `json:"options"` + Status opStatus `json:"status"` bus events.Eventer kopia *kopia.Wrapper @@ -69,11 +72,14 @@ func newOperation( ) operation { return operation{ CreatedAt: time.Now(), + Errors: fault.New(opts.FailFast), Options: opts, - bus: bus, - kopia: kw, - store: sw, - Status: InProgress, + + bus: bus, + kopia: kw, + store: sw, + + Status: InProgress, } } @@ -95,7 +101,7 @@ func connectToM365( sel selectors.Selector, acct account.Account, ) (*connector.GraphConnector, error) { - complete, closer := observe.MessageWithCompletion(ctx, "Connecting to M365") + complete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Connecting to M365")) defer func() { complete <- struct{}{} close(complete) @@ -108,7 +114,7 @@ func connectToM365( resource = connector.Sites } - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), acct, resource) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), acct, resource) if err != nil { return nil, err } diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index 03c203b05..db5ca9a93 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -44,7 +44,7 @@ type RestoreOperation struct { // RestoreResults aggregate the details of the results of the operation. type RestoreResults struct { - stats.Errs + stats.Errs // deprecated in place of fault.Errors in the base operation. stats.ReadWrites stats.StartAndEndTime } @@ -89,7 +89,6 @@ type restoreStats struct { gc *support.ConnectorOperationStatus bytesRead *stats.ByteCounter resourceCount int - started bool readErr, writeErr error // a transient value only used to pair up start-end events. @@ -143,10 +142,8 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De detailsStore, ) if err != nil { - err = errors.Wrap(err, "restore") - opStats.readErr = err - - return nil, err + opStats.readErr = errors.Wrap(err, "restore") + return nil, opStats.readErr } ctx = clues.Add(ctx, "resource_owner", bup.Selector.DiscreteOwner) @@ -170,18 +167,16 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De ctx = clues.Add(ctx, "details_paths", len(paths)) - observe.Message(ctx, fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID)) + observe.Message(ctx, observe.Safe(fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID))) - kopiaComplete, closer := observe.MessageWithCompletion(ctx, "Enumerating items in repository") + kopiaComplete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Enumerating items in repository")) defer closer() defer close(kopiaComplete) dcs, err := op.kopia.RestoreMultipleItems(ctx, bup.SnapshotID, paths, opStats.bytesRead) if err != nil { - err = errors.Wrap(err, "retrieving service data") - opStats.readErr = err - - return nil, err + opStats.readErr = errors.Wrap(err, "retrieving service data") + return nil, opStats.readErr } kopiaComplete <- struct{}{} @@ -196,7 +191,7 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De return nil, opStats.readErr } - restoreComplete, closer := observe.MessageWithCompletion(ctx, "Restoring data") + restoreComplete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Restoring data")) defer closer() defer close(restoreComplete) @@ -207,14 +202,11 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De op.Destination, dcs) if err != nil { - err = errors.Wrap(err, "restoring service data") - opStats.writeErr = err - - return nil, err + opStats.writeErr = errors.Wrap(err, "restoring service data") + return nil, opStats.writeErr } restoreComplete <- struct{}{} - opStats.started = true opStats.gc = gc.AwaitStatus() logger.Ctx(ctx).Debug(gc.PrintableStatus()) @@ -230,10 +222,12 @@ func (op *RestoreOperation) persistResults( ) error { op.Results.StartedAt = started op.Results.CompletedAt = time.Now() + op.Results.ReadErrors = opStats.readErr + op.Results.WriteErrors = opStats.writeErr op.Status = Completed - if !opStats.started { + if opStats.readErr != nil || opStats.writeErr != nil { op.Status = Failed return multierror.Append( @@ -246,9 +240,6 @@ func (op *RestoreOperation) persistResults( op.Status = NoData } - op.Results.ReadErrors = opStats.readErr - op.Results.WriteErrors = opStats.writeErr - op.Results.BytesRead = opStats.bytesRead.NumBytes op.Results.ItemsRead = len(opStats.cs) // TODO: file count, not collection count op.Results.ItemsWritten = opStats.gc.Successful @@ -309,5 +300,9 @@ func formatDetailsForRestoration( paths[i] = p } + if errs != nil { + return nil, errs + } + return paths, nil } diff --git a/src/internal/operations/restore_test.go b/src/internal/operations/restore_test.go index 44de04c66..9bf21b806 100644 --- a/src/internal/operations/restore_test.go +++ b/src/internal/operations/restore_test.go @@ -57,7 +57,6 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { expectStatus: Completed, expectErr: assert.NoError, stats: restoreStats{ - started: true, resourceCount: 1, bytesRead: &stats.ByteCounter{ NumBytes: 42, @@ -73,7 +72,7 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { expectStatus: Failed, expectErr: assert.Error, stats: restoreStats{ - started: false, + readErr: assert.AnError, bytesRead: &stats.ByteCounter{}, gc: &support.ConnectorOperationStatus{}, }, @@ -82,7 +81,6 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { expectStatus: NoData, expectErr: assert.NoError, stats: restoreStats{ - started: true, bytesRead: &stats.ByteCounter{}, cs: []data.Collection{}, gc: &support.ConnectorOperationStatus{}, @@ -106,10 +104,10 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() { assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status") assert.Equal(t, len(test.stats.cs), op.Results.ItemsRead, "items read") - assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors") assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsWritten, "items written") assert.Equal(t, test.stats.bytesRead.NumBytes, op.Results.BytesRead, "resource owners") assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners") + assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors") assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors") assert.Equal(t, now, op.Results.StartedAt, "started at") assert.Less(t, now, op.Results.CompletedAt, "completed at") @@ -295,8 +293,10 @@ func (suite *RestoreOpIntegrationSuite) TestRestore_Run() { assert.Less(t, 0, ro.Results.ItemsWritten, "restored items written") assert.Less(t, int64(0), ro.Results.BytesRead, "bytes read") assert.Equal(t, 1, ro.Results.ResourceOwners, "resource Owners") - assert.Zero(t, ro.Results.ReadErrors, "errors while reading restore data") - assert.Zero(t, ro.Results.WriteErrors, "errors while writing restore data") + assert.NoError(t, ro.Errors.Err(), "non-recoverable error") + assert.Empty(t, ro.Errors.Errs(), "recoverable errors") + assert.NoError(t, ro.Results.ReadErrors, "errors while reading restore data") + assert.NoError(t, ro.Results.WriteErrors, "errors while writing restore data") assert.Equal(t, suite.numItems, ro.Results.ItemsWritten, "backup and restore wrote the same num of items") assert.Equal(t, 1, mb.TimesCalled[events.RestoreStart], "restore-start events") assert.Equal(t, 1, mb.TimesCalled[events.RestoreEnd], "restore-end events") diff --git a/src/internal/streamstore/streamstore.go b/src/internal/streamstore/streamstore.go index 92123194e..fb5dfccd9 100644 --- a/src/internal/streamstore/streamstore.go +++ b/src/internal/streamstore/streamstore.go @@ -78,6 +78,7 @@ func (ss *streamStore) WriteBackupDetails( nil, []data.Collection{dc}, nil, + nil, false, ) if err != nil { diff --git a/src/internal/tester/storage.go b/src/internal/tester/storage.go index 0cfabba20..3e16ce05e 100644 --- a/src/internal/tester/storage.go +++ b/src/internal/tester/storage.go @@ -29,11 +29,14 @@ func NewPrefixedS3Storage(t *testing.T) storage.Storage { cfg, err := readTestConfig() require.NoError(t, err, "configuring storage from test file") + prefix := testRepoRootPrefix + t.Name() + "-" + now + t.Logf("testing at s3 bucket [%s] prefix [%s]", cfg[TestCfgBucket], prefix) + st, err := storage.NewStorage( storage.ProviderS3, storage.S3Config{ Bucket: cfg[TestCfgBucket], - Prefix: testRepoRootPrefix + t.Name() + "-" + now, + Prefix: prefix, }, storage.CommonConfig{ Corso: credentials.GetCorso(), diff --git a/src/internal/version/version.go b/src/internal/version/version.go index 4a07d07ba..6b1859a80 100644 --- a/src/internal/version/version.go +++ b/src/internal/version/version.go @@ -1,3 +1,29 @@ package version +import ( + "os/exec" + "strings" +) + var Version = "dev" + +func CurrentVersion() string { + if len(Version) == 0 || Version == "dev" { + c, b := exec.Command("git", "describe", "--tag"), new(strings.Builder) + c.Stdout = b + + if err := c.Run(); err != nil { + return "dev" + } + + s := strings.TrimRight(b.String(), "\n") + + if len(s) != 0 { + return "dev-" + s + } + + return "dev" + } + + return Version +} diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index 6d73498eb..d0d9ddffd 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -10,6 +10,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/stats" + "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -31,8 +32,11 @@ type Backup struct { // Selector used in this operation Selector selectors.Selector `json:"selectors"` + // Errors contains all errors aggregated during a backup operation. + Errors fault.ErrorsData `json:"errors"` + // stats are embedded so that the values appear as top-level properties - stats.Errs + stats.Errs // Deprecated, replaced with Errors. stats.ReadWrites stats.StartAndEndTime } @@ -46,6 +50,7 @@ func New( selector selectors.Selector, rw stats.ReadWrites, se stats.StartAndEndTime, + errs *fault.Errors, ) *Backup { return &Backup{ BaseModel: model.BaseModel{ @@ -59,6 +64,7 @@ func New( DetailsID: detailsID, Status: status, Selector: selector, + Errors: errs.Data(), ReadWrites: rw, StartAndEndTime: se, } @@ -102,7 +108,7 @@ type Printable struct { func (b Backup) MinimumPrintable() any { return Printable{ ID: b.ID, - ErrorCount: support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors), + ErrorCount: b.errorCount(), StartedAt: b.StartedAt, Status: b.Status, Version: "0", @@ -125,8 +131,7 @@ func (b Backup) Headers() []string { // Values returns the values matching the Headers list for printing // out to a terminal in a columnar display. func (b Backup) Values() []string { - errCount := support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors) - status := fmt.Sprintf("%s (%d errors)", b.Status, errCount) + status := fmt.Sprintf("%s (%d errors)", b.Status, b.errorCount()) return []string{ common.FormatTabularDisplayTime(b.StartedAt), @@ -135,3 +140,23 @@ func (b Backup) Values() []string { b.Selector.DiscreteOwner, } } + +func (b Backup) errorCount() int { + var errCount int + + // current tracking + if b.ReadErrors != nil || b.WriteErrors != nil { + return support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors) + } + + // future tracking + if b.Errors.Err != nil || len(b.Errors.Errs) > 0 { + if b.Errors.Err != nil { + errCount++ + } + + errCount += len(b.Errors.Errs) + } + + return errCount +} diff --git a/src/pkg/backup/backup_test.go b/src/pkg/backup/backup_test.go index 1ddf4f4a2..8fbf8c62f 100644 --- a/src/pkg/backup/backup_test.go +++ b/src/pkg/backup/backup_test.go @@ -13,6 +13,7 @@ import ( "github.com/alcionai/corso/src/internal/model" "github.com/alcionai/corso/src/internal/stats" "github.com/alcionai/corso/src/pkg/backup" + "github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -40,6 +41,9 @@ func stubBackup(t time.Time) backup.Backup { DetailsID: "details", Status: "status", Selector: sel.Selector, + Errors: fault.ErrorsData{ + Errs: []error{errors.New("read"), errors.New("write")}, + }, Errs: stats.Errs{ ReadErrors: errors.New("1"), WriteErrors: errors.New("1"), diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index f64febe2e..0d5ffe250 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -6,6 +6,7 @@ import ( "path/filepath" "time" + "github.com/alcionai/clues" "github.com/spf13/cobra" "github.com/spf13/pflag" "go.uber.org/zap" @@ -264,7 +265,7 @@ func Ctx(ctx context.Context) *zap.SugaredLogger { return singleton(levelOf(llFlag), defaultLogLocation()) } - return l.(*zap.SugaredLogger) + return l.(*zap.SugaredLogger).With(clues.Slice(ctx)...) } // transforms the llevel flag value to a logLevel enum diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index c98a037c7..f8c8d3d49 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -154,7 +154,7 @@ func Connect( // their output getting clobbered (#1720) defer observe.Complete() - complete, closer := observe.MessageWithCompletion(ctx, "Connecting to repository") + complete, closer := observe.MessageWithCompletion(ctx, observe.Safe("Connecting to repository")) defer closer() defer close(complete) diff --git a/src/pkg/repository/repository_load_test.go b/src/pkg/repository/repository_load_test.go index 5725e5fda..36714df71 100644 --- a/src/pkg/repository/repository_load_test.go +++ b/src/pkg/repository/repository_load_test.go @@ -114,6 +114,7 @@ func runLoadTest( prefix, service string, usersUnderTest []string, bupSel, restSel selectors.Selector, + runRestore bool, ) { //revive:enable:context-as-argument t.Run(prefix+"_load_test_main", func(t *testing.T) { @@ -126,12 +127,33 @@ func runLoadTest( runBackupListLoadTest(t, ctx, r, service, bid) runBackupDetailsLoadTest(t, ctx, r, service, bid, usersUnderTest) + runRestoreLoadTest(t, ctx, r, prefix, service, bid, usersUnderTest, restSel, b, runRestore) + }) +} + +//revive:disable:context-as-argument +func runRestoreLoadTest( + t *testing.T, + ctx context.Context, + r repository.Repository, + prefix, service, backupID string, + usersUnderTest []string, + restSel selectors.Selector, + bup operations.BackupOperation, + runRestore bool, +) { + //revive:enable:context-as-argument + t.Run(prefix+"_load_test_restore", func(t *testing.T) { + if !runRestore { + t.Skip("restore load test is toggled off") + } + dest := tester.DefaultTestRestoreDestination() - rst, err := r.NewRestore(ctx, bid, restSel, dest) + rst, err := r.NewRestore(ctx, backupID, restSel, dest) require.NoError(t, err) - runRestoreLoadTest(t, ctx, rst, service, b.Results.ItemsWritten, usersUnderTest) + doRestoreLoadTest(t, ctx, rst, service, bup.Results.ItemsWritten, usersUnderTest) }) } @@ -162,8 +184,10 @@ func runBackupLoadTest( assert.Less(t, 0, b.Results.ItemsWritten, "items written") assert.Less(t, int64(0), b.Results.BytesUploaded, "bytes uploaded") assert.Equal(t, len(users), b.Results.ResourceOwners, "resource owners") - assert.Zero(t, b.Results.ReadErrors, "read errors") - assert.Zero(t, b.Results.WriteErrors, "write errors") + assert.NoError(t, b.Errors.Err(), "non-recoverable error") + assert.Empty(t, b.Errors.Errs(), "recoverable errors") + assert.NoError(t, b.Results.ReadErrors, "read errors") + assert.NoError(t, b.Results.WriteErrors, "write errors") }) } @@ -240,7 +264,7 @@ func runBackupDetailsLoadTest( } //revive:disable:context-as-argument -func runRestoreLoadTest( +func doRestoreLoadTest( t *testing.T, ctx context.Context, r operations.RestoreOperation, @@ -268,8 +292,10 @@ func runRestoreLoadTest( assert.Less(t, 0, r.Results.ItemsRead, "items read") assert.Less(t, 0, r.Results.ItemsWritten, "items written") assert.Equal(t, len(users), r.Results.ResourceOwners, "resource owners") - assert.Zero(t, r.Results.ReadErrors, "read errors") - assert.Zero(t, r.Results.WriteErrors, "write errors") + assert.NoError(t, r.Errors.Err(), "non-recoverable error") + assert.Empty(t, r.Errors.Errs(), "recoverable errors") + assert.NoError(t, r.Results.ReadErrors, "read errors") + assert.NoError(t, r.Results.WriteErrors, "write errors") assert.Equal(t, expectItemCount, r.Results.ItemsWritten, "backup and restore wrote the same count of items") ensureAllUsersInDetails(t, users, ds, "restore", name) @@ -408,6 +434,7 @@ func (suite *RepositoryLoadTestExchangeSuite) TestExchange() { "all_users", "exchange", suite.usersUnderTest, sel, sel, // same selection for backup and restore + true, ) } @@ -456,6 +483,7 @@ func (suite *RepositoryIndividualLoadTestExchangeSuite) TestExchange() { "single_user", "exchange", suite.usersUnderTest, sel, sel, // same selection for backup and restore + true, ) } @@ -504,6 +532,7 @@ func (suite *RepositoryLoadTestOneDriveSuite) TestOneDrive() { "all_users", "one_drive", suite.usersUnderTest, sel, sel, // same selection for backup and restore + false, ) } @@ -523,7 +552,6 @@ func TestRepositoryIndividualLoadTestOneDriveSuite(t *testing.T) { func (suite *RepositoryIndividualLoadTestOneDriveSuite) SetupSuite() { t := suite.T() - t.Skip("not running onedrive load tests atm") t.Parallel() suite.ctx, suite.repo, suite.acct, suite.st = initM365Repo(t) suite.usersUnderTest = singleUserSet(t) @@ -548,6 +576,7 @@ func (suite *RepositoryIndividualLoadTestOneDriveSuite) TestOneDrive() { "single_user", "one_drive", suite.usersUnderTest, sel, sel, // same selection for backup and restore + false, ) } @@ -596,6 +625,7 @@ func (suite *RepositoryLoadTestSharePointSuite) TestSharePoint() { "all_sites", "share_point", suite.sitesUnderTest, sel, sel, // same selection for backup and restore + false, ) } @@ -640,5 +670,6 @@ func (suite *RepositoryIndividualLoadTestSharePointSuite) TestSharePoint() { "single_site", "share_point", suite.sitesUnderTest, sel, sel, // same selection for backup and restore + false, ) } diff --git a/src/pkg/services/m365/m365.go b/src/pkg/services/m365/m365.go index e0dd75af9..984326b6d 100644 --- a/src/pkg/services/m365/m365.go +++ b/src/pkg/services/m365/m365.go @@ -21,7 +21,7 @@ type User struct { // Users returns a list of users in the specified M365 tenant // TODO: Implement paging support func Users(ctx context.Context, m365Account account.Account) ([]*User, error) { - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), m365Account, connector.Users) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), m365Account, connector.Users) if err != nil { return nil, errors.Wrap(err, "could not initialize M365 graph connection") } @@ -77,7 +77,7 @@ func UserPNs(ctx context.Context, m365Account account.Account) ([]string, error) // SiteURLs returns a list of SharePoint site WebURLs in the specified M365 tenant func SiteURLs(ctx context.Context, m365Account account.Account) ([]string, error) { - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), m365Account, connector.Sites) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), m365Account, connector.Sites) if err != nil { return nil, errors.Wrap(err, "could not initialize M365 graph connection") } @@ -87,7 +87,7 @@ func SiteURLs(ctx context.Context, m365Account account.Account) ([]string, error // SiteURLs returns a list of SharePoint sites IDs in the specified M365 tenant func SiteIDs(ctx context.Context, m365Account account.Account) ([]string, error) { - gc, err := connector.NewGraphConnector(ctx, graph.LargeItemClient(), m365Account, connector.Sites) + gc, err := connector.NewGraphConnector(ctx, graph.HTTPClient(graph.NoTimeout()), m365Account, connector.Sites) if err != nil { return nil, errors.Wrap(err, "could not initialize M365 graph connection") } diff --git a/src/testfiles/otel_daemon/Dockerfile b/src/testfiles/otel_daemon/Dockerfile index 85afeb757..89b956905 100644 --- a/src/testfiles/otel_daemon/Dockerfile +++ b/src/testfiles/otel_daemon/Dockerfile @@ -1,4 +1,4 @@ -FROM amazonlinux +FROM amazonlinux:2 RUN yum install -y unzip RUN curl -o daemon.zip https://s3.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-linux-3.x.zip RUN unzip daemon.zip && cp xray /usr/bin/xray diff --git a/website/docs/developers/build.md b/website/docs/developers/build.md index baa3261ed..8d0dfa878 100644 --- a/website/docs/developers/build.md +++ b/website/docs/developers/build.md @@ -19,10 +19,12 @@ If you don't have Go available, you can find installation instructions [here](ht This will generate a binary named `corso` in the directory where you run the build. :::note -You can download binary artifacts of the latest commit from GitHub by -navigating to the "Summary" page of the `Build/Release Corso` CI job -that was run for that commit. -You will find the artifacts at the bottom of the page. +Prebuilt binary artifacts of the latest commit are available on GitHub. +You can access them by navigating to the "Summary" page of +the [`Build/Release Corso` CI job](https://github.com/alcionai/corso/actions/workflows/ci.yml?query=branch%3Amain) +that was run for the latest commit on the `main` branch. +The downloads will be available in the "Artifacts" section towards the +bottom of the page. ::: ### Building via Docker diff --git a/website/docs/setup/fault-tolerance.md b/website/docs/setup/fault-tolerance.md new file mode 100644 index 000000000..1771f600a --- /dev/null +++ b/website/docs/setup/fault-tolerance.md @@ -0,0 +1,48 @@ +# Fault tolerance + +Given the millions of objects found in a typical Microsoft 365 tenant, +Corso is optimized for high-performance processing, hardened to +tolerate transient failures and, most importantly, able to restart backups. + +Corso’s fault-tolerance architecture is motivated by Microsoft’s Graph +API variable performance and throttling. Corso follows Microsoft’s +recommend best practices (for example, [correctly decorating API +traffic](https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online#how-to-decorate-your-http-traffic)) +and, in addition, implements a number of optimizations to improve +backup and restore reliability. + +## Recovery from transient failures + +Corso, at the HTTP layer, will retry requests (after a HTTP timeout, +for example) and will respect Graph API’s directives such as the +`retry-after` header to backoff when needed. This allows backups to +succeed in the face of transient or temporary failures. + +## Restarting from permanent API failures + +The Graph API can, for internal reasons, exhibit extended periods of +failures for particular Graph objects. In this scenario, bounded retries +will be ineffective. Unless invoked with the +fail fast option, Corso will skip over these failing objects. For +backups, it will move forward with backing up other objects belonging +to the user and, for restores, it will continue with trying to restore +any remaining objects. If a multi-user backed is in progress (via `*` +or by specifying multiple users with the `—user` argument), Corso will +also continue processing backups for the remaining users. In both +cases, Corso will exit with a non-zero exit code to reflect incomplete +backups or restores. + +On subsequent backup attempts, Corso will try to +minimize the work involved. If the previous backup was successful and +Corso’s stored state tokens haven’t expired, it will use [delta +queries](https://learn.microsoft.com/en-us/graph/delta-query-overview), +wherever supported, to perform incremental backups. + +If the previous backup for a user had resulted in a failure, Corso +uses a variety of fallback mechanisms to reduce the amount of data +downloaded and reduce the number of objects enumerated. For example, with +OneDrive, Corso won't redo downloads of data from Microsoft 365 or +uploads of data to the Corso repository if it had successfully backed +up that OneDrive file as a part of a previously incomplete and failed +backup. Even if the Graph API might not allow Corso to skip +downloading data, Corso can still skip another upload it to the repository. diff --git a/website/docs/setup/repos.md b/website/docs/setup/repos.md index 6dbc62dff..3c2a7c119 100644 --- a/website/docs/setup/repos.md +++ b/website/docs/setup/repos.md @@ -10,9 +10,15 @@ import TabItem from '@theme/TabItem'; import TOCInline from '@theme/TOCInline'; import {Version} from '@site/src/corsoEnv'; -A Corso [repository](../concepts#corso-concepts) stores encrypted copies of your backup data. Corso uses +A Corso [repository](../concepts#corso-concepts) stores encrypted copies of a Microsoft 365 tenant's +backup data. Each repository is configured to store data in an object storage bucket and, optionally, +a user-specified prefix within the bucket. A repository is only meant to store a single tenant's data +but a single object storage bucket can contain multiple repositories if unique `--prefix` options are +specified when initializing a repository. + +Within a repository, Corso uses AES256-GCM-HMAC-SHA256 to encrypt data at rest using keys that are derived from the repository passphrase. -Data in flight is encrypted via TLS. +Data in flight to and from the repositiry is encrypted via TLS. Repositories are supported on the following object storage systems: diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 375df5a88..18382ab0e 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -59,7 +59,7 @@ const config = { filename: 'sitemap.xml', }, gtag: { - trackingID: 'G-YXBFPQZ05N', + trackingID: 'GTM-KM3XWPV', }, theme: { customCss: require.resolve('./src/css/custom.scss'), diff --git a/website/package-lock.json b/website/package-lock.json index 0662eb756..11a0d15e1 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -8,10 +8,10 @@ "name": "docs", "version": "0.1.0", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/plugin-google-gtag": "^2.2.0", - "@docusaurus/preset-classic": "2.2.0", - "@loadable/component": "^5.15.2", + "@docusaurus/core": "2.3.0", + "@docusaurus/plugin-google-gtag": "^2.3.0", + "@docusaurus/preset-classic": "2.3.0", + "@loadable/component": "^5.15.3", "@mdx-js/react": "^1.6.22", "animate.css": "^4.1.1", "clsx": "^1.2.1", @@ -29,29 +29,27 @@ "wow.js": "^1.2.2" }, "devDependencies": { - "@docusaurus/module-type-aliases": "2.2.0", - "@iconify/react": "^4.0.1", + "@docusaurus/module-type-aliases": "2.3.0", + "@iconify/react": "^4.1.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.21", "tailwindcss": "^3.2.4" } }, "node_modules/@algolia/autocomplete-core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.2.tgz", - "integrity": "sha512-eclwUDC6qfApNnEfu1uWcL/rudQsn59tjEoUYZYE2JSXZrHLRjBUGMxiCoknobU2Pva8ejb0eRxpIYDtVVqdsw==", - "license": "MIT", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz", + "integrity": "sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg==", "dependencies": { - "@algolia/autocomplete-shared": "1.7.2" + "@algolia/autocomplete-shared": "1.7.4" } }, "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.2.tgz", - "integrity": "sha512-+RYEG6B0QiGGfRb2G3MtPfyrl0dALF3cQNTWBzBX6p5o01vCCGTTinAm2UKG3tfc2CnOMAtnPLkzNZyJUpnVJw==", - "license": "MIT", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz", + "integrity": "sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ==", "dependencies": { - "@algolia/autocomplete-shared": "1.7.2" + "@algolia/autocomplete-shared": "1.7.4" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -59,144 +57,128 @@ } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz", - "integrity": "sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug==", - "license": "MIT" + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz", + "integrity": "sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg==" }, "node_modules/@algolia/cache-browser-local-storage": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.2.tgz", - "integrity": "sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz", + "integrity": "sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw==", "dependencies": { - "@algolia/cache-common": "4.14.2" + "@algolia/cache-common": "4.14.3" } }, "node_modules/@algolia/cache-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.14.2.tgz", - "integrity": "sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==", - "license": "MIT" + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.14.3.tgz", + "integrity": "sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q==" }, "node_modules/@algolia/cache-in-memory": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.14.2.tgz", - "integrity": "sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz", + "integrity": "sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg==", "dependencies": { - "@algolia/cache-common": "4.14.2" + "@algolia/cache-common": "4.14.3" } }, "node_modules/@algolia/client-account": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.14.2.tgz", - "integrity": "sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.14.3.tgz", + "integrity": "sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ==", "dependencies": { - "@algolia/client-common": "4.14.2", - "@algolia/client-search": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/client-search": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "node_modules/@algolia/client-analytics": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.14.2.tgz", - "integrity": "sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.14.3.tgz", + "integrity": "sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw==", "dependencies": { - "@algolia/client-common": "4.14.2", - "@algolia/client-search": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/client-search": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "node_modules/@algolia/client-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.14.2.tgz", - "integrity": "sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.14.3.tgz", + "integrity": "sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw==", "dependencies": { - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "node_modules/@algolia/client-personalization": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.14.2.tgz", - "integrity": "sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.14.3.tgz", + "integrity": "sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg==", "dependencies": { - "@algolia/client-common": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "node_modules/@algolia/client-search": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.14.2.tgz", - "integrity": "sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.14.3.tgz", + "integrity": "sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A==", "dependencies": { - "@algolia/client-common": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "node_modules/@algolia/events": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", - "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", - "license": "MIT" + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==" }, "node_modules/@algolia/logger-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.14.2.tgz", - "integrity": "sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==", - "license": "MIT" + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.14.3.tgz", + "integrity": "sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw==" }, "node_modules/@algolia/logger-console": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.14.2.tgz", - "integrity": "sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.14.3.tgz", + "integrity": "sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw==", "dependencies": { - "@algolia/logger-common": "4.14.2" + "@algolia/logger-common": "4.14.3" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.2.tgz", - "integrity": "sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz", + "integrity": "sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q==", "dependencies": { - "@algolia/requester-common": "4.14.2" + "@algolia/requester-common": "4.14.3" } }, "node_modules/@algolia/requester-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.14.2.tgz", - "integrity": "sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==", - "license": "MIT" + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.14.3.tgz", + "integrity": "sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw==" }, "node_modules/@algolia/requester-node-http": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.14.2.tgz", - "integrity": "sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz", + "integrity": "sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA==", "dependencies": { - "@algolia/requester-common": "4.14.2" + "@algolia/requester-common": "4.14.3" } }, "node_modules/@algolia/transporter": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.14.2.tgz", - "integrity": "sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.14.3.tgz", + "integrity": "sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w==", "dependencies": { - "@algolia/cache-common": "4.14.2", - "@algolia/logger-common": "4.14.2", - "@algolia/requester-common": "4.14.2" + "@algolia/cache-common": "4.14.3", + "@algolia/logger-common": "4.14.3", + "@algolia/requester-common": "4.14.3" } }, "node_modules/@ampproject/remapping": { @@ -1974,20 +1956,18 @@ } }, "node_modules/@docsearch/css": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.3.0.tgz", - "integrity": "sha512-rODCdDtGyudLj+Va8b6w6Y85KE85bXRsps/R4Yjwt5vueXKXZQKYw0aA9knxLBT6a/bI/GMrAcmCR75KYOM6hg==", - "license": "MIT" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.3.2.tgz", + "integrity": "sha512-dctFYiwbvDZkksMlsmc7pj6W6By/EjnVXJq5TEPd05MwQe+dcdHJgaIn1c8wfsucxHpIsdrUcgSkACHCq6aIhw==" }, "node_modules/@docsearch/react": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.3.0.tgz", - "integrity": "sha512-fhS5adZkae2SSdMYEMVg6pxI5a/cE+tW16ki1V0/ur4Fdok3hBRkmN/H8VvlXnxzggkQIIRIVvYPn00JPjen3A==", - "license": "MIT", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.3.2.tgz", + "integrity": "sha512-ugILab2TYKSh6IEHf6Z9xZbOovsYbsdfo60PBj+Bw+oMJ1MHJ7pBt1TTcmPki1hSgg8mysgKy2hDiVdPm7XWSQ==", "dependencies": { - "@algolia/autocomplete-core": "1.7.2", - "@algolia/autocomplete-preset-algolia": "1.7.2", - "@docsearch/css": "3.3.0", + "@algolia/autocomplete-core": "1.7.4", + "@algolia/autocomplete-preset-algolia": "1.7.4", + "@docsearch/css": "3.3.2", "algoliasearch": "^4.0.0" }, "peerDependencies": { @@ -2008,10 +1988,9 @@ } }, "node_modules/@docusaurus/core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.2.0.tgz", - "integrity": "sha512-Vd6XOluKQqzG12fEs9prJgDtyn6DPok9vmUWDR2E6/nV5Fl9SVkhEQOBxwObjk3kQh7OY7vguFaLh0jqdApWsA==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.3.0.tgz", + "integrity": "sha512-2AU5HfKyExO+/mi41SBnx5uY0aGZFXr3D93wntBY4lN1gsDKUpi7EE4lPBAXm9CoH4Pw6N24yDHy9CPR3sh/uA==", "dependencies": { "@babel/core": "^7.18.6", "@babel/generator": "^7.18.7", @@ -2023,13 +2002,13 @@ "@babel/runtime": "^7.18.6", "@babel/runtime-corejs3": "^7.18.6", "@babel/traverse": "^7.18.8", - "@docusaurus/cssnano-preset": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", + "@docusaurus/cssnano-preset": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "@slorber/static-site-generator-webpack-plugin": "^4.0.7", "@svgr/webpack": "^6.2.1", "autoprefixer": "^10.4.7", @@ -2167,10 +2146,9 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.2.0.tgz", - "integrity": "sha512-mAAwCo4n66TMWBH1kXnHVZsakW9VAXJzTO4yZukuL3ro4F+JtkMwKfh42EG75K/J/YIFQG5I/Bzy0UH/hFxaTg==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.3.0.tgz", + "integrity": "sha512-igmsXc1Q95lMeq07A1xua0/5wOPygDQ/ENSV7VVbiGhnvMv4gzkba8ZvbAtc7PmqK+kpYRfPzNCOk0GnQCvibg==", "dependencies": { "cssnano-preset-advanced": "^5.3.8", "postcss": "^8.4.14", @@ -2182,10 +2160,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.2.0.tgz", - "integrity": "sha512-DF3j1cA5y2nNsu/vk8AG7xwpZu6f5MKkPPMaaIbgXLnWGfm6+wkOeW7kNrxnM95YOhKUkJUophX69nGUnLsm0A==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.3.0.tgz", + "integrity": "sha512-GO8s+FJpNT0vwt6kr/BZ/B1iB8EgHH/CF590i55Epy3TP2baQHGEHcAnQWvz5067OXIEke7Sa8sUNi0V9FrcJw==", "dependencies": { "chalk": "^4.1.2", "tslib": "^2.4.0" @@ -2198,7 +2175,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -2213,7 +2189,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2229,7 +2204,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2240,14 +2214,12 @@ "node_modules/@docusaurus/logger/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/@docusaurus/logger/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", "engines": { "node": ">=8" } @@ -2256,7 +2228,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2265,15 +2236,14 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.2.0.tgz", - "integrity": "sha512-X2bzo3T0jW0VhUU+XdQofcEeozXOTmKQMvc8tUnWRdTnCvj4XEcBVdC3g+/jftceluiwSTNRAX4VBOJdNt18jA==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.3.0.tgz", + "integrity": "sha512-uxownG7dlg/l19rTIfUP0KDsbI8lTCgziWsdubMcWpGvOgXgm1p4mKSmWPzAwkRENn+un4L8DBhl3j1toeJy1A==", "dependencies": { "@babel/parser": "^7.18.8", "@babel/traverse": "^7.18.8", - "@docusaurus/logger": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/utils": "2.3.0", "@mdx-js/mdx": "^1.6.22", "escape-html": "^1.0.3", "file-loader": "^6.2.0", @@ -2297,12 +2267,12 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.2.0.tgz", - "integrity": "sha512-wDGW4IHKoOr9YuJgy7uYuKWrDrSpsUSDHLZnWQYM9fN7D5EpSmYHjFruUpKWVyxLpD/Wh0rW8hYZwdjJIQUQCQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.3.0.tgz", + "integrity": "sha512-DvJtVejgrgIgxSNZ0pRaVu4EndRVBgbtp1LKvIO4xBgKlrsq8o4qkj1HKwH6yok5NoMqGApu8/E0KPOdZBtDpQ==", "dependencies": { "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/types": "2.2.0", + "@docusaurus/types": "2.3.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2316,18 +2286,17 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.2.0.tgz", - "integrity": "sha512-0mWBinEh0a5J2+8ZJXJXbrCk1tSTNf7Nm4tYAl5h2/xx+PvH/Bnu0V+7mMljYm/1QlDYALNIIaT/JcoZQFUN3w==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.3.0.tgz", + "integrity": "sha512-/v+nWEaqRxH1U4I6uJIMdj8Iilrh0XwIG5vsmsi4AXbpArgqqyfMjbf70lzPOmSdYfdWYgb7tWcA6OhJqyKj0w==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "cheerio": "^1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^10.1.0", @@ -2347,18 +2316,17 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.2.0.tgz", - "integrity": "sha512-BOazBR0XjzsHE+2K1wpNxz5QZmrJgmm3+0Re0EVPYFGW8qndCWGNtXW/0lGKhecVPML8yyFeAmnUCIs7xM2wPw==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.3.0.tgz", + "integrity": "sha512-P53gYvtPY/VJTMdV5pFnKv8d7qMBOPyu/4NPREQU5PWsXJOYedCwNBqdAR7A5P69l55TrzyUEUYLjIcwuoSPGg==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/module-type-aliases": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "@types/react-router-config": "^5.0.6", "combine-promises": "^1.1.0", "fs-extra": "^10.1.0", @@ -2378,16 +2346,15 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.2.0.tgz", - "integrity": "sha512-+OTK3FQHk5WMvdelz8v19PbEbx+CNT6VSpx7nVOvMNs5yJCKvmqBJBQ2ZSxROxhVDYn+CZOlmyrC56NSXzHf6g==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.3.0.tgz", + "integrity": "sha512-H21Ux3Ln+pXlcp0RGdD1fyes7H3tsyhFpeflkxnCoXfTQf/pQB9IMuddFnxuXzj+34rp6jAQmLSaPssuixJXRQ==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "fs-extra": "^10.1.0", "tslib": "^2.4.0", "webpack": "^5.73.0" @@ -2401,14 +2368,13 @@ } }, "node_modules/@docusaurus/plugin-debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.2.0.tgz", - "integrity": "sha512-p9vOep8+7OVl6r/NREEYxf4HMAjV8JMYJ7Bos5fCFO0Wyi9AZEo0sCTliRd7R8+dlJXZEgcngSdxAUo/Q+CJow==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.3.0.tgz", + "integrity": "sha512-TyeH3DMA9/8sIXyX8+zpdLtSixBnLJjW9KSvncKj/iXs1t20tpUZ1WFL7D+G1gxGGbLCBUGDluh738VvsRHC6Q==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", "fs-extra": "^10.1.0", "react-json-view": "^1.21.3", "tslib": "^2.4.0" @@ -2422,14 +2388,13 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.2.0.tgz", - "integrity": "sha512-+eZVVxVeEnV5nVQJdey9ZsfyEVMls6VyWTIj8SmX0k5EbqGvnIfET+J2pYEuKQnDIHxy+syRMoRM6AHXdHYGIg==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.3.0.tgz", + "integrity": "sha512-Z9FqTQzeOC1R6i/x07VgkrTKpQ4OtMe3WBOKZKzgldWXJr6CDUWPSR8pfDEjA+RRAj8ajUh0E+BliKBmFILQvQ==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "tslib": "^2.4.0" }, "engines": { @@ -2441,14 +2406,31 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.2.0.tgz", - "integrity": "sha512-6SOgczP/dYdkqUMGTRqgxAS1eTp6MnJDAQMy8VCF1QKbWZmlkx4agHDexihqmYyCujTYHqDAhm1hV26EET54NQ==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.3.0.tgz", + "integrity": "sha512-oZavqtfwQAGjz+Dyhsb45mVssTevCW1PJgLcmr3WKiID15GTolbBrrp/fueTrEh60DzOd81HbiCLs56JWBwDhQ==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.14" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.3.0.tgz", + "integrity": "sha512-toAhuMX1h+P2CfavwoDlz9s2/Zm7caiEznW/inxq3izywG2l9ujWI/o6u2g70O3ACQ19eHMGHDsyEUcRDPrxBw==", + "dependencies": { + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "tslib": "^2.4.0" }, "engines": { @@ -2460,17 +2442,16 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.2.0.tgz", - "integrity": "sha512-0jAmyRDN/aI265CbWZNZuQpFqiZuo+5otk2MylU9iVrz/4J7gSc+ZJ9cy4EHrEsW7PV8s1w18hIEsmcA1YgkKg==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.3.0.tgz", + "integrity": "sha512-kwIHLP6lyubWOnNO0ejwjqdxB9C6ySnATN61etd6iwxHri5+PBZCEOv1sVm5U1gfQiDR1sVsXnJq2zNwLwgEtQ==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "fs-extra": "^10.1.0", "sitemap": "^7.1.1", "tslib": "^2.4.0" @@ -2484,23 +2465,23 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.2.0.tgz", - "integrity": "sha512-yKIWPGNx7BT8v2wjFIWvYrS+nvN04W+UameSFf8lEiJk6pss0kL6SG2MRvyULiI3BDxH+tj6qe02ncpSPGwumg==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.3.0.tgz", + "integrity": "sha512-mI37ieJe7cs5dHuvWz415U7hO209Q19Fp4iSHeFFgtQoK1PiRg7HJHkVbEsLZII2MivdzGFB5Hxoq2wUPWdNEA==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/plugin-content-blog": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/plugin-content-pages": "2.2.0", - "@docusaurus/plugin-debug": "2.2.0", - "@docusaurus/plugin-google-analytics": "2.2.0", - "@docusaurus/plugin-google-gtag": "2.2.0", - "@docusaurus/plugin-sitemap": "2.2.0", - "@docusaurus/theme-classic": "2.2.0", - "@docusaurus/theme-common": "2.2.0", - "@docusaurus/theme-search-algolia": "2.2.0", - "@docusaurus/types": "2.2.0" + "@docusaurus/core": "2.3.0", + "@docusaurus/plugin-content-blog": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/plugin-content-pages": "2.3.0", + "@docusaurus/plugin-debug": "2.3.0", + "@docusaurus/plugin-google-analytics": "2.3.0", + "@docusaurus/plugin-google-gtag": "2.3.0", + "@docusaurus/plugin-google-tag-manager": "2.3.0", + "@docusaurus/plugin-sitemap": "2.3.0", + "@docusaurus/theme-classic": "2.3.0", + "@docusaurus/theme-common": "2.3.0", + "@docusaurus/theme-search-algolia": "2.3.0", + "@docusaurus/types": "2.3.0" }, "engines": { "node": ">=16.14" @@ -2524,23 +2505,22 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.2.0.tgz", - "integrity": "sha512-kjbg/qJPwZ6H1CU/i9d4l/LcFgnuzeiGgMQlt6yPqKo0SOJIBMPuz7Rnu3r/WWbZFPi//o8acclacOzmXdUUEg==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.3.0.tgz", + "integrity": "sha512-x2h9KZ4feo22b1aArsfqvK05aDCgTkLZGRgAPY/9TevFV5/Yy19cZtBOCbzaKa2dKq1ofBRK9Hm1DdLJdLB14A==", "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/module-type-aliases": "2.2.0", - "@docusaurus/plugin-content-blog": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/plugin-content-pages": "2.2.0", - "@docusaurus/theme-common": "2.2.0", - "@docusaurus/theme-translations": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/plugin-content-blog": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/plugin-content-pages": "2.3.0", + "@docusaurus/theme-common": "2.3.0", + "@docusaurus/theme-translations": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "copy-text-to-clipboard": "^3.0.1", @@ -2564,17 +2544,16 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.2.0.tgz", - "integrity": "sha512-R8BnDjYoN90DCL75gP7qYQfSjyitXuP9TdzgsKDmSFPNyrdE3twtPNa2dIN+h+p/pr+PagfxwWbd6dn722A1Dw==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.3.0.tgz", + "integrity": "sha512-1eAvaULgu6ywHbjkdWOOHl1PdMylne/88i0kg25qimmkMgRHoIQ23JgRD/q5sFr+2YX7U7SggR1UNNsqu2zZPw==", "dependencies": { - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/module-type-aliases": "2.2.0", - "@docusaurus/plugin-content-blog": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/plugin-content-pages": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/plugin-content-blog": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/plugin-content-pages": "2.3.0", + "@docusaurus/utils": "2.3.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2582,6 +2561,7 @@ "parse-numeric-range": "^1.3.0", "prism-react-renderer": "^1.3.5", "tslib": "^2.4.0", + "use-sync-external-store": "^1.2.0", "utility-types": "^3.10.0" }, "engines": { @@ -2593,19 +2573,18 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.2.0.tgz", - "integrity": "sha512-2h38B0tqlxgR2FZ9LpAkGrpDWVdXZ7vltfmTdX+4RsDs3A7khiNsmZB+x/x6sA4+G2V2CvrsPMlsYBy5X+cY1w==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.3.0.tgz", + "integrity": "sha512-/i5k1NAlbYvgnw69vJQA174+ipwdtTCCUvxRp7bVZ+8KmviEybAC/kuKe7WmiUbIGVYbAbwYaEsPuVnsd65DrA==", "dependencies": { "@docsearch/react": "^3.1.1", - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/theme-common": "2.2.0", - "@docusaurus/theme-translations": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/theme-common": "2.3.0", + "@docusaurus/theme-translations": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "algoliasearch": "^4.13.1", "algoliasearch-helper": "^3.10.0", "clsx": "^1.2.1", @@ -2624,10 +2603,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.2.0.tgz", - "integrity": "sha512-3T140AG11OjJrtKlY4pMZ5BzbGRDjNs2co5hJ6uYJG1bVWlhcaFGqkaZ5lCgKflaNHD7UHBHU9Ec5f69jTdd6w==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.3.0.tgz", + "integrity": "sha512-YLVD6LrszBld1EvThTOa9PcblKAZs1jOmRjwtffdg1CGjQWFXEeWUL24n2M4ARByzuLry5D8ZRVmKyRt3LOwsw==", "dependencies": { "fs-extra": "^10.1.0", "tslib": "^2.4.0" @@ -2637,9 +2615,9 @@ } }, "node_modules/@docusaurus/types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.2.0.tgz", - "integrity": "sha512-b6xxyoexfbRNRI8gjblzVOnLr4peCJhGbYGPpJ3LFqpi5nsFfoK4mmDLvWdeah0B7gmJeXabN7nQkFoqeSdmOw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.3.0.tgz", + "integrity": "sha512-c5C0nROxVFsgMAm4vWDB1LDv3v4K18Y8eVxazL3dEr7w+7kNLc5koWrW7fWmCnrbItnuTna4nLS2PcSZrkYidg==", "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", @@ -2656,13 +2634,13 @@ } }, "node_modules/@docusaurus/utils": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.2.0.tgz", - "integrity": "sha512-oNk3cjvx7Tt1Lgh/aeZAmFpGV2pDr5nHKrBVx6hTkzGhrnMuQqLt6UPlQjdYQ3QHXwyF/ZtZMO1D5Pfi0lu7SA==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-6+GCurDsePHHbLM3ktcjv8N4zrjgrl1O7gOQNG4UMktcwHssFFVm+geVcB4M8siOmwUjV2VaNrp0hpGy8DOQHw==", "dependencies": { - "@docusaurus/logger": "2.2.0", + "@docusaurus/logger": "2.3.0", "@svgr/webpack": "^6.2.1", + "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.1.0", "github-slugger": "^1.4.0", @@ -2690,10 +2668,9 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.2.0.tgz", - "integrity": "sha512-qebnerHp+cyovdUseDQyYFvMW1n1nv61zGe5JJfoNQUnjKuApch3IVsz+/lZ9a38pId8kqehC1Ao2bW/s0ntDA==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.3.0.tgz", + "integrity": "sha512-nu5An+26FS7SQTwvyFR4g9lw3NU1u2RLcxJPZF+NCOG8Ne96ciuQosa7+N1kllm/heEJqfTaAUD0sFxpTZrDtw==", "dependencies": { "tslib": "^2.4.0" }, @@ -2710,13 +2687,12 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.2.0.tgz", - "integrity": "sha512-I1hcsG3yoCkasOL5qQAYAfnmVoLei7apugT6m4crQjmDGxq+UkiRrq55UqmDDyZlac/6ax/JC0p+usZ6W4nVyg==", - "license": "MIT", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.3.0.tgz", + "integrity": "sha512-TBJCLqwAoiQQJ6dbgBpuLvzsn/XiTgbZkd6eJFUIQYLb1d473Zv58QrHXVmVQDLWiCgmJpHW2LpMfumTpCDgJw==", "dependencies": { - "@docusaurus/logger": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/utils": "2.3.0", "joi": "^17.6.0", "js-yaml": "^4.1.0", "tslib": "^2.4.0" @@ -2725,6 +2701,17 @@ "node": ">=16.14" } }, + "node_modules/@docusaurus/utils/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -2741,9 +2728,9 @@ } }, "node_modules/@iconify/react": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@iconify/react/-/react-4.0.1.tgz", - "integrity": "sha512-/DBJqh5K7W4f+d4kpvyJa/OTpVa3GfgrE9bZFAKP0vIWDr0cvVU9MVvbbkek216w9nLQhpJY/FeJtc6izB1PHw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-4.1.0.tgz", + "integrity": "sha512-Mf72i3TNNKpKCKxmo7kzqyrUdCgaoljpqtWmtqpqwyxoV4ukhnDsSRNLhf2yBnqGr3cVZsdj/i0FMpXIY0Qk0g==", "dev": true, "dependencies": { "@iconify/types": "^2.0.0" @@ -2831,9 +2818,9 @@ "license": "MIT" }, "node_modules/@loadable/component": { - "version": "5.15.2", - "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.2.tgz", - "integrity": "sha512-ryFAZOX5P2vFkUdzaAtTG88IGnr9qxSdvLRvJySXcUA4B4xVWurUNADu3AnKPksxOZajljqTrDEDcYjeL4lvLw==", + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.3.tgz", + "integrity": "sha512-VOgYgCABn6+/7aGIpg7m0Ruj34tGetaJzt4bQ345FwEovDQZ+dua+NWLmuJKv8rWZyxOUSfoJkmGnzyDXH2BAQ==", "dependencies": { "@babel/runtime": "^7.7.7", "hoist-non-react-statics": "^3.3.1", @@ -2847,14 +2834,13 @@ "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { - "react": ">=16.3.0" + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@mdx-js/mdx": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz", "integrity": "sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==", - "license": "MIT", "dependencies": { "@babel/core": "7.12.9", "@babel/plugin-syntax-jsx": "7.12.1", @@ -2885,7 +2871,6 @@ "version": "7.12.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", @@ -2916,7 +2901,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -2928,7 +2912,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -2937,7 +2920,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -2946,7 +2928,6 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", - "license": "MIT", "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -2977,7 +2958,6 @@ "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz", "integrity": "sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -3408,7 +3388,6 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", - "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -3444,7 +3423,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", - "license": "MIT", "dependencies": { "@types/unist": "*" } @@ -3468,8 +3446,7 @@ "node_modules/@types/parse5": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", - "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==", - "license": "MIT" + "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -3538,7 +3515,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz", "integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==", - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -3924,32 +3900,30 @@ } }, "node_modules/algoliasearch": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.14.2.tgz", - "integrity": "sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==", - "license": "MIT", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.14.3.tgz", + "integrity": "sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg==", "dependencies": { - "@algolia/cache-browser-local-storage": "4.14.2", - "@algolia/cache-common": "4.14.2", - "@algolia/cache-in-memory": "4.14.2", - "@algolia/client-account": "4.14.2", - "@algolia/client-analytics": "4.14.2", - "@algolia/client-common": "4.14.2", - "@algolia/client-personalization": "4.14.2", - "@algolia/client-search": "4.14.2", - "@algolia/logger-common": "4.14.2", - "@algolia/logger-console": "4.14.2", - "@algolia/requester-browser-xhr": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/requester-node-http": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/cache-browser-local-storage": "4.14.3", + "@algolia/cache-common": "4.14.3", + "@algolia/cache-in-memory": "4.14.3", + "@algolia/client-account": "4.14.3", + "@algolia/client-analytics": "4.14.3", + "@algolia/client-common": "4.14.3", + "@algolia/client-personalization": "4.14.3", + "@algolia/client-search": "4.14.3", + "@algolia/logger-common": "4.14.3", + "@algolia/logger-console": "4.14.3", + "@algolia/requester-browser-xhr": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/requester-node-http": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "node_modules/algoliasearch-helper": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.1.tgz", - "integrity": "sha512-mvsPN3eK4E0bZG0/WlWJjeqe/bUD2KOEVOl0GyL/TGXn6wcpZU8NOuztGHCUKXkyg5gq6YzUakVTmnmSSO5Yiw==", - "license": "MIT", + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.3.tgz", + "integrity": "sha512-TbaEvLwiuGygHQIB8y+OsJKQQ40+JKUua5B91X66tMUHyyhbNHvqyr0lqd3wCoyKx7WybyQrC0WJvzoIeh24Aw==", "dependencies": { "@algolia/events": "^4.0.1" }, @@ -4046,8 +4020,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "2.1.2", @@ -4067,8 +4040,7 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -4142,7 +4114,6 @@ "version": "1.6.22", "resolved": "https://registry.npmjs.org/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz", "integrity": "sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==", - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "7.10.4", "@mdx-js/util": "1.6.22" @@ -4158,8 +4129,7 @@ "node_modules/babel-plugin-apply-mdx-type-prop/node_modules/@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "license": "MIT" + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" }, "node_modules/babel-plugin-dynamic-import-node": { "version": "2.3.3", @@ -4174,7 +4144,6 @@ "version": "1.6.22", "resolved": "https://registry.npmjs.org/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz", "integrity": "sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==", - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "7.10.4" }, @@ -4186,8 +4155,7 @@ "node_modules/babel-plugin-extract-import-names/node_modules/@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "license": "MIT" + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.2", @@ -4235,7 +4203,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4250,8 +4217,7 @@ "node_modules/base16": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==", - "license": "MIT" + "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==" }, "node_modules/batch": { "version": "0.6.1", @@ -4630,7 +4596,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4654,7 +4619,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4664,7 +4628,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4674,7 +4637,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4718,7 +4680,6 @@ "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", @@ -4739,7 +4700,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", @@ -4756,7 +4716,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -4772,7 +4731,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -4786,7 +4744,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -4801,7 +4758,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -4815,7 +4771,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -4827,7 +4782,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -4841,7 +4795,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -4856,7 +4809,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -4870,7 +4822,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -4889,7 +4840,6 @@ "url": "https://github.com/sponsors/fb55" } ], - "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -5042,7 +4992,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5088,7 +5037,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5245,7 +5193,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz", "integrity": "sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==", - "license": "MIT", "engines": { "node": ">=12" }, @@ -5432,7 +5379,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "license": "MIT", "dependencies": { "node-fetch": "2.6.7" } @@ -5659,7 +5605,6 @@ "version": "5.3.9", "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.9.tgz", "integrity": "sha512-njnh4pp1xCsibJcEHnWZb4EEzni0ePMqPuPNyuWT4Z+YeXmsgqNuTPIljXFEXhxGsWs9183JkXgHxc1TcsahIg==", - "license": "MIT", "dependencies": { "autoprefixer": "^10.4.12", "cssnano-preset-default": "^5.2.13", @@ -6300,7 +6245,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.4.tgz", "integrity": "sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==", - "license": "MIT", "dependencies": { "repeat-string": "^1.5.4" }, @@ -6616,7 +6560,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-3.2.0.tgz", "integrity": "sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -6728,7 +6671,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -6970,14 +6912,12 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -7046,7 +6986,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", - "license": "BSD-3-Clause", "dependencies": { "fbjs": "^3.0.0" } @@ -7055,7 +6994,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz", "integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==", - "license": "MIT", "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", @@ -7069,8 +7007,7 @@ "node_modules/fbjs-css-vars": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "license": "MIT" + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" }, "node_modules/feather-icons": { "version": "4.29.0", @@ -7085,7 +7022,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", - "license": "MIT", "dependencies": { "xml-js": "^1.6.11" }, @@ -7219,7 +7155,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.3.tgz", "integrity": "sha512-yKAbrp7JhZhj6uiT1FTuVMlIAT1J4jqEyBpFApi1kxpGZCvacMVc/t1pMQyotqHhAgvoE3bNvAykhCo2CLjnYw==", - "license": "BSD-3-Clause", "dependencies": { "fbemitter": "^3.0.0", "fbjs": "^3.0.1" @@ -7500,8 +7435,7 @@ "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "license": "ISC" + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" }, "node_modules/get-stream": { "version": "4.1.0", @@ -7516,8 +7450,9 @@ } }, "node_modules/github-slugger": { - "version": "1.4.0", - "license": "ISC" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" }, "node_modules/glob": { "version": "7.2.2", @@ -7676,7 +7611,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", @@ -7691,7 +7625,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -7700,7 +7633,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -7786,7 +7718,6 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", "integrity": "sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==", - "license": "MIT", "dependencies": { "@types/unist": "^2.0.3", "comma-separated-tokens": "^1.0.0", @@ -7805,7 +7736,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz", "integrity": "sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==", - "license": "MIT", "dependencies": { "@types/parse5": "^5.0.0", "hastscript": "^6.0.0", @@ -7823,7 +7753,6 @@ "version": "2.2.5", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7833,7 +7762,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-6.0.1.tgz", "integrity": "sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==", - "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", "hast-util-from-parse5": "^6.0.0", @@ -7854,14 +7782,12 @@ "node_modules/hast-util-raw/node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, "node_modules/hast-util-to-parse5": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz", "integrity": "sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==", - "license": "MIT", "dependencies": { "hast-to-hyperscript": "^9.0.0", "property-information": "^5.0.0", @@ -7878,7 +7804,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", - "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", @@ -8017,7 +7942,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz", "integrity": "sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8204,7 +8128,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", - "license": "MIT", "dependencies": { "queue": "6.0.2" }, @@ -8276,7 +8199,6 @@ "version": "0.2.0-alpha.42", "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.42.tgz", "integrity": "sha512-ift8OXNbQQwtbIt6z16KnSWP7uJ/SysSMFI4F87MNRTicypfl4Pv3E2OGVv6N3nSZFJvA8imYulCBS64iyHYww==", - "license": "MIT", "engines": { "node": ">=12" } @@ -8306,8 +8228,7 @@ "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", - "license": "MIT" + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, "node_modules/internmap": { "version": "2.0.3", @@ -8348,7 +8269,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8358,7 +8278,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -8404,7 +8323,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "engines": { "node": ">=4" } @@ -8435,7 +8353,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8460,7 +8377,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8499,7 +8415,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8546,7 +8461,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8573,7 +8487,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "license": "MIT", "engines": { "node": ">=8" } @@ -8594,7 +8507,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8630,7 +8542,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8640,7 +8551,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8752,7 +8662,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -8938,8 +8847,7 @@ "node_modules/lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", - "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==", - "license": "MIT" + "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -8950,8 +8858,7 @@ "node_modules/lodash.flow": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", - "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==", - "license": "MIT" + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -9035,7 +8942,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -9045,7 +8951,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz", "integrity": "sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==", - "license": "MIT", "dependencies": { "unist-util-remove": "^2.0.0" }, @@ -9058,7 +8963,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", - "license": "MIT", "dependencies": { "unist-util-visit": "^2.0.0" }, @@ -9071,7 +8975,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz", "integrity": "sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==", - "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -9091,7 +8994,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -9106,8 +9008,7 @@ "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "license": "MIT" + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/mdx-mermaid": { "version": "1.3.2", @@ -9443,7 +9344,6 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "license": "MIT", "dependencies": { "lodash": "^4.17.21" } @@ -9452,7 +9352,6 @@ "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9533,8 +9432,7 @@ "node_modules/nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "license": "MIT" + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==" }, "node_modules/nth-check": { "version": "2.0.1", @@ -9797,7 +9695,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "license": "MIT", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -9832,14 +9729,12 @@ "node_modules/parse-numeric-range": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", - "license": "ISC" + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" }, "node_modules/parse5": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.1.tgz", - "integrity": "sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==", - "license": "MIT", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dependencies": { "entities": "^4.4.0" }, @@ -9851,7 +9746,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", - "license": "MIT", "dependencies": { "domhandler": "^5.0.2", "parse5": "^7.0.0" @@ -9864,7 +9758,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -9879,7 +9772,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -10197,7 +10089,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz", "integrity": "sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw==", - "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.5" }, @@ -10297,7 +10188,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz", "integrity": "sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw==", - "license": "MIT", "dependencies": { "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" @@ -10639,7 +10529,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz", "integrity": "sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg==", - "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -10698,7 +10587,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-4.3.0.tgz", "integrity": "sha512-jAl8gJM2DvuIJiI9sL1CuiHtKM4s5aEIomkU8G3LFvbP+p8i7Sz8VV63uieTgoewGqKbi+hxBTiOKJlB35upCg==", - "license": "MIT", "dependencies": { "sort-css-media-queries": "2.1.0" }, @@ -10750,7 +10638,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-5.1.0.tgz", "integrity": "sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==", - "license": "MIT", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -10799,7 +10686,6 @@ "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", - "license": "MIT", "engines": { "node": ">=6" } @@ -10814,7 +10700,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "license": "MIT", "dependencies": { "asap": "~2.0.3" } @@ -10847,7 +10732,6 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", "dependencies": { "xtend": "^4.0.0" }, @@ -10909,8 +10793,7 @@ "node_modules/pure-color": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", - "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==", - "license": "MIT" + "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==" }, "node_modules/qs": { "version": "6.10.3", @@ -10929,7 +10812,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", "dependencies": { "inherits": "~2.0.3" } @@ -11048,7 +10930,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==", - "license": "MIT", "dependencies": { "base16": "^1.0.0", "lodash.curry": "^4.0.1", @@ -11295,7 +11176,6 @@ "version": "1.21.3", "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz", "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==", - "license": "MIT", "dependencies": { "flux": "^4.0.1", "react-base16-styling": "^0.6.0", @@ -11310,8 +11190,7 @@ "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "node_modules/react-loadable": { "name": "@docusaurus/react-loadable", @@ -11392,8 +11271,9 @@ } }, "node_modules/react-textarea-autosize": { - "version": "8.3.4", - "license": "MIT", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.0.tgz", + "integrity": "sha512-YrTFaEHLgJsi8sJVYHBzYn+mkP3prGkmP2DKb/tm0t7CLJY5t1Rxix8070LAKb0wby7bl/lf2EeHkuMihMZMwQ==", "dependencies": { "@babel/runtime": "^7.10.2", "use-composed-ref": "^1.3.0", @@ -11444,8 +11324,7 @@ "node_modules/reading-time": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==" }, "node_modules/rechoir": { "version": "0.6.2", @@ -11568,7 +11447,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-2.2.0.tgz", "integrity": "sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w==", - "license": "MIT", "dependencies": { "emoticon": "^3.2.0", "node-emoji": "^1.10.0", @@ -11579,7 +11457,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-2.0.0.tgz", "integrity": "sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -11589,7 +11466,6 @@ "version": "1.6.22", "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-1.6.22.tgz", "integrity": "sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==", - "license": "MIT", "dependencies": { "@babel/core": "7.12.9", "@babel/helper-plugin-utils": "7.10.4", @@ -11609,7 +11485,6 @@ "version": "7.12.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", @@ -11639,14 +11514,12 @@ "node_modules/remark-mdx/node_modules/@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", - "license": "MIT" + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" }, "node_modules/remark-mdx/node_modules/@babel/plugin-proposal-object-rest-spread": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", @@ -11660,7 +11533,6 @@ "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -11672,7 +11544,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -11681,7 +11552,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -11690,7 +11560,6 @@ "version": "9.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", - "license": "MIT", "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -11708,7 +11577,6 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.3.tgz", "integrity": "sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==", - "license": "MIT", "dependencies": { "ccount": "^1.0.0", "collapse-white-space": "^1.0.2", @@ -11736,7 +11604,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz", "integrity": "sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==", - "license": "MIT", "dependencies": { "mdast-squeeze-paragraphs": "^4.0.0" }, @@ -11762,7 +11629,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", "engines": { "node": ">=0.10" } @@ -11880,7 +11746,6 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz", "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==", - "license": "MIT", "dependencies": { "find-up": "^5.0.0", "picocolors": "^1.0.0", @@ -11895,7 +11760,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -11911,7 +11775,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -11926,7 +11789,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -11941,7 +11803,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -12071,8 +11932,7 @@ "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "license": "ISC" + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/scheduler": { "version": "0.20.2", @@ -12106,7 +11966,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "license": "MIT", "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" @@ -12345,8 +12204,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -12458,7 +12316,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz", "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==", - "license": "MIT", "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", @@ -12497,7 +12354,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz", "integrity": "sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==", - "license": "MIT", "engines": { "node": ">= 6.3.0" } @@ -12534,7 +12390,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12573,8 +12428,7 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/stable": { "version": "0.1.8", @@ -12586,7 +12440,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12682,7 +12535,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "license": "BSD-2-Clause", "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", @@ -12708,7 +12560,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12726,7 +12577,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", "engines": { "node": ">=8" }, @@ -12738,7 +12588,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", - "license": "MIT", "dependencies": { "inline-style-parser": "0.1.1" } @@ -13035,8 +12884,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/trim": { "version": "0.0.1", @@ -13047,7 +12895,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13057,7 +12904,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13241,7 +13087,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "license": "MIT", "dependencies": { "inherits": "^2.0.0", "xtend": "^4.0.0" @@ -13291,7 +13136,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", - "license": "MIT", "dependencies": { "bail": "^1.0.0", "extend": "^3.0.0", @@ -13321,7 +13165,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -13331,7 +13174,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -13351,7 +13193,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -13361,7 +13202,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-2.1.0.tgz", "integrity": "sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==", - "license": "MIT", "dependencies": { "unist-util-is": "^4.0.0" }, @@ -13374,7 +13214,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz", "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==", - "license": "MIT", "dependencies": { "unist-util-visit": "^2.0.0" }, @@ -13387,7 +13226,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "license": "MIT", "dependencies": { "@types/unist": "^2.0.2" }, @@ -13760,7 +13598,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", - "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -13769,7 +13606,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", - "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, @@ -13783,7 +13619,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", - "license": "MIT", "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, @@ -13796,6 +13631,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13812,7 +13655,6 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", - "license": "MIT", "engines": { "node": ">= 4" } @@ -13854,7 +13696,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", - "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", @@ -13870,7 +13711,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz", "integrity": "sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -13880,7 +13720,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" @@ -13940,7 +13779,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -13949,8 +13787,7 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { "version": "5.74.0", @@ -14500,7 +14337,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -14652,7 +14488,6 @@ "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "license": "MIT", "dependencies": { "sax": "^1.2.4" }, @@ -14700,7 +14535,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -14709,95 +14543,95 @@ }, "dependencies": { "@algolia/autocomplete-core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.2.tgz", - "integrity": "sha512-eclwUDC6qfApNnEfu1uWcL/rudQsn59tjEoUYZYE2JSXZrHLRjBUGMxiCoknobU2Pva8ejb0eRxpIYDtVVqdsw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz", + "integrity": "sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg==", "requires": { - "@algolia/autocomplete-shared": "1.7.2" + "@algolia/autocomplete-shared": "1.7.4" } }, "@algolia/autocomplete-preset-algolia": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.2.tgz", - "integrity": "sha512-+RYEG6B0QiGGfRb2G3MtPfyrl0dALF3cQNTWBzBX6p5o01vCCGTTinAm2UKG3tfc2CnOMAtnPLkzNZyJUpnVJw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz", + "integrity": "sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ==", "requires": { - "@algolia/autocomplete-shared": "1.7.2" + "@algolia/autocomplete-shared": "1.7.4" } }, "@algolia/autocomplete-shared": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.2.tgz", - "integrity": "sha512-QCckjiC7xXHIUaIL3ektBtjJ0w7tTA3iqKcAE/Hjn1lZ5omp7i3Y4e09rAr9ZybqirL7AbxCLLq0Ra5DDPKeug==" + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz", + "integrity": "sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg==" }, "@algolia/cache-browser-local-storage": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.2.tgz", - "integrity": "sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz", + "integrity": "sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw==", "requires": { - "@algolia/cache-common": "4.14.2" + "@algolia/cache-common": "4.14.3" } }, "@algolia/cache-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.14.2.tgz", - "integrity": "sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==" + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.14.3.tgz", + "integrity": "sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q==" }, "@algolia/cache-in-memory": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.14.2.tgz", - "integrity": "sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz", + "integrity": "sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg==", "requires": { - "@algolia/cache-common": "4.14.2" + "@algolia/cache-common": "4.14.3" } }, "@algolia/client-account": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.14.2.tgz", - "integrity": "sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.14.3.tgz", + "integrity": "sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ==", "requires": { - "@algolia/client-common": "4.14.2", - "@algolia/client-search": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/client-search": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "@algolia/client-analytics": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.14.2.tgz", - "integrity": "sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.14.3.tgz", + "integrity": "sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw==", "requires": { - "@algolia/client-common": "4.14.2", - "@algolia/client-search": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/client-search": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "@algolia/client-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.14.2.tgz", - "integrity": "sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.14.3.tgz", + "integrity": "sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw==", "requires": { - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "@algolia/client-personalization": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.14.2.tgz", - "integrity": "sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.14.3.tgz", + "integrity": "sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg==", "requires": { - "@algolia/client-common": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "@algolia/client-search": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.14.2.tgz", - "integrity": "sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.14.3.tgz", + "integrity": "sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A==", "requires": { - "@algolia/client-common": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/client-common": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "@algolia/events": { @@ -14806,47 +14640,47 @@ "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==" }, "@algolia/logger-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.14.2.tgz", - "integrity": "sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==" + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.14.3.tgz", + "integrity": "sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw==" }, "@algolia/logger-console": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.14.2.tgz", - "integrity": "sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.14.3.tgz", + "integrity": "sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw==", "requires": { - "@algolia/logger-common": "4.14.2" + "@algolia/logger-common": "4.14.3" } }, "@algolia/requester-browser-xhr": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.2.tgz", - "integrity": "sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz", + "integrity": "sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q==", "requires": { - "@algolia/requester-common": "4.14.2" + "@algolia/requester-common": "4.14.3" } }, "@algolia/requester-common": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.14.2.tgz", - "integrity": "sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==" + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.14.3.tgz", + "integrity": "sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw==" }, "@algolia/requester-node-http": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.14.2.tgz", - "integrity": "sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz", + "integrity": "sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA==", "requires": { - "@algolia/requester-common": "4.14.2" + "@algolia/requester-common": "4.14.3" } }, "@algolia/transporter": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.14.2.tgz", - "integrity": "sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.14.3.tgz", + "integrity": "sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w==", "requires": { - "@algolia/cache-common": "4.14.2", - "@algolia/logger-common": "4.14.2", - "@algolia/requester-common": "4.14.2" + "@algolia/cache-common": "4.14.3", + "@algolia/logger-common": "4.14.3", + "@algolia/requester-common": "4.14.3" } }, "@ampproject/remapping": { @@ -15937,25 +15771,25 @@ "optional": true }, "@docsearch/css": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.3.0.tgz", - "integrity": "sha512-rODCdDtGyudLj+Va8b6w6Y85KE85bXRsps/R4Yjwt5vueXKXZQKYw0aA9knxLBT6a/bI/GMrAcmCR75KYOM6hg==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.3.2.tgz", + "integrity": "sha512-dctFYiwbvDZkksMlsmc7pj6W6By/EjnVXJq5TEPd05MwQe+dcdHJgaIn1c8wfsucxHpIsdrUcgSkACHCq6aIhw==" }, "@docsearch/react": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.3.0.tgz", - "integrity": "sha512-fhS5adZkae2SSdMYEMVg6pxI5a/cE+tW16ki1V0/ur4Fdok3hBRkmN/H8VvlXnxzggkQIIRIVvYPn00JPjen3A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.3.2.tgz", + "integrity": "sha512-ugILab2TYKSh6IEHf6Z9xZbOovsYbsdfo60PBj+Bw+oMJ1MHJ7pBt1TTcmPki1hSgg8mysgKy2hDiVdPm7XWSQ==", "requires": { - "@algolia/autocomplete-core": "1.7.2", - "@algolia/autocomplete-preset-algolia": "1.7.2", - "@docsearch/css": "3.3.0", + "@algolia/autocomplete-core": "1.7.4", + "@algolia/autocomplete-preset-algolia": "1.7.4", + "@docsearch/css": "3.3.2", "algoliasearch": "^4.0.0" } }, "@docusaurus/core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.2.0.tgz", - "integrity": "sha512-Vd6XOluKQqzG12fEs9prJgDtyn6DPok9vmUWDR2E6/nV5Fl9SVkhEQOBxwObjk3kQh7OY7vguFaLh0jqdApWsA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-2.3.0.tgz", + "integrity": "sha512-2AU5HfKyExO+/mi41SBnx5uY0aGZFXr3D93wntBY4lN1gsDKUpi7EE4lPBAXm9CoH4Pw6N24yDHy9CPR3sh/uA==", "requires": { "@babel/core": "^7.18.6", "@babel/generator": "^7.18.7", @@ -15967,13 +15801,13 @@ "@babel/runtime": "^7.18.6", "@babel/runtime-corejs3": "^7.18.6", "@babel/traverse": "^7.18.8", - "@docusaurus/cssnano-preset": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", + "@docusaurus/cssnano-preset": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "@slorber/static-site-generator-webpack-plugin": "^4.0.7", "@svgr/webpack": "^6.2.1", "autoprefixer": "^10.4.7", @@ -16076,9 +15910,9 @@ } }, "@docusaurus/cssnano-preset": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.2.0.tgz", - "integrity": "sha512-mAAwCo4n66TMWBH1kXnHVZsakW9VAXJzTO4yZukuL3ro4F+JtkMwKfh42EG75K/J/YIFQG5I/Bzy0UH/hFxaTg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-2.3.0.tgz", + "integrity": "sha512-igmsXc1Q95lMeq07A1xua0/5wOPygDQ/ENSV7VVbiGhnvMv4gzkba8ZvbAtc7PmqK+kpYRfPzNCOk0GnQCvibg==", "requires": { "cssnano-preset-advanced": "^5.3.8", "postcss": "^8.4.14", @@ -16087,9 +15921,9 @@ } }, "@docusaurus/logger": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.2.0.tgz", - "integrity": "sha512-DF3j1cA5y2nNsu/vk8AG7xwpZu6f5MKkPPMaaIbgXLnWGfm6+wkOeW7kNrxnM95YOhKUkJUophX69nGUnLsm0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-2.3.0.tgz", + "integrity": "sha512-GO8s+FJpNT0vwt6kr/BZ/B1iB8EgHH/CF590i55Epy3TP2baQHGEHcAnQWvz5067OXIEke7Sa8sUNi0V9FrcJw==", "requires": { "chalk": "^4.1.2", "tslib": "^2.4.0" @@ -16141,14 +15975,14 @@ } }, "@docusaurus/mdx-loader": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.2.0.tgz", - "integrity": "sha512-X2bzo3T0jW0VhUU+XdQofcEeozXOTmKQMvc8tUnWRdTnCvj4XEcBVdC3g+/jftceluiwSTNRAX4VBOJdNt18jA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-2.3.0.tgz", + "integrity": "sha512-uxownG7dlg/l19rTIfUP0KDsbI8lTCgziWsdubMcWpGvOgXgm1p4mKSmWPzAwkRENn+un4L8DBhl3j1toeJy1A==", "requires": { "@babel/parser": "^7.18.8", "@babel/traverse": "^7.18.8", - "@docusaurus/logger": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/utils": "2.3.0", "@mdx-js/mdx": "^1.6.22", "escape-html": "^1.0.3", "file-loader": "^6.2.0", @@ -16165,12 +15999,12 @@ } }, "@docusaurus/module-type-aliases": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.2.0.tgz", - "integrity": "sha512-wDGW4IHKoOr9YuJgy7uYuKWrDrSpsUSDHLZnWQYM9fN7D5EpSmYHjFruUpKWVyxLpD/Wh0rW8hYZwdjJIQUQCQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-2.3.0.tgz", + "integrity": "sha512-DvJtVejgrgIgxSNZ0pRaVu4EndRVBgbtp1LKvIO4xBgKlrsq8o4qkj1HKwH6yok5NoMqGApu8/E0KPOdZBtDpQ==", "requires": { "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/types": "2.2.0", + "@docusaurus/types": "2.3.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -16180,17 +16014,17 @@ } }, "@docusaurus/plugin-content-blog": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.2.0.tgz", - "integrity": "sha512-0mWBinEh0a5J2+8ZJXJXbrCk1tSTNf7Nm4tYAl5h2/xx+PvH/Bnu0V+7mMljYm/1QlDYALNIIaT/JcoZQFUN3w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.3.0.tgz", + "integrity": "sha512-/v+nWEaqRxH1U4I6uJIMdj8Iilrh0XwIG5vsmsi4AXbpArgqqyfMjbf70lzPOmSdYfdWYgb7tWcA6OhJqyKj0w==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "cheerio": "^1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^10.1.0", @@ -16203,17 +16037,17 @@ } }, "@docusaurus/plugin-content-docs": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.2.0.tgz", - "integrity": "sha512-BOazBR0XjzsHE+2K1wpNxz5QZmrJgmm3+0Re0EVPYFGW8qndCWGNtXW/0lGKhecVPML8yyFeAmnUCIs7xM2wPw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.3.0.tgz", + "integrity": "sha512-P53gYvtPY/VJTMdV5pFnKv8d7qMBOPyu/4NPREQU5PWsXJOYedCwNBqdAR7A5P69l55TrzyUEUYLjIcwuoSPGg==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/module-type-aliases": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "@types/react-router-config": "^5.0.6", "combine-promises": "^1.1.0", "fs-extra": "^10.1.0", @@ -16226,88 +16060,100 @@ } }, "@docusaurus/plugin-content-pages": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.2.0.tgz", - "integrity": "sha512-+OTK3FQHk5WMvdelz8v19PbEbx+CNT6VSpx7nVOvMNs5yJCKvmqBJBQ2ZSxROxhVDYn+CZOlmyrC56NSXzHf6g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.3.0.tgz", + "integrity": "sha512-H21Ux3Ln+pXlcp0RGdD1fyes7H3tsyhFpeflkxnCoXfTQf/pQB9IMuddFnxuXzj+34rp6jAQmLSaPssuixJXRQ==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "fs-extra": "^10.1.0", "tslib": "^2.4.0", "webpack": "^5.73.0" } }, "@docusaurus/plugin-debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.2.0.tgz", - "integrity": "sha512-p9vOep8+7OVl6r/NREEYxf4HMAjV8JMYJ7Bos5fCFO0Wyi9AZEo0sCTliRd7R8+dlJXZEgcngSdxAUo/Q+CJow==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-2.3.0.tgz", + "integrity": "sha512-TyeH3DMA9/8sIXyX8+zpdLtSixBnLJjW9KSvncKj/iXs1t20tpUZ1WFL7D+G1gxGGbLCBUGDluh738VvsRHC6Q==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", "fs-extra": "^10.1.0", "react-json-view": "^1.21.3", "tslib": "^2.4.0" } }, "@docusaurus/plugin-google-analytics": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.2.0.tgz", - "integrity": "sha512-+eZVVxVeEnV5nVQJdey9ZsfyEVMls6VyWTIj8SmX0k5EbqGvnIfET+J2pYEuKQnDIHxy+syRMoRM6AHXdHYGIg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.3.0.tgz", + "integrity": "sha512-Z9FqTQzeOC1R6i/x07VgkrTKpQ4OtMe3WBOKZKzgldWXJr6CDUWPSR8pfDEjA+RRAj8ajUh0E+BliKBmFILQvQ==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "tslib": "^2.4.0" } }, "@docusaurus/plugin-google-gtag": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.2.0.tgz", - "integrity": "sha512-6SOgczP/dYdkqUMGTRqgxAS1eTp6MnJDAQMy8VCF1QKbWZmlkx4agHDexihqmYyCujTYHqDAhm1hV26EET54NQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.3.0.tgz", + "integrity": "sha512-oZavqtfwQAGjz+Dyhsb45mVssTevCW1PJgLcmr3WKiID15GTolbBrrp/fueTrEh60DzOd81HbiCLs56JWBwDhQ==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", + "tslib": "^2.4.0" + } + }, + "@docusaurus/plugin-google-tag-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.3.0.tgz", + "integrity": "sha512-toAhuMX1h+P2CfavwoDlz9s2/Zm7caiEznW/inxq3izywG2l9ujWI/o6u2g70O3ACQ19eHMGHDsyEUcRDPrxBw==", + "requires": { + "@docusaurus/core": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "tslib": "^2.4.0" } }, "@docusaurus/plugin-sitemap": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.2.0.tgz", - "integrity": "sha512-0jAmyRDN/aI265CbWZNZuQpFqiZuo+5otk2MylU9iVrz/4J7gSc+ZJ9cy4EHrEsW7PV8s1w18hIEsmcA1YgkKg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.3.0.tgz", + "integrity": "sha512-kwIHLP6lyubWOnNO0ejwjqdxB9C6ySnATN61etd6iwxHri5+PBZCEOv1sVm5U1gfQiDR1sVsXnJq2zNwLwgEtQ==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "fs-extra": "^10.1.0", "sitemap": "^7.1.1", "tslib": "^2.4.0" } }, "@docusaurus/preset-classic": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.2.0.tgz", - "integrity": "sha512-yKIWPGNx7BT8v2wjFIWvYrS+nvN04W+UameSFf8lEiJk6pss0kL6SG2MRvyULiI3BDxH+tj6qe02ncpSPGwumg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.3.0.tgz", + "integrity": "sha512-mI37ieJe7cs5dHuvWz415U7hO209Q19Fp4iSHeFFgtQoK1PiRg7HJHkVbEsLZII2MivdzGFB5Hxoq2wUPWdNEA==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/plugin-content-blog": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/plugin-content-pages": "2.2.0", - "@docusaurus/plugin-debug": "2.2.0", - "@docusaurus/plugin-google-analytics": "2.2.0", - "@docusaurus/plugin-google-gtag": "2.2.0", - "@docusaurus/plugin-sitemap": "2.2.0", - "@docusaurus/theme-classic": "2.2.0", - "@docusaurus/theme-common": "2.2.0", - "@docusaurus/theme-search-algolia": "2.2.0", - "@docusaurus/types": "2.2.0" + "@docusaurus/core": "2.3.0", + "@docusaurus/plugin-content-blog": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/plugin-content-pages": "2.3.0", + "@docusaurus/plugin-debug": "2.3.0", + "@docusaurus/plugin-google-analytics": "2.3.0", + "@docusaurus/plugin-google-gtag": "2.3.0", + "@docusaurus/plugin-google-tag-manager": "2.3.0", + "@docusaurus/plugin-sitemap": "2.3.0", + "@docusaurus/theme-classic": "2.3.0", + "@docusaurus/theme-common": "2.3.0", + "@docusaurus/theme-search-algolia": "2.3.0", + "@docusaurus/types": "2.3.0" } }, "@docusaurus/react-loadable": { @@ -16320,22 +16166,22 @@ } }, "@docusaurus/theme-classic": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.2.0.tgz", - "integrity": "sha512-kjbg/qJPwZ6H1CU/i9d4l/LcFgnuzeiGgMQlt6yPqKo0SOJIBMPuz7Rnu3r/WWbZFPi//o8acclacOzmXdUUEg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-2.3.0.tgz", + "integrity": "sha512-x2h9KZ4feo22b1aArsfqvK05aDCgTkLZGRgAPY/9TevFV5/Yy19cZtBOCbzaKa2dKq1ofBRK9Hm1DdLJdLB14A==", "requires": { - "@docusaurus/core": "2.2.0", - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/module-type-aliases": "2.2.0", - "@docusaurus/plugin-content-blog": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/plugin-content-pages": "2.2.0", - "@docusaurus/theme-common": "2.2.0", - "@docusaurus/theme-translations": "2.2.0", - "@docusaurus/types": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-common": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/plugin-content-blog": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/plugin-content-pages": "2.3.0", + "@docusaurus/theme-common": "2.3.0", + "@docusaurus/theme-translations": "2.3.0", + "@docusaurus/types": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-common": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "copy-text-to-clipboard": "^3.0.1", @@ -16352,16 +16198,16 @@ } }, "@docusaurus/theme-common": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.2.0.tgz", - "integrity": "sha512-R8BnDjYoN90DCL75gP7qYQfSjyitXuP9TdzgsKDmSFPNyrdE3twtPNa2dIN+h+p/pr+PagfxwWbd6dn722A1Dw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.3.0.tgz", + "integrity": "sha512-1eAvaULgu6ywHbjkdWOOHl1PdMylne/88i0kg25qimmkMgRHoIQ23JgRD/q5sFr+2YX7U7SggR1UNNsqu2zZPw==", "requires": { - "@docusaurus/mdx-loader": "2.2.0", - "@docusaurus/module-type-aliases": "2.2.0", - "@docusaurus/plugin-content-blog": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/plugin-content-pages": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/mdx-loader": "2.3.0", + "@docusaurus/module-type-aliases": "2.3.0", + "@docusaurus/plugin-content-blog": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/plugin-content-pages": "2.3.0", + "@docusaurus/utils": "2.3.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -16369,22 +16215,23 @@ "parse-numeric-range": "^1.3.0", "prism-react-renderer": "^1.3.5", "tslib": "^2.4.0", + "use-sync-external-store": "^1.2.0", "utility-types": "^3.10.0" } }, "@docusaurus/theme-search-algolia": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.2.0.tgz", - "integrity": "sha512-2h38B0tqlxgR2FZ9LpAkGrpDWVdXZ7vltfmTdX+4RsDs3A7khiNsmZB+x/x6sA4+G2V2CvrsPMlsYBy5X+cY1w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.3.0.tgz", + "integrity": "sha512-/i5k1NAlbYvgnw69vJQA174+ipwdtTCCUvxRp7bVZ+8KmviEybAC/kuKe7WmiUbIGVYbAbwYaEsPuVnsd65DrA==", "requires": { "@docsearch/react": "^3.1.1", - "@docusaurus/core": "2.2.0", - "@docusaurus/logger": "2.2.0", - "@docusaurus/plugin-content-docs": "2.2.0", - "@docusaurus/theme-common": "2.2.0", - "@docusaurus/theme-translations": "2.2.0", - "@docusaurus/utils": "2.2.0", - "@docusaurus/utils-validation": "2.2.0", + "@docusaurus/core": "2.3.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/plugin-content-docs": "2.3.0", + "@docusaurus/theme-common": "2.3.0", + "@docusaurus/theme-translations": "2.3.0", + "@docusaurus/utils": "2.3.0", + "@docusaurus/utils-validation": "2.3.0", "algoliasearch": "^4.13.1", "algoliasearch-helper": "^3.10.0", "clsx": "^1.2.1", @@ -16396,18 +16243,18 @@ } }, "@docusaurus/theme-translations": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.2.0.tgz", - "integrity": "sha512-3T140AG11OjJrtKlY4pMZ5BzbGRDjNs2co5hJ6uYJG1bVWlhcaFGqkaZ5lCgKflaNHD7UHBHU9Ec5f69jTdd6w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.3.0.tgz", + "integrity": "sha512-YLVD6LrszBld1EvThTOa9PcblKAZs1jOmRjwtffdg1CGjQWFXEeWUL24n2M4ARByzuLry5D8ZRVmKyRt3LOwsw==", "requires": { "fs-extra": "^10.1.0", "tslib": "^2.4.0" } }, "@docusaurus/types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.2.0.tgz", - "integrity": "sha512-b6xxyoexfbRNRI8gjblzVOnLr4peCJhGbYGPpJ3LFqpi5nsFfoK4mmDLvWdeah0B7gmJeXabN7nQkFoqeSdmOw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.3.0.tgz", + "integrity": "sha512-c5C0nROxVFsgMAm4vWDB1LDv3v4K18Y8eVxazL3dEr7w+7kNLc5koWrW7fWmCnrbItnuTna4nLS2PcSZrkYidg==", "requires": { "@types/history": "^4.7.11", "@types/react": "*", @@ -16420,12 +16267,13 @@ } }, "@docusaurus/utils": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.2.0.tgz", - "integrity": "sha512-oNk3cjvx7Tt1Lgh/aeZAmFpGV2pDr5nHKrBVx6hTkzGhrnMuQqLt6UPlQjdYQ3QHXwyF/ZtZMO1D5Pfi0lu7SA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-6+GCurDsePHHbLM3ktcjv8N4zrjgrl1O7gOQNG4UMktcwHssFFVm+geVcB4M8siOmwUjV2VaNrp0hpGy8DOQHw==", "requires": { - "@docusaurus/logger": "2.2.0", + "@docusaurus/logger": "2.3.0", "@svgr/webpack": "^6.2.1", + "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.1.0", "github-slugger": "^1.4.0", @@ -16439,23 +16287,30 @@ "tslib": "^2.4.0", "url-loader": "^4.1.1", "webpack": "^5.73.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } } }, "@docusaurus/utils-common": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.2.0.tgz", - "integrity": "sha512-qebnerHp+cyovdUseDQyYFvMW1n1nv61zGe5JJfoNQUnjKuApch3IVsz+/lZ9a38pId8kqehC1Ao2bW/s0ntDA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-2.3.0.tgz", + "integrity": "sha512-nu5An+26FS7SQTwvyFR4g9lw3NU1u2RLcxJPZF+NCOG8Ne96ciuQosa7+N1kllm/heEJqfTaAUD0sFxpTZrDtw==", "requires": { "tslib": "^2.4.0" } }, "@docusaurus/utils-validation": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.2.0.tgz", - "integrity": "sha512-I1hcsG3yoCkasOL5qQAYAfnmVoLei7apugT6m4crQjmDGxq+UkiRrq55UqmDDyZlac/6ax/JC0p+usZ6W4nVyg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-2.3.0.tgz", + "integrity": "sha512-TBJCLqwAoiQQJ6dbgBpuLvzsn/XiTgbZkd6eJFUIQYLb1d473Zv58QrHXVmVQDLWiCgmJpHW2LpMfumTpCDgJw==", "requires": { - "@docusaurus/logger": "2.2.0", - "@docusaurus/utils": "2.2.0", + "@docusaurus/logger": "2.3.0", + "@docusaurus/utils": "2.3.0", "joi": "^17.6.0", "js-yaml": "^4.1.0", "tslib": "^2.4.0" @@ -16475,9 +16330,9 @@ } }, "@iconify/react": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@iconify/react/-/react-4.0.1.tgz", - "integrity": "sha512-/DBJqh5K7W4f+d4kpvyJa/OTpVa3GfgrE9bZFAKP0vIWDr0cvVU9MVvbbkek216w9nLQhpJY/FeJtc6izB1PHw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-4.1.0.tgz", + "integrity": "sha512-Mf72i3TNNKpKCKxmo7kzqyrUdCgaoljpqtWmtqpqwyxoV4ukhnDsSRNLhf2yBnqGr3cVZsdj/i0FMpXIY0Qk0g==", "dev": true, "requires": { "@iconify/types": "^2.0.0" @@ -16541,9 +16396,9 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, "@loadable/component": { - "version": "5.15.2", - "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.2.tgz", - "integrity": "sha512-ryFAZOX5P2vFkUdzaAtTG88IGnr9qxSdvLRvJySXcUA4B4xVWurUNADu3AnKPksxOZajljqTrDEDcYjeL4lvLw==", + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.3.tgz", + "integrity": "sha512-VOgYgCABn6+/7aGIpg7m0Ruj34tGetaJzt4bQ345FwEovDQZ+dua+NWLmuJKv8rWZyxOUSfoJkmGnzyDXH2BAQ==", "requires": { "@babel/runtime": "^7.7.7", "hoist-non-react-statics": "^3.3.1", @@ -17300,30 +17155,30 @@ "requires": {} }, "algoliasearch": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.14.2.tgz", - "integrity": "sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.14.3.tgz", + "integrity": "sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg==", "requires": { - "@algolia/cache-browser-local-storage": "4.14.2", - "@algolia/cache-common": "4.14.2", - "@algolia/cache-in-memory": "4.14.2", - "@algolia/client-account": "4.14.2", - "@algolia/client-analytics": "4.14.2", - "@algolia/client-common": "4.14.2", - "@algolia/client-personalization": "4.14.2", - "@algolia/client-search": "4.14.2", - "@algolia/logger-common": "4.14.2", - "@algolia/logger-console": "4.14.2", - "@algolia/requester-browser-xhr": "4.14.2", - "@algolia/requester-common": "4.14.2", - "@algolia/requester-node-http": "4.14.2", - "@algolia/transporter": "4.14.2" + "@algolia/cache-browser-local-storage": "4.14.3", + "@algolia/cache-common": "4.14.3", + "@algolia/cache-in-memory": "4.14.3", + "@algolia/client-account": "4.14.3", + "@algolia/client-analytics": "4.14.3", + "@algolia/client-common": "4.14.3", + "@algolia/client-personalization": "4.14.3", + "@algolia/client-search": "4.14.3", + "@algolia/logger-common": "4.14.3", + "@algolia/logger-console": "4.14.3", + "@algolia/requester-browser-xhr": "4.14.3", + "@algolia/requester-common": "4.14.3", + "@algolia/requester-node-http": "4.14.3", + "@algolia/transporter": "4.14.3" } }, "algoliasearch-helper": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.1.tgz", - "integrity": "sha512-mvsPN3eK4E0bZG0/WlWJjeqe/bUD2KOEVOl0GyL/TGXn6wcpZU8NOuztGHCUKXkyg5gq6YzUakVTmnmSSO5Yiw==", + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.11.3.tgz", + "integrity": "sha512-TbaEvLwiuGygHQIB8y+OsJKQQ40+JKUua5B91X66tMUHyyhbNHvqyr0lqd3wCoyKx7WybyQrC0WJvzoIeh24Aw==", "requires": { "@algolia/events": "^4.0.1" } @@ -19708,7 +19563,9 @@ } }, "github-slugger": { - "version": "1.4.0" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" }, "glob": { "version": "7.2.2", @@ -21220,9 +21077,9 @@ "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" }, "parse5": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.1.tgz", - "integrity": "sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "requires": { "entities": "^4.4.0" }, @@ -22176,7 +22033,9 @@ } }, "react-textarea-autosize": { - "version": "8.3.4", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.0.tgz", + "integrity": "sha512-YrTFaEHLgJsi8sJVYHBzYn+mkP3prGkmP2DKb/tm0t7CLJY5t1Rxix8070LAKb0wby7bl/lf2EeHkuMihMZMwQ==", "requires": { "@babel/runtime": "^7.10.2", "use-composed-ref": "^1.3.0", @@ -23753,6 +23612,12 @@ "use-isomorphic-layout-effect": "^1.1.1" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/website/package.json b/website/package.json index 0cb897b46..5d5670674 100644 --- a/website/package.json +++ b/website/package.json @@ -14,10 +14,10 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "2.2.0", - "@docusaurus/plugin-google-gtag": "^2.2.0", - "@docusaurus/preset-classic": "2.2.0", - "@loadable/component": "^5.15.2", + "@docusaurus/core": "2.3.0", + "@docusaurus/plugin-google-gtag": "^2.3.0", + "@docusaurus/preset-classic": "2.3.0", + "@loadable/component": "^5.15.3", "@mdx-js/react": "^1.6.22", "animate.css": "^4.1.1", "clsx": "^1.2.1", @@ -35,8 +35,8 @@ "wow.js": "^1.2.2" }, "devDependencies": { - "@docusaurus/module-type-aliases": "2.2.0", - "@iconify/react": "^4.0.1", + "@docusaurus/module-type-aliases": "2.3.0", + "@iconify/react": "^4.1.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.21", "tailwindcss": "^3.2.4" diff --git a/website/sidebars.js b/website/sidebars.js index fd92ea50e..2403412b7 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -19,8 +19,8 @@ const sidebars = { 'quickstart', { type: 'category', - label: 'Corso setup', - items: ['setup/concepts', 'setup/download', 'setup/m365-access', 'setup/configuration', 'setup/repos'], + label: 'Usage', + items: ['setup/concepts', 'setup/download', 'setup/m365-access', 'setup/configuration', 'setup/repos', 'setup/fault-tolerance'], }, { type: 'category', diff --git a/website/styles/Vocab/Base/accept.txt b/website/styles/Vocab/Base/accept.txt index eae43d149..69565b8a5 100644 --- a/website/styles/Vocab/Base/accept.txt +++ b/website/styles/Vocab/Base/accept.txt @@ -36,4 +36,5 @@ Atlassian SLAs runbooks stdout -stderr \ No newline at end of file +stderr +backoff \ No newline at end of file