From 6967f1bf6ef830c5df26d8290d76f2e177044d28 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 10 Jan 2023 17:04:41 -0800 Subject: [PATCH 01/38] Fix compile error in factory script (#2092) ## Description Go does not allow importing code from a file marked as package main into another file. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * closes #2026 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/cmd/factory/factory.go | 208 ++----------------------- src/cmd/factory/impl/common.go | 198 +++++++++++++++++++++++ src/cmd/factory/{ => impl}/exchange.go | 32 ++-- src/cmd/factory/{ => impl}/onedrive.go | 4 +- 4 files changed, 226 insertions(+), 216 deletions(-) create mode 100644 src/cmd/factory/impl/common.go rename src/cmd/factory/{ => impl}/exchange.go (83%) rename src/cmd/factory/{ => impl}/onedrive.go (88%) diff --git a/src/cmd/factory/factory.go b/src/cmd/factory/factory.go index 30474f32c..d7bb14620 100644 --- a/src/cmd/factory/factory.go +++ b/src/cmd/factory/factory.go @@ -3,25 +3,12 @@ package main import ( "context" "os" - "strings" - "time" - "github.com/google/uuid" - "github.com/pkg/errors" "github.com/spf13/cobra" . "github.com/alcionai/corso/src/cli/print" - "github.com/alcionai/corso/src/internal/common" - "github.com/alcionai/corso/src/internal/connector" - "github.com/alcionai/corso/src/internal/connector/mockconnector" - "github.com/alcionai/corso/src/internal/data" - "github.com/alcionai/corso/src/pkg/account" - "github.com/alcionai/corso/src/pkg/backup/details" - "github.com/alcionai/corso/src/pkg/control" - "github.com/alcionai/corso/src/pkg/credentials" + "github.com/alcionai/corso/src/cmd/factory/impl" "github.com/alcionai/corso/src/pkg/logger" - "github.com/alcionai/corso/src/pkg/path" - "github.com/alcionai/corso/src/pkg/selectors" ) var factoryCmd = &cobra.Command{ @@ -42,17 +29,6 @@ var oneDriveCmd = &cobra.Command{ RunE: handleOneDriveFactory, } -var ( - count int - destination string - tenant string - user string -) - -// TODO: ErrGenerating = errors.New("not all items were successfully generated") - -var ErrNotYetImplemeted = errors.New("not yet implemented") - // ------------------------------------------------------------------------------------------ // CLI command handlers // ------------------------------------------------------------------------------------------ @@ -65,18 +41,18 @@ func main() { // persistent flags that are common to all use cases fs := factoryCmd.PersistentFlags() - fs.StringVar(&tenant, "tenant", "", "m365 tenant containing the user") - fs.StringVar(&user, "user", "", "m365 user owning the new data") + fs.StringVar(&impl.Tenant, "tenant", "", "m365 tenant containing the user") + fs.StringVar(&impl.User, "user", "", "m365 user owning the new data") cobra.CheckErr(factoryCmd.MarkPersistentFlagRequired("user")) - fs.IntVar(&count, "count", 0, "count of items to produce") + fs.IntVar(&impl.Count, "count", 0, "count of items to produce") cobra.CheckErr(factoryCmd.MarkPersistentFlagRequired("count")) - fs.StringVar(&destination, "destination", "", "destination of the new data (will create as needed)") + fs.StringVar(&impl.Destination, "destination", "", "destination of the new data (will create as needed)") cobra.CheckErr(factoryCmd.MarkPersistentFlagRequired("destination")) factoryCmd.AddCommand(exchangeCmd) - addExchangeCommands(exchangeCmd) + impl.AddExchangeCommands(exchangeCmd) factoryCmd.AddCommand(oneDriveCmd) - addOneDriveCommands(oneDriveCmd) + impl.AddOneDriveCommands(oneDriveCmd) if err := factoryCmd.ExecuteContext(ctx); err != nil { logger.Flush(ctx) @@ -85,180 +61,16 @@ func main() { } func handleFactoryRoot(cmd *cobra.Command, args []string) error { - Err(cmd.Context(), ErrNotYetImplemeted) + Err(cmd.Context(), impl.ErrNotYetImplemeted) return cmd.Help() } func handleExchangeFactory(cmd *cobra.Command, args []string) error { - Err(cmd.Context(), ErrNotYetImplemeted) + Err(cmd.Context(), impl.ErrNotYetImplemeted) return cmd.Help() } func handleOneDriveFactory(cmd *cobra.Command, args []string) error { - Err(cmd.Context(), ErrNotYetImplemeted) + Err(cmd.Context(), impl.ErrNotYetImplemeted) return cmd.Help() } - -// ------------------------------------------------------------------------------------------ -// Restoration -// ------------------------------------------------------------------------------------------ - -type dataBuilderFunc func(id, now, subject, body string) []byte - -func generateAndRestoreItems( - ctx context.Context, - gc *connector.GraphConnector, - acct account.Account, - service path.ServiceType, - cat path.CategoryType, - sel selectors.Selector, - userID, destFldr string, - howMany int, - dbf dataBuilderFunc, -) (*details.Details, error) { - items := make([]item, 0, howMany) - - for i := 0; i < howMany; i++ { - var ( - now = common.Now() - nowLegacy = common.FormatLegacyTime(time.Now()) - id = uuid.NewString() - subject = "automated " + now[:16] + " - " + id[:8] - body = "automated " + cat.String() + " generation for " + userID + " at " + now + " - " + id - ) - - items = append(items, item{ - name: id, - data: dbf(id, nowLegacy, subject, body), - }) - } - - collections := []collection{{ - pathElements: []string{destFldr}, - category: cat, - items: items, - }} - - // TODO: fit the desination to the containers - dest := control.DefaultRestoreDestination(common.SimpleTimeTesting) - dest.ContainerName = destFldr - - dataColls, err := buildCollections( - service, - acct.ID(), userID, - dest, - collections, - ) - if err != nil { - return nil, err - } - - Infof(ctx, "Generating %d %s items in %s\n", howMany, cat, destination) - - return gc.RestoreDataCollections(ctx, acct, sel, dest, dataColls) -} - -// ------------------------------------------------------------------------------------------ -// Common Helpers -// ------------------------------------------------------------------------------------------ - -func getGCAndVerifyUser(ctx context.Context, userID string) (*connector.GraphConnector, account.Account, error) { - tid := common.First(tenant, os.Getenv(account.AzureTenantID)) - - // get account info - m365Cfg := account.M365Config{ - M365: credentials.GetM365(), - AzureTenantID: tid, - } - - acct, err := account.NewAccount(account.ProviderM365, m365Cfg) - if err != nil { - return nil, account.Account{}, errors.Wrap(err, "finding m365 account details") - } - - // build a graph connector - gc, err := connector.NewGraphConnector(ctx, acct, connector.Users) - if err != nil { - return nil, account.Account{}, errors.Wrap(err, "connecting to graph api") - } - - normUsers := map[string]struct{}{} - - for k := range gc.Users { - normUsers[strings.ToLower(k)] = struct{}{} - } - - if _, ok := normUsers[strings.ToLower(user)]; !ok { - return nil, account.Account{}, errors.New("user not found within tenant") - } - - return gc, acct, nil -} - -type item struct { - name string - data []byte -} - -type collection struct { - // Elements (in order) for the path representing this collection. Should - // only contain elements after the prefix that corso uses for the path. For - // example, a collection for the Inbox folder in exchange mail would just be - // "Inbox". - pathElements []string - category path.CategoryType - items []item -} - -func buildCollections( - service path.ServiceType, - tenant, user string, - dest control.RestoreDestination, - colls []collection, -) ([]data.Collection, error) { - collections := make([]data.Collection, 0, len(colls)) - - for _, c := range colls { - pth, err := toDataLayerPath( - service, - tenant, - user, - c.category, - c.pathElements, - false, - ) - if err != nil { - return nil, err - } - - mc := mockconnector.NewMockExchangeCollection(pth, len(c.items)) - - for i := 0; i < len(c.items); i++ { - mc.Names[i] = c.items[i].name - mc.Data[i] = c.items[i].data - } - - collections = append(collections, mc) - } - - return collections, nil -} - -func toDataLayerPath( - service path.ServiceType, - tenant, user string, - category path.CategoryType, - elements []string, - isItem bool, -) (path.Path, error) { - pb := path.Builder{}.Append(elements...) - - switch service { - case path.ExchangeService: - return pb.ToDataLayerExchangePathForCategory(tenant, user, category, isItem) - case path.OneDriveService: - return pb.ToDataLayerOneDrivePath(tenant, user, isItem) - } - - return nil, errors.Errorf("unknown service %s", service.String()) -} diff --git a/src/cmd/factory/impl/common.go b/src/cmd/factory/impl/common.go new file mode 100644 index 000000000..369d80d20 --- /dev/null +++ b/src/cmd/factory/impl/common.go @@ -0,0 +1,198 @@ +package impl + +import ( + "context" + "os" + "strings" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" + + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/internal/common" + "github.com/alcionai/corso/src/internal/connector" + "github.com/alcionai/corso/src/internal/connector/mockconnector" + "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/credentials" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/selectors" +) + +var ( + Count int + Destination string + Tenant string + User string +) + +// TODO: ErrGenerating = errors.New("not all items were successfully generated") + +var ErrNotYetImplemeted = errors.New("not yet implemented") + +// ------------------------------------------------------------------------------------------ +// Restoration +// ------------------------------------------------------------------------------------------ + +type dataBuilderFunc func(id, now, subject, body string) []byte + +func generateAndRestoreItems( + ctx context.Context, + gc *connector.GraphConnector, + acct account.Account, + service path.ServiceType, + cat path.CategoryType, + sel selectors.Selector, + tenantID, userID, destFldr string, + howMany int, + dbf dataBuilderFunc, +) (*details.Details, error) { + items := make([]item, 0, howMany) + + for i := 0; i < howMany; i++ { + var ( + now = common.Now() + nowLegacy = common.FormatLegacyTime(time.Now()) + id = uuid.NewString() + subject = "automated " + now[:16] + " - " + id[:8] + body = "automated " + cat.String() + " generation for " + userID + " at " + now + " - " + id + ) + + items = append(items, item{ + name: id, + data: dbf(id, nowLegacy, subject, body), + }) + } + + collections := []collection{{ + pathElements: []string{destFldr}, + category: cat, + items: items, + }} + + // TODO: fit the desination to the containers + dest := control.DefaultRestoreDestination(common.SimpleTimeTesting) + dest.ContainerName = destFldr + + dataColls, err := buildCollections( + service, + tenantID, userID, + dest, + collections, + ) + if err != nil { + return nil, err + } + + Infof(ctx, "Generating %d %s items in %s\n", howMany, cat, Destination) + + return gc.RestoreDataCollections(ctx, acct, sel, dest, dataColls) +} + +// ------------------------------------------------------------------------------------------ +// Common Helpers +// ------------------------------------------------------------------------------------------ + +func getGCAndVerifyUser(ctx context.Context, userID string) (*connector.GraphConnector, account.Account, error) { + tid := common.First(Tenant, os.Getenv(account.AzureTenantID)) + + // get account info + m365Cfg := account.M365Config{ + M365: credentials.GetM365(), + AzureTenantID: tid, + } + + acct, err := account.NewAccount(account.ProviderM365, m365Cfg) + if err != nil { + return nil, account.Account{}, errors.Wrap(err, "finding m365 account details") + } + + // build a graph connector + gc, err := connector.NewGraphConnector(ctx, acct, connector.Users) + if err != nil { + return nil, account.Account{}, errors.Wrap(err, "connecting to graph api") + } + + normUsers := map[string]struct{}{} + + for k := range gc.Users { + normUsers[strings.ToLower(k)] = struct{}{} + } + + if _, ok := normUsers[strings.ToLower(User)]; !ok { + return nil, account.Account{}, errors.New("user not found within tenant") + } + + return gc, acct, nil +} + +type item struct { + name string + data []byte +} + +type collection struct { + // Elements (in order) for the path representing this collection. Should + // only contain elements after the prefix that corso uses for the path. For + // example, a collection for the Inbox folder in exchange mail would just be + // "Inbox". + pathElements []string + category path.CategoryType + items []item +} + +func buildCollections( + service path.ServiceType, + tenant, user string, + dest control.RestoreDestination, + colls []collection, +) ([]data.Collection, error) { + collections := make([]data.Collection, 0, len(colls)) + + for _, c := range colls { + pth, err := toDataLayerPath( + service, + tenant, + user, + c.category, + c.pathElements, + false, + ) + if err != nil { + return nil, err + } + + mc := mockconnector.NewMockExchangeCollection(pth, len(c.items)) + + for i := 0; i < len(c.items); i++ { + mc.Names[i] = c.items[i].name + mc.Data[i] = c.items[i].data + } + + collections = append(collections, mc) + } + + return collections, nil +} + +func toDataLayerPath( + service path.ServiceType, + tenant, user string, + category path.CategoryType, + elements []string, + isItem bool, +) (path.Path, error) { + pb := path.Builder{}.Append(elements...) + + switch service { + case path.ExchangeService: + return pb.ToDataLayerExchangePathForCategory(tenant, user, category, isItem) + case path.OneDriveService: + return pb.ToDataLayerOneDrivePath(tenant, user, isItem) + } + + return nil, errors.Errorf("unknown service %s", service.String()) +} diff --git a/src/cmd/factory/exchange.go b/src/cmd/factory/impl/exchange.go similarity index 83% rename from src/cmd/factory/exchange.go rename to src/cmd/factory/impl/exchange.go index 6a292a8dc..26f7eef09 100644 --- a/src/cmd/factory/exchange.go +++ b/src/cmd/factory/impl/exchange.go @@ -1,4 +1,4 @@ -package main +package impl import ( "github.com/spf13/cobra" @@ -30,7 +30,7 @@ var ( } ) -func addExchangeCommands(cmd *cobra.Command) { +func AddExchangeCommands(cmd *cobra.Command) { cmd.AddCommand(emailsCmd) cmd.AddCommand(eventsCmd) cmd.AddCommand(contactsCmd) @@ -47,7 +47,7 @@ func handleExchangeEmailFactory(cmd *cobra.Command, args []string) error { return nil } - gc, acct, err := getGCAndVerifyUser(ctx, user) + gc, acct, err := getGCAndVerifyUser(ctx, User) if err != nil { return Only(ctx, err) } @@ -58,12 +58,12 @@ func handleExchangeEmailFactory(cmd *cobra.Command, args []string) error { acct, service, category, - selectors.NewExchangeRestore([]string{user}).Selector, - user, destination, - count, + selectors.NewExchangeRestore([]string{User}).Selector, + Tenant, User, Destination, + Count, func(id, now, subject, body string) []byte { return mockconnector.GetMockMessageWith( - user, user, user, + User, User, User, subject, body, body, now, now, now, now) }, @@ -88,7 +88,7 @@ func handleExchangeCalendarEventFactory(cmd *cobra.Command, args []string) error return nil } - gc, acct, err := getGCAndVerifyUser(ctx, user) + gc, acct, err := getGCAndVerifyUser(ctx, User) if err != nil { return Only(ctx, err) } @@ -99,12 +99,12 @@ func handleExchangeCalendarEventFactory(cmd *cobra.Command, args []string) error acct, service, category, - selectors.NewExchangeRestore([]string{user}).Selector, - user, destination, - count, + selectors.NewExchangeRestore([]string{User}).Selector, + Tenant, User, Destination, + Count, func(id, now, subject, body string) []byte { return mockconnector.GetMockEventWith( - user, subject, body, body, + User, subject, body, body, now, now, false) }, ) @@ -128,7 +128,7 @@ func handleExchangeContactFactory(cmd *cobra.Command, args []string) error { return nil } - gc, acct, err := getGCAndVerifyUser(ctx, user) + gc, acct, err := getGCAndVerifyUser(ctx, User) if err != nil { return Only(ctx, err) } @@ -139,9 +139,9 @@ func handleExchangeContactFactory(cmd *cobra.Command, args []string) error { acct, service, category, - selectors.NewExchangeRestore([]string{user}).Selector, - user, destination, - count, + selectors.NewExchangeRestore([]string{User}).Selector, + Tenant, User, Destination, + Count, func(id, now, subject, body string) []byte { given, mid, sur := id[:8], id[9:13], id[len(id)-12:] diff --git a/src/cmd/factory/onedrive.go b/src/cmd/factory/impl/onedrive.go similarity index 88% rename from src/cmd/factory/onedrive.go rename to src/cmd/factory/impl/onedrive.go index a76d222b9..f6bef0edf 100644 --- a/src/cmd/factory/onedrive.go +++ b/src/cmd/factory/impl/onedrive.go @@ -1,4 +1,4 @@ -package main +package impl import ( "github.com/spf13/cobra" @@ -13,7 +13,7 @@ var filesCmd = &cobra.Command{ RunE: handleOneDriveFileFactory, } -func addOneDriveCommands(cmd *cobra.Command) { +func AddOneDriveCommands(cmd *cobra.Command) { cmd.AddCommand(filesCmd) } From 921f53325224aa5b87439ffd6ea5504551d93cbf Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 10 Jan 2023 23:28:10 -0700 Subject: [PATCH 02/38] remove design/cli.md; conflicts with docs (#2097) ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup --- design/cli.md | 214 -------------------------------------------------- 1 file changed, 214 deletions(-) delete mode 100644 design/cli.md diff --git a/design/cli.md b/design/cli.md deleted file mode 100644 index 9d2cc4166..000000000 --- a/design/cli.md +++ /dev/null @@ -1,214 +0,0 @@ -# CLI Commands -## Status - -Revision: v0.0.1 - ------ - - -This is a proposal for Corso cli commands extrapolated from the Functional Requirements product documentation. Open questions are listed in the `Details & Discussion` section. The command set includes some p1/p2 actions for completeness. This proposal only intends to describe the available commands themselves and does not evaluate functionality or feature design beyond that goal. - -# CLI Goals - -- Ease (and enjoyment) of Use, more than minimal functionality. -- Intended for use by Humans, not Computers. -- Outputs should be either interactive/progressive (for ongoing work) or easily greppable/parseable. - -## Todo/Undefined: - -- Interactivity and sub-selection/helpful action completion within command operation. -- Quality-of-life and niceties such as interactive/output display, formatting and presentation, or maximum minimization of user effort to run Corso. - ------ -## Commands - -Standard format: -`corso {command} [{subcommand}] [{service|repository}] [{flag}...]` - -| Cmd | | | Flags | Notes | -| --- | --- | --- | --- | --- | -| version | | | | Same as `corso --version` | -| | | | —version | Outputs Corso version details. | -| help | | | | Same as `corso —-help` | -| * | * | help | | Same as `{command} -—help` | -| * | * | | —help | Same as `{command} help` | - -| Cmd | | | Flags | Notes | -| --- | --- | --- | --- | --- | -| repo | * | | | Same as `repo [*] --help`. | -| repo | init | {repository} | | Initialize a Corso repository. | -| repo | init | {repository} | —tenant {azure_tenant_id} | Provides the account’s tenant ID. | -| repo | init | {repository} | —client {azure_client_id} | Provides the account’s client ID. | -| repo | connect | {repository} | | Connects to the specified repo. | -| repo | configure | {repository} | | Sets mutable config properties to the provided values. | -| repo | * | * | —config {cfg_file_path} | Specify a repo configuration file. Values may also be provided via individual flags and env vars. | -| repo | * | * | —{config-prop} | Blanket commitment to support config via flags. | -| repo | * | * | —credentials {creds_file_path} | Specify a file containing credentials or secrets. Values may also be provided via env vars. | - -| Cmd | | | Flags | Notes | -| --- | --- | --- | --- | --- | -| backup | * | | | Same as backup [*] -—help | -| backup | list | {service} | | List all backups in the repository for the specified service. | -| backup | create | {service} | | Backup the specified service. | -| backup | * | {service} | —token {token} | Provides a security key for permission to perform backup. | -| backup | * | {service} | —{entity} {entity_id}... | Only involve the target entity(s). Entities are things like users, groups, sites, etc. Entity flag support is service-specific. | - -| Cmd | | | Flags | Notes | -| --- | --- | --- | --- | --- | -| restore | | | | Same as `restore -—help` | -| restore | {service} | | | Complete service restoration using the latest versioned backup. | -| restore | {service} | | —backup {backup_id} | Restore data from only the targeted backup(s). | -| restore | {service} | | —{entity} {entity_id}... | Only involve the target entity(s). Entities are things like users, groups, sites, etc. Entity flag support is service-specific. | ---- - - -## Examples -### Basic Usage - -**First Run** - -```bash -$ export AZURE_CLIENT_SECRET=my_azure_secret -$ export AWS_SECRET_ACCESS_KEY=my_s3_secret -$ corso repo init s3 --bucket my_s3_bucket --access-key my_s3_key \ - --tenant my_azure_tenant_id --clientid my_azure_client_id -$ corso backup express -``` - -**Follow-up Actions** - -```bash -$ corso repo connect s3 --bucket my_s3_bucket --access-key my_s3_key -$ corso backup express -$ corso backup list express -``` ------ - -# Details & Discussion - -## UC0 - CLI User Interface - -Base command: `corso` - -Standard format: `corso {command} [{subcommand}] [{service}] [{flag}...]` - -Examples: - -- `corso help` -- `corso repo init --repository s3 --tenant t_1` -- `corso backup create teams` -- `corso restore teams --backup b_1` - -## UC1 - Initialization and Connection - -**Account Handling** - -M365 accounts are paired with repo initialization, resulting in a single-tenancy storage. Any `repo` action applies the same behavior to the account as well. That is, `init` will handle all initialization steps for both the repository and the account, and both must succeed for the command to complete successfully, including all necessary validation checks. Likewise, `connect` will validate and establish a connection (or, at least, the ability to communicate) with both the account and the repository. - -**Init** - -`corso repo init {repository} --config {cfg} --credentials {creds}` - -Initializes a repository, bootstrapping resources as necessary and storing configuration details within Corso. Repo is the name of the repository provider, eg: ‘s3’. Cfg and creds, in this example, point to json (or alternatively yaml?) files containing the details required to establish the connection. Configuration options, when known, will get support for flag-based declaration. Similarly, env vars will be supported as needed. - -**Connection** - -`corso repo connect {repository} --credentials {creds}` - -[https://docs.flexera.com/flexera/EN/SaaSManager/M365CCIntegration.htm#integrations_3059193938_1840275](https://docs.flexera.com/flexera/EN/SaaSManager/M365CCIntegration.htm#integrations_3059193938_1840275) - -Connects to an existing (ie, initialized) repository. - -Corso is expected to gracefully handle transient disconnections during backup/restore runtimes (and otherwise, as needed). - -**Deletion** - -`corso repo delete {repository}` - -(Included here for discussion, but not being added to the CLI command set at this time.) - -Removes a repository from Corso. More exploration is needed here to explore cascading effects (or lack thereof) from the command. At minimum, expect additional user involvement to confirm that the deletion is wanted, and not erroneous. - -## UC1.1 - Version - -`corso --version` outputs the current version details such as: commit id and datetime, maybe semver (complete release version details to be decided). -Further versioning controls are not currently covered in this proposal. - -## UC2 - Configuration - -`corso repo configure --reposiory {repo} --config {cfg}` - -Updates the configuration details for an existing repository. - -Configuration is divided between mutable and immutable properties. Generally, initialization-specific configurations (those that identify the storage repository, it’s connection, and its fundamental behavior), among other properties, are considered immutable and cannot be reconfigured. As a result, `repo configure` will not be able to rectify a misconfigured init; some other user flow will be needed to resolve that issue. - -Configure allows mutation of config properties that can be safely and transiently applied. For example: backup retention and expiration policies. A complete list of how each property is classified is forthcoming as we build that list of properties. - -## UC3 - On-Demand Backup - -`corso backup` is reserved as a non-actionable command, rather than have it kick off a backup action. This is to ensure users don’t accidentally kick off a migration in the process of exploring the api.  `corso backup` produces the same output as `corso backup --help`. - -**Full Service Backup** - -- `corso backup create {service}` - -**Selective Backup** - -- `corso backup create {service} --{entity} {entity_id}...` - -Entities are service-applicable objects that match up to m365 objects. Users, groups, sites, mailboxes, etc. Entity flags are available on a per-service basis. For example, —site is available for the sharepoint service, and —mailbox for express, but not the reverse. A full list of system-entity mappings is coming in the future. - -**Examples** - -- `corso backup` → displays the help output. -- `corso backup create teams` → generates a full backup of the teams service. -- `corso backup create express --group g_1` → backs up the g_1 group within express. - -## UC3.2 - Security Token - -(This section is incomplete: further design details are needed about security expression.) Some commands, such as Backup/Restore require a security key declaration to verify that the caller has permission to perform the command. - -`corso * * --token {token}` - -## UC5 - Backup Ops - -`corso backup list {service}` - -Produces a list of the backups which currently exist in the repository. - -`corso backup list {service} --{entity} {entity_id}...` - -The list can be filtered to contain backups relevant to the specified entities. A possible user flow for restoration is for the user to use this to discover which backups match their needs, and then apply those backups in a restore operation. - -**Expiration Control** - -Will appear in a future revision. - -## UC6 - Restore - -Similar to backup, `corso restore` is reserved as a non-actionable command to serve up the same output as `corso restore —help`. - -### UC6.1 - -**Full Service Restore** - -- `corso restore {service} [--backup {backup_id}...]` - -If no backups are specified, this defaults to the most recent backup of the specified service. - -**Selective Restore** - -- `corso restore {service} [--backup {backup_id}...] [--{entity} {entity_id}...]` - -Entities are service-applicable objects that match up to m365 objects. Users, groups, sites, mailboxes, etc. Entity flags are available on a per-service basis. For example, —site is available for the sharepoint service, and —mailbox for express, but not the reverse. A full list of system-entity mappings is coming in the future. - -**Examples** - -- `corso restore` → displays the help output. -- `corso restore teams` → restores all data in the teams service. -- `corso restore sharepoint --backup b_1` → restores the sharepoint data in the b_1 backup. -- `corso restore express --group g_1` → restores the g_1 group within sharepoint. - -## UC6.2 - disaster recovery - -Multi-service backup/restoration is still under review. From fc17c387f1ace58dc1f0f0443daa07964e5c3a4f Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Tue, 10 Jan 2023 23:09:14 -0800 Subject: [PATCH 03/38] Update list of skus that license onedrive (#2105) ## Description Adds a few missing SKUs that license OneDrive. This is not the complete list. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2104 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/internal/connector/onedrive/drive.go | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26296e27d..5a2c79930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Issue where repository connect progress bar was clobbering backup/restore operation output. - Issue where a `backup create exchange` produced one backup record per data type. - Specifying multiple users in a onedrive backup (ex: `--user a,b,c`) now properly delimits the input along the commas. +- Updated the list of M365 SKUs used to check if a user has a OneDrive license. ### Known Issues diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index 55e1ead53..36a79dca1 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -54,6 +54,18 @@ var ( "afcafa6a-d966-4462-918c-ec0b4e0fe642", // Microsoft 365 E5 Developer "c42b9cae-ea4f-4ab7-9717-81576235ccac", + // Microsoft 365 E5 + "06ebc4ee-1bb5-47dd-8120-11324bc54e06", + // Office 365 E4 + "1392051d-0cb9-4b7a-88d5-621fee5e8711", + // Microsoft 365 E3 + "05e9a617-0261-4cee-bb44-138d3ef5d965", + // Microsoft 365 Business Premium + "cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46", + // Microsoft 365 Business Standard + "f245ecc8-75af-4f8e-b61f-27d8114de5f3", + // Microsoft 365 Business Basic + "3b555118-da6a-4418-894f-7df1e2096870", } ) From 3869baac64976ee02a99c204c0cb80ebd54ec6c5 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Tue, 10 Jan 2023 23:45:01 -0800 Subject: [PATCH 04/38] Support specifying an AWS credential provider (#2081) ## Description Implements support for a caller to specify a credential provider. This assumes an S3 credential provider but we should follow-up with a change that isn't S3 specific. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [x] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2106 * ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/go.mod | 18 ++++++----- src/go.sum | 45 +++++++++++++++++---------- src/internal/kopia/s3.go | 38 +++++++++++++++++++++- src/pkg/repository/repository_test.go | 19 +++++++++++ src/pkg/storage/storage.go | 18 +++++++++++ 5 files changed, 112 insertions(+), 26 deletions(-) diff --git a/src/go.mod b/src/go.mod index ef900e954..3734d8ed9 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,6 +2,8 @@ module github.com/alcionai/corso/src go 1.19 +replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f + require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/aws/aws-sdk-go v1.44.176 @@ -72,10 +74,10 @@ require ( github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/compress v1.15.12 // indirect github.com/klauspost/cpuid/v2 v2.1.1 // indirect github.com/klauspost/pgzip v1.2.5 // indirect - github.com/klauspost/reedsolomon v1.11.0 // indirect + github.com/klauspost/reedsolomon v1.11.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect @@ -84,7 +86,7 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/kiota-serialization-text-go v0.6.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minio-go/v7 v7.0.39 // indirect + github.com/minio/minio-go/v7 v7.0.45 github.com/minio/sha256-simd v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -92,8 +94,8 @@ require ( github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.13.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect @@ -109,14 +111,14 @@ require ( go.opentelemetry.io/otel/trace v1.11.2 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect - golang.org/x/crypto v0.1.0 // indirect + golang.org/x/crypto v0.3.0 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect - google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect - google.golang.org/grpc v1.50.1 // indirect + google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect + google.golang.org/grpc v1.51.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/src/go.sum b/src/go.sum index 5156b1c7e..4bc44766b 100644 --- a/src/go.sum +++ b/src/go.sum @@ -47,15 +47,19 @@ github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f h1:RsXSF0CgmtCMHTew/O7QJJjUoqb/X3rJqI0qRXve/Lc= +github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f/go.mod h1:yzJV11S6N6XMboXt7oCO6Jy2jJHPeSMtA+KOJ9Y1548= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +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.176 h1:mxcfI3IWHemX+5fEKt5uqIS/hdbaR7qzGfJYo5UyjJE= @@ -85,6 +89,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -119,6 +124,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -184,7 +191,9 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -217,8 +226,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -226,12 +235,11 @@ github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdy github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/klauspost/reedsolomon v1.11.0 h1:fc24kMFf4I6dXJwSkVAsw8Za/dMcJrV5ImeDjG3ss1M= -github.com/klauspost/reedsolomon v1.11.0/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k= +github.com/klauspost/reedsolomon v1.11.3 h1:rX9UNNvDhJ0Bq45y6uBy/eYehcjyz5faokTuZmu1Q9U= +github.com/klauspost/reedsolomon v1.11.3/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kopia/kopia v0.12.0 h1:8Pj7Q7Pn1hoDdzmHX6rryfO0f/3AAEy/f5xW2itVHIo= -github.com/kopia/kopia v0.12.0/go.mod h1:pkf8YKBD69IEb/2X/D8jddYaJSb1eXQCtK4kiMa+BIc= +github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 h1:WP5VfIQL7AaYkO4zTNSCsVOawTzudbc4tvLojvg0RKc= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -275,8 +283,8 @@ github.com/microsoftgraph/msgraph-sdk-go-core v0.31.1 h1:aVvnO5l8qLCEcvELc5n9grt github.com/microsoftgraph/msgraph-sdk-go-core v0.31.1/go.mod h1:RE4F2qGCTehGtQGc9Txafc4l+XMpbjYuO4amDLFgOWE= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.39 h1:upnbu1jCGOqEvrGSpRauSN9ZG7RCHK7VHxXS8Vmg2zk= -github.com/minio/minio-go/v7 v7.0.39/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= +github.com/minio/minio-go/v7 v7.0.45 h1:g4IeM9M9pW/Lo8AGGNOjBZYlvmtlE1N5TQEYWXRWzIs= +github.com/minio/minio-go/v7 v7.0.45/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -312,13 +320,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= @@ -375,6 +384,7 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -400,6 +410,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -434,8 +445,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -731,8 +742,8 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e h1:S9GbmC1iCgvbLyAokVCwiO6tVIrU9Y7c5oMx1V/ki/Y= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc= +google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -749,8 +760,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/src/internal/kopia/s3.go b/src/internal/kopia/s3.go index d25dec610..65f0f538e 100644 --- a/src/internal/kopia/s3.go +++ b/src/internal/kopia/s3.go @@ -3,8 +3,10 @@ package kopia import ( "context" + awscreds "github.com/aws/aws-sdk-go/aws/credentials" "github.com/kopia/kopia/repo/blob" "github.com/kopia/kopia/repo/blob/s3" + miniocreds "github.com/minio/minio-go/v7/pkg/credentials" "github.com/alcionai/corso/src/pkg/storage" ) @@ -30,7 +32,41 @@ func s3BlobStorage(ctx context.Context, s storage.Storage) (blob.Storage, error) Prefix: cfg.Prefix, DoNotUseTLS: cfg.DoNotUseTLS, DoNotVerifyTLS: cfg.DoNotVerifyTLS, + Creds: credentials(s.Creds), } - return s3.New(ctx, &opts) + return s3.New(ctx, &opts, false) +} + +// credentials converts an AWS Credential to a Minio credential (which Kopia uses) +func credentials(creds *awscreds.Credentials) *miniocreds.Credentials { + if creds == nil { + return nil + } + + return miniocreds.New(&minioProvider{creds: creds}) +} + +// minioProvider is a shim that implements the Minio `Provider` interface +// for an AWS credential +type minioProvider struct { + creds *awscreds.Credentials +} + +func (mp *minioProvider) Retrieve() (miniocreds.Value, error) { + v, err := mp.creds.Get() + if err != nil { + return miniocreds.Value{}, err + } + + return miniocreds.Value{ + AccessKeyID: v.AccessKeyID, + SecretAccessKey: v.SecretAccessKey, + SessionToken: v.SessionToken, + SignerType: miniocreds.SignatureV4, + }, nil +} + +func (mp *minioProvider) IsExpired() bool { + return mp.creds.IsExpired() } diff --git a/src/pkg/repository/repository_test.go b/src/pkg/repository/repository_test.go index c84938615..39b9cfd47 100644 --- a/src/pkg/repository/repository_test.go +++ b/src/pkg/repository/repository_test.go @@ -3,6 +3,7 @@ package repository_test import ( "testing" + awscreds "github.com/aws/aws-sdk-go/aws/credentials" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -10,6 +11,7 @@ import ( "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/credentials" "github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/storage" @@ -140,6 +142,23 @@ func (suite *RepositoryIntegrationSuite) TestInitialize() { } } +func (suite *RepositoryIntegrationSuite) TestInitializeCustomCredentials() { + ctx, flush := tester.NewContext() + defer flush() + + st := tester.NewPrefixedS3Storage(suite.T()) + + ak := credentials.GetAWS(map[string]string{}) + st.Creds = awscreds.NewStaticCredentials(ak.AccessKey, ak.SecretKey, ak.SessionToken) + + r, err := repository.Initialize(ctx, account.Account{}, st, control.Options{}) + require.NoError(suite.T(), err) + + defer func() { + r.Close(ctx) + }() +} + func (suite *RepositoryIntegrationSuite) TestConnect() { ctx, flush := tester.NewContext() defer flush() diff --git a/src/pkg/storage/storage.go b/src/pkg/storage/storage.go index 029e29596..cc5585ed1 100644 --- a/src/pkg/storage/storage.go +++ b/src/pkg/storage/storage.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/alcionai/corso/src/internal/common" ) @@ -35,6 +37,7 @@ const ( type Storage struct { Provider storageProvider Config map[string]string + Creds *credentials.Credentials } // NewStorage aggregates all the supplied configurations into a single configuration. @@ -47,6 +50,21 @@ func NewStorage(p storageProvider, cfgs ...common.StringConfigurer) (Storage, er }, err } +// NewStorageWithCredentials supports specifying a credential container +func NewStorageWithCredentials( + p storageProvider, + creds *credentials.Credentials, + cfgs ...common.StringConfigurer, +) (Storage, error) { + cs, err := common.UnionStringConfigs(cfgs...) + + return Storage{ + Provider: p, + Config: cs, + Creds: creds, + }, err +} + // Helper for parsing the values in a config object. // If the value is nil or not a string, returns an empty string. func orEmptyString(v any) string { From 9f05e83d433fd9eff54ef70c6962f11e109ad864 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 11 Jan 2023 07:49:20 -0500 Subject: [PATCH 05/38] CLI: Adds to the displayable headers for OneDrive and SharePoint. (#2090) ## Description The update adds `DriveName` to SharePoint details. Values are currently the M365ID ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature ## Issue(s) * related to #1938 * related to #2064 ## Test Plan - [x] :zap: Unit test --- src/internal/connector/onedrive/item.go | 35 ++++++++++++++++++------- src/pkg/backup/details/details.go | 8 +++--- src/pkg/backup/details/details_test.go | 14 ++++++++-- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/internal/connector/onedrive/item.go b/src/internal/connector/onedrive/item.go index 7f377d2cd..73391033b 100644 --- a/src/internal/connector/onedrive/item.go +++ b/src/internal/connector/onedrive/item.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "strings" msdrives "github.com/microsoftgraph/msgraph-sdk-go/drives" "github.com/microsoftgraph/msgraph-sdk-go/models" @@ -128,10 +129,11 @@ func oneDriveItemInfo(di models.DriveItemable, itemSize int64) *details.OneDrive // separately for restore processes because the local itemable // doesn't have its size value updated as a side effect of creation, // and kiota drops any SetSize update. +// TODO: Update drive name during Issue #2071 func sharePointItemInfo(di models.DriveItemable, itemSize int64) *details.SharePointInfo { var ( - id string - url string + id, parent, url string + reference = di.GetParentReference() ) // TODO: we rely on this info for details/restore lookups, @@ -148,14 +150,29 @@ func sharePointItemInfo(di models.DriveItemable, itemSize int64) *details.ShareP } } + if reference != nil { + parent = *reference.GetDriveId() + + if reference.GetName() != nil { + // EndPoint is not always populated from external apps + temp := *reference.GetName() + temp = strings.TrimSpace(temp) + + if temp != "" { + parent = temp + } + } + } + return &details.SharePointInfo{ - ItemType: details.OneDriveItem, - ItemName: *di.GetName(), - Created: *di.GetCreatedDateTime(), - Modified: *di.GetLastModifiedDateTime(), - Size: itemSize, - Owner: id, - WebURL: url, + ItemType: details.OneDriveItem, + ItemName: *di.GetName(), + Created: *di.GetCreatedDateTime(), + Modified: *di.GetLastModifiedDateTime(), + DriveName: parent, + Size: itemSize, + Owner: id, + WebURL: url, } } diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go index 923410dde..d407ca690 100644 --- a/src/pkg/backup/details/details.go +++ b/src/pkg/backup/details/details.go @@ -477,6 +477,7 @@ func (i ExchangeInfo) Values() []string { type SharePointInfo struct { Created time.Time `json:"created,omitempty"` ItemName string `json:"itemName,omitempty"` + DriveName string `json:"driveName,omitempty"` ItemType ItemType `json:"itemType,omitempty"` Modified time.Time `josn:"modified,omitempty"` Owner string `json:"owner,omitempty"` @@ -488,7 +489,7 @@ type SharePointInfo struct { // Headers returns the human-readable names of properties in a SharePointInfo // for printing out to a terminal in a columnar display. func (i SharePointInfo) Headers() []string { - return []string{"ItemName", "ParentPath", "Size", "WebURL", "Created", "Modified"} + return []string{"ItemName", "Drive", "ParentPath", "Size", "WebURL", "Created", "Modified"} } // Values returns the values matching the Headers list for printing @@ -496,6 +497,7 @@ func (i SharePointInfo) Headers() []string { func (i SharePointInfo) Values() []string { return []string{ i.ItemName, + i.DriveName, i.ParentPath, humanize.Bytes(uint64(i.Size)), i.WebURL, @@ -518,8 +520,8 @@ func (i *SharePointInfo) UpdateParentPath(newPath path.Path) error { // OneDriveInfo describes a oneDrive item type OneDriveInfo struct { Created time.Time `json:"created,omitempty"` - ItemName string `json:"itemName"` - DriveName string `json:"driveName"` + ItemName string `json:"itemName,omitempty"` + DriveName string `json:"driveName,omitempty"` ItemType ItemType `json:"itemType,omitempty"` Modified time.Time `json:"modified,omitempty"` Owner string `json:"owner,omitempty"` diff --git a/src/pkg/backup/details/details_test.go b/src/pkg/backup/details/details_test.go index 328576e99..efc654246 100644 --- a/src/pkg/backup/details/details_test.go +++ b/src/pkg/backup/details/details_test.go @@ -107,13 +107,23 @@ func (suite *DetailsUnitSuite) TestDetailsEntry_HeadersValues() { ParentPath: "parentPath", Size: 1000, WebURL: "https://not.a.real/url", + DriveName: "aDrive", Created: now, Modified: now, }, }, }, - expectHs: []string{"ID", "ItemName", "ParentPath", "Size", "WebURL", "Created", "Modified"}, - expectVs: []string{"deadbeef", "itemName", "parentPath", "1.0 kB", "https://not.a.real/url", nowStr, nowStr}, + expectHs: []string{"ID", "ItemName", "Drive", "ParentPath", "Size", "WebURL", "Created", "Modified"}, + expectVs: []string{ + "deadbeef", + "itemName", + "aDrive", + "parentPath", + "1.0 kB", + "https://not.a.real/url", + nowStr, + nowStr, + }, }, { name: "oneDrive info", From dd54bc280c935e311a855666dfbe5af0e1240e23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jan 2023 13:08:34 +0000 Subject: [PATCH 06/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.176=20to=201.44.177=20in=20/src=20(#?= =?UTF-8?q?2102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.176 to 1.44.177.
Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.177 (2023-01-10)

Service Client Updates

  • service/location: Updates service API and documentation
  • service/rds: Updates service API, documentation, waiters, paginators, and examples
    • This release adds support for configuring allocated storage on the CreateDBInstanceReadReplica, RestoreDBInstanceFromDBSnapshot, and RestoreDBInstanceToPointInTime APIs.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.176&new-version=1.44.177)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 3734d8ed9..6634c617b 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,7 +6,7 @@ replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.2023011005 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/aws/aws-sdk-go v1.44.176 + github.com/aws/aws-sdk-go v1.44.177 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 diff --git a/src/go.sum b/src/go.sum index 4bc44766b..be4a51dde 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.176 h1:mxcfI3IWHemX+5fEKt5uqIS/hdbaR7qzGfJYo5UyjJE= -github.com/aws/aws-sdk-go v1.44.176/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.177 h1:ckMJhU5Gj+4Rta+bJIUiUd7jvHom84aim3zkGPblq0s= +github.com/aws/aws-sdk-go v1.44.177/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= From 7829c070797fa659c9c9f9842e765519e0d9f627 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 11 Jan 2023 09:58:01 -0800 Subject: [PATCH 07/38] Remove dead code from refactoring Resource struct (#2093) ## Description Now unused code ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * closes #1916 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [x] :green_heart: E2E --- src/internal/kopia/snapshot_manager.go | 51 -------------------------- 1 file changed, 51 deletions(-) diff --git a/src/internal/kopia/snapshot_manager.go b/src/internal/kopia/snapshot_manager.go index 55cf7b8a6..8fc950dd2 100644 --- a/src/internal/kopia/snapshot_manager.go +++ b/src/internal/kopia/snapshot_manager.go @@ -66,30 +66,6 @@ type snapshotManager interface { LoadSnapshots(ctx context.Context, ids []manifest.ID) ([]*snapshot.Manifest, error) } -type OwnersCats struct { - ResourceOwners map[string]struct{} - ServiceCats map[string]ServiceCat -} - -type ServiceCat struct { - Service path.ServiceType - Category path.CategoryType -} - -// MakeServiceCat produces the expected OwnersCats.ServiceCats key from a -// path service and path category, as well as the ServiceCat value. -func MakeServiceCat(s path.ServiceType, c path.CategoryType) (string, ServiceCat) { - return serviceCatString(s, c), ServiceCat{s, c} -} - -// TODO(ashmrtn): Remove in a future PR. -// -//nolint:unused -//lint:ignore U1000 will be removed in future PR. -func serviceCatTag(p path.Path) string { - return serviceCatString(p.Service(), p.Category()) -} - func serviceCatString(s path.ServiceType, c path.CategoryType) string { return s.String() + c.String() } @@ -104,33 +80,6 @@ func makeTagKV(k string) (string, string) { return userTagPrefix + k, defaultTagValue } -// tagsFromStrings returns a map[string]string with tags for all ownersCats -// passed in. Currently uses placeholder values for each tag because there can -// be multiple instances of resource owners and categories in a single snapshot. -// TODO(ashmrtn): Remove in future PR. -// -//nolint:unused -//lint:ignore U1000 will be removed in future PR. -func tagsFromStrings(oc *OwnersCats) map[string]string { - if oc == nil { - return map[string]string{} - } - - res := make(map[string]string, len(oc.ServiceCats)+len(oc.ResourceOwners)) - - for k := range oc.ServiceCats { - tk, tv := makeTagKV(k) - res[tk] = tv - } - - for k := range oc.ResourceOwners { - tk, tv := makeTagKV(k) - res[tk] = tv - } - - return res -} - // getLastIdx searches for manifests contained in both foundMans and metas // and returns the most recent complete manifest index and the manifest it // corresponds to. If no complete manifest is in both lists returns nil, -1. From 53c5828caa41e039099762165204dacc792a1444 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 11 Jan 2023 13:38:22 -0500 Subject: [PATCH 08/38] Path: Add category --> Pages. (#2109) ## Description Adds path.Category `Pages` for SharePoint pages data type ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature ## Issue(s) * closes #2108 # ## Test Plan - [z] :zap: Unit test --- src/pkg/path/categorytype_string.go | 7 ++++--- src/pkg/path/resource_path.go | 4 ++++ src/pkg/path/resource_path_test.go | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/pkg/path/categorytype_string.go b/src/pkg/path/categorytype_string.go index b62b6fc78..626cc4e31 100644 --- a/src/pkg/path/categorytype_string.go +++ b/src/pkg/path/categorytype_string.go @@ -15,12 +15,13 @@ func _() { _ = x[FilesCategory-4] _ = x[ListsCategory-5] _ = x[LibrariesCategory-6] - _ = x[DetailsCategory-7] + _ = x[PagesCategory-7] + _ = x[DetailsCategory-8] } -const _CategoryType_name = "UnknownCategoryemailcontactseventsfileslistslibrariesdetails" +const _CategoryType_name = "UnknownCategoryemailcontactseventsfileslistslibrariespagesdetails" -var _CategoryType_index = [...]uint8{0, 15, 20, 28, 34, 39, 44, 53, 60} +var _CategoryType_index = [...]uint8{0, 15, 20, 28, 34, 39, 44, 53, 58, 65} func (i CategoryType) String() string { if i < 0 || i >= CategoryType(len(_CategoryType_index)-1) { diff --git a/src/pkg/path/resource_path.go b/src/pkg/path/resource_path.go index ec76083bb..c66cd300e 100644 --- a/src/pkg/path/resource_path.go +++ b/src/pkg/path/resource_path.go @@ -65,6 +65,7 @@ const ( FilesCategory // files ListsCategory // lists LibrariesCategory // libraries + PagesCategory // pages DetailsCategory // details ) @@ -82,6 +83,8 @@ func ToCategoryType(category string) CategoryType { return LibrariesCategory case ListsCategory.String(): return ListsCategory + case PagesCategory.String(): + return PagesCategory case DetailsCategory.String(): return DetailsCategory default: @@ -103,6 +106,7 @@ var serviceCategories = map[ServiceType]map[CategoryType]struct{}{ SharePointService: { LibrariesCategory: {}, ListsCategory: {}, + PagesCategory: {}, }, } diff --git a/src/pkg/path/resource_path_test.go b/src/pkg/path/resource_path_test.go index 4ccfd03f8..c3655d19c 100644 --- a/src/pkg/path/resource_path_test.go +++ b/src/pkg/path/resource_path_test.go @@ -116,6 +116,13 @@ var ( return pb.ToDataLayerSharePointPath(tenant, site, path.ListsCategory, isItem) }, }, + { + service: path.SharePointService, + category: path.PagesCategory, + pathFunc: func(pb *path.Builder, tenant, site string, isItem bool) (path.Path, error) { + return pb.ToDataLayerSharePointPath(tenant, site, path.PagesCategory, isItem) + }, + }, } ) @@ -300,6 +307,13 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() { expectedService: path.SharePointMetadataService, check: assert.NoError, }, + { + name: "Passes", + service: path.SharePointService, + category: path.PagesCategory, + expectedService: path.SharePointMetadataService, + check: assert.NoError, + }, } for _, test := range table { From 32b11cdca83f58201dc680e760da62d6ecbab1a1 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 11 Jan 2023 16:00:41 -0800 Subject: [PATCH 09/38] Dedupe collection items for OneDrive (#2118) ## Description Under some circumstances items can be returned multiple times when iterating through the endpoint. This dedupes items within a single collection. It does not address the following: * item being removed from the collection during iteration * item appearing multiple times in different collections ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #1954 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/onedrive/collection.go | 5 +- .../connector/onedrive/collection_test.go | 53 +++++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 8e4e2e3f0..1f985162b 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -49,7 +49,7 @@ type Collection struct { // represents folderPath path.Path // M365 IDs of file items within this collection - driveItems []models.DriveItemable + driveItems map[string]models.DriveItemable // M365 ID of the drive this collection was created from driveID string source driveSource @@ -79,6 +79,7 @@ func NewCollection( ) *Collection { c := &Collection{ folderPath: folderPath, + driveItems: map[string]models.DriveItemable{}, driveID: driveID, source: source, service: service, @@ -101,7 +102,7 @@ func NewCollection( // Adds an itemID to the collection // This will make it eligible to be populated func (oc *Collection) Add(item models.DriveItemable) { - oc.driveItems = append(oc.driveItems, item) + oc.driveItems[*item.GetId()] = item } // Items() returns the channel containing M365 Exchange objects diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index a5c815f14..e55675618 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -61,14 +61,16 @@ func (suite *CollectionUnitTestSuite) TestCollection() { ) table := []struct { - name string - source driveSource - itemReader itemReaderFunc - infoFrom func(*testing.T, details.ItemInfo) (string, string) + name string + numInstances int + source driveSource + itemReader itemReaderFunc + infoFrom func(*testing.T, details.ItemInfo) (string, string) }{ { - name: "oneDrive", - source: OneDriveSource, + name: "oneDrive, no duplicates", + numInstances: 1, + source: OneDriveSource, itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName}}, io.NopCloser(bytes.NewReader(testItemData)), @@ -80,8 +82,37 @@ func (suite *CollectionUnitTestSuite) TestCollection() { }, }, { - name: "sharePoint", - source: SharePointSource, + name: "oneDrive, duplicates", + numInstances: 3, + source: OneDriveSource, + itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { + return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName}}, + io.NopCloser(bytes.NewReader(testItemData)), + nil + }, + infoFrom: func(t *testing.T, dii details.ItemInfo) (string, string) { + require.NotNil(t, dii.OneDrive) + return dii.OneDrive.ItemName, dii.OneDrive.ParentPath + }, + }, + { + name: "sharePoint, no duplicates", + numInstances: 1, + source: SharePointSource, + itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { + return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName}}, + io.NopCloser(bytes.NewReader(testItemData)), + nil + }, + infoFrom: func(t *testing.T, dii details.ItemInfo) (string, string) { + require.NotNil(t, dii.SharePoint) + return dii.SharePoint.ItemName, dii.SharePoint.ParentPath + }, + }, + { + name: "sharePoint, duplicates", + numInstances: 3, + source: SharePointSource, itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName}}, io.NopCloser(bytes.NewReader(testItemData)), @@ -119,7 +150,11 @@ func (suite *CollectionUnitTestSuite) TestCollection() { // Set a item reader, add an item and validate we get the item back mockItem := models.NewDriveItem() mockItem.SetId(&testItemID) - coll.Add(mockItem) + + for i := 0; i < test.numInstances; i++ { + coll.Add(mockItem) + } + coll.itemReader = test.itemReader // Read items from the collection From 4b1641e9786996783624c176636eb4f8d4a9cf1e Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Wed, 11 Jan 2023 16:28:39 -0800 Subject: [PATCH 10/38] Don't set updated in backup details for cached items (#2119) ## Description If an item is discovered to be cached in kopia (i.e. kopia-assisted incremental), set the backup details for the item to note that it was not updated. Cached items are discovered by checking the item path and mod time against the snapshots passed into kopia's Upload function ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * closes #2115 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/kopia/upload.go | 19 +++++++- src/internal/kopia/upload_test.go | 73 +++++++++++++++++++++--------- src/internal/kopia/wrapper_test.go | 12 ++++- 3 files changed, 79 insertions(+), 25 deletions(-) diff --git a/src/internal/kopia/upload.go b/src/internal/kopia/upload.go index c5e7a5c5a..a64715fc3 100644 --- a/src/internal/kopia/upload.go +++ b/src/internal/kopia/upload.go @@ -121,6 +121,7 @@ type itemDetails struct { info *details.ItemInfo repoPath path.Path prevPath path.Path + cached bool } type corsoProgress struct { @@ -179,7 +180,7 @@ func (cp *corsoProgress) FinishedFile(relativePath string, err error) { d.repoPath.String(), d.repoPath.ShortRef(), parent.ShortRef(), - true, + !d.cached, *d.info, ) @@ -187,7 +188,7 @@ func (cp *corsoProgress) FinishedFile(relativePath string, err error) { cp.deets.AddFoldersForItem( folders, *d.info, - true, // itemUpdated = true + !d.cached, ) } @@ -199,6 +200,20 @@ func (cp *corsoProgress) FinishedHashingFile(fname string, bs int64) { atomic.AddInt64(&cp.totalBytes, bs) } +// Kopia interface function used as a callback when kopia detects a previously +// uploaded file that matches the current file and skips uploading the new +// (duplicate) version. +func (cp *corsoProgress) CachedFile(fname string, size int64) { + defer cp.UploadProgress.CachedFile(fname, size) + + d := cp.get(fname) + if d == nil { + return + } + + d.cached = true +} + func (cp *corsoProgress) put(k string, v *itemDetails) { cp.mu.Lock() defer cp.mu.Unlock() diff --git a/src/internal/kopia/upload_test.go b/src/internal/kopia/upload_test.go index c382bb8ca..a3a865cd8 100644 --- a/src/internal/kopia/upload_test.go +++ b/src/internal/kopia/upload_test.go @@ -433,29 +433,58 @@ var finishedFileTable = []struct { } func (suite *CorsoProgressUnitSuite) TestFinishedFile() { - for _, test := range finishedFileTable { - suite.T().Run(test.name, func(t *testing.T) { - bd := &details.Builder{} - cp := corsoProgress{ - UploadProgress: &snapshotfs.NullUploadProgress{}, - deets: bd, - pending: map[string]*itemDetails{}, + table := []struct { + name string + cached bool + }{ + { + name: "all updated", + cached: false, + }, + { + name: "all cached", + cached: true, + }, + } + + for _, cachedTest := range table { + suite.T().Run(cachedTest.name, func(outerT *testing.T) { + for _, test := range finishedFileTable { + outerT.Run(test.name, func(t *testing.T) { + bd := &details.Builder{} + cp := corsoProgress{ + UploadProgress: &snapshotfs.NullUploadProgress{}, + deets: bd, + pending: map[string]*itemDetails{}, + } + + ci := test.cachedItems(suite.targetFileName, suite.targetFilePath) + + for k, v := range ci { + cp.put(k, v.info) + } + + require.Len(t, cp.pending, len(ci)) + + for k, v := range ci { + if cachedTest.cached { + cp.CachedFile(k, 42) + } + + cp.FinishedFile(k, v.err) + } + + assert.Empty(t, cp.pending) + + entries := bd.Details().Entries + + assert.Len(t, entries, test.expectedNumEntries) + + for _, entry := range entries { + assert.Equal(t, !cachedTest.cached, entry.Updated) + } + }) } - - ci := test.cachedItems(suite.targetFileName, suite.targetFilePath) - - for k, v := range ci { - cp.put(k, v.info) - } - - require.Len(t, cp.pending, len(ci)) - - for k, v := range ci { - cp.FinishedFile(k, v.err) - } - - assert.Empty(t, cp.pending) - assert.Len(t, bd.Details().Entries, test.expectedNumEntries) }) } } diff --git a/src/internal/kopia/wrapper_test.go b/src/internal/kopia/wrapper_test.go index 07fa78567..3654d8846 100644 --- a/src/internal/kopia/wrapper_test.go +++ b/src/internal/kopia/wrapper_test.go @@ -241,16 +241,20 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections() { name string expectedUploadedFiles int expectedCachedFiles int + // Whether entries in the resulting details should be marked as updated. + deetsUpdated bool }{ { name: "Uncached", expectedUploadedFiles: 47, expectedCachedFiles: 0, + deetsUpdated: true, }, { name: "Cached", expectedUploadedFiles: 0, expectedCachedFiles: 47, + deetsUpdated: false, }, } @@ -274,13 +278,19 @@ func (suite *KopiaIntegrationSuite) TestBackupCollections() { assert.Equal(t, 0, stats.IgnoredErrorCount) assert.Equal(t, 0, stats.ErrorCount) assert.False(t, stats.Incomplete) + // 47 file and 6 folder entries. + details := deets.Details().Entries assert.Len( t, - deets.Details().Entries, + details, test.expectedUploadedFiles+test.expectedCachedFiles+6, ) + for _, entry := range details { + assert.Equal(t, test.deetsUpdated, entry.Updated) + } + checkSnapshotTags( t, suite.ctx, From cfe65499870527b72133c35f384c8ede19f250f0 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Wed, 11 Jan 2023 19:50:26 -0800 Subject: [PATCH 11/38] Re-Enable Kopia-assisted incrementals for OneDrive (#2126) ## Description This addresses the deadlock in the item progress reader by deferring the reader creation to when the first read is issued for the item ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [x] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #1702 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/go.mod | 1 + src/go.sum | 2 ++ src/internal/connector/onedrive/collection.go | 26 ++++++++++--------- .../connector/onedrive/collection_test.go | 15 ++++++++--- src/internal/operations/backup.go | 5 ++++ src/pkg/backup/details/details.go | 4 +-- 6 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/go.mod b/src/go.mod index 6634c617b..7a55c7c13 100644 --- a/src/go.mod +++ b/src/go.mod @@ -19,6 +19,7 @@ require ( github.com/microsoftgraph/msgraph-sdk-go-core v0.31.1 github.com/pkg/errors v0.9.1 github.com/rudderlabs/analytics-go v3.3.3+incompatible + github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.14.0 diff --git a/src/go.sum b/src/go.sum index be4a51dde..f17de6887 100644 --- a/src/go.sum +++ b/src/go.sum @@ -357,6 +357,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1 h1:lQ3JvmcVO1/AMFbabvUSJ4YtJRpEAX9Qza73p5j03sw= +github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1/go.mod h1:4aKqcbhASNqjbrG0h9BmkzcWvPJGxbef4B+j0XfFrZo= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 1f985162b..22ac8d746 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -9,6 +9,7 @@ import ( "time" "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/spatialcurrent/go-lazy/pkg/lazy" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" @@ -34,11 +35,10 @@ const ( ) var ( - _ data.Collection = &Collection{} - _ data.Stream = &Item{} - _ data.StreamInfo = &Item{} - // TODO(ashmrtn): Uncomment when #1702 is resolved. - //_ data.StreamModTime = &Item{} + _ data.Collection = &Collection{} + _ data.Stream = &Item{} + _ data.StreamInfo = &Item{} + _ data.StreamModTime = &Item{} ) // Collection represents a set of OneDrive objects retrieved from M365 @@ -158,10 +158,9 @@ func (od *Item) Info() details.ItemInfo { return od.info } -// TODO(ashmrtn): Uncomment when #1702 is resolved. -//func (od *Item) ModTime() time.Time { -// return od.info.Modified -//} +func (od *Item) ModTime() time.Time { + return od.info.Modified() +} // populateItems iterates through items added to the collection // and uses the collection `itemReader` to read the item @@ -253,8 +252,11 @@ func (oc *Collection) populateItems(ctx context.Context) { itemSize = itemInfo.OneDrive.Size } - progReader, closer := observe.ItemProgress(itemData, observe.ItemBackupMsg, itemName, itemSize) - go closer() + itemReader := lazy.NewLazyReadCloser(func() (io.ReadCloser, error) { + progReader, closer := observe.ItemProgress(itemData, observe.ItemBackupMsg, itemName, itemSize) + go closer() + return progReader, nil + }) // Item read successfully, add to collection atomic.AddInt64(&itemsRead, 1) @@ -263,7 +265,7 @@ func (oc *Collection) populateItems(ctx context.Context) { oc.data <- &Item{ id: itemName, - data: progReader, + data: itemReader, info: itemInfo, } folderProgress <- struct{}{} diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index e55675618..a19021ff7 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -7,6 +7,7 @@ import ( "io" "sync" "testing" + "time" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" "github.com/microsoftgraph/msgraph-sdk-go/models" @@ -58,6 +59,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { testItemID = "fakeItemID" testItemName = "itemName" testItemData = []byte("testdata") + now = time.Now() ) table := []struct { @@ -72,7 +74,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { numInstances: 1, source: OneDriveSource, itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { - return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName}}, + return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), nil }, @@ -86,7 +88,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { numInstances: 3, source: OneDriveSource, itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { - return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName}}, + return details.ItemInfo{OneDrive: &details.OneDriveInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), nil }, @@ -100,7 +102,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { numInstances: 1, source: SharePointSource, itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { - return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName}}, + return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), nil }, @@ -114,7 +116,7 @@ func (suite *CollectionUnitTestSuite) TestCollection() { numInstances: 3, source: SharePointSource, itemReader: func(context.Context, models.DriveItemable) (details.ItemInfo, io.ReadCloser, error) { - return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName}}, + return details.ItemInfo{SharePoint: &details.SharePointInfo{ItemName: testItemName, Modified: now}}, io.NopCloser(bytes.NewReader(testItemData)), nil }, @@ -176,6 +178,11 @@ func (suite *CollectionUnitTestSuite) TestCollection() { readItemInfo := readItem.(data.StreamInfo) 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) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index d4d3056a3..f2e68aec5 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -218,6 +218,11 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { // checker to see if conditions are correct for incremental backup behavior such as // retrieving metadata like delta tokens and previous paths. func useIncrementalBackup(sel selectors.Selector, opts control.Options) bool { + // Delta-based incrementals currently only supported for Exchange + if sel.Service != selectors.ServiceExchange { + return false + } + return !opts.ToggleFeatures.DisableIncrementals } diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go index d407ca690..f244c72c9 100644 --- a/src/pkg/backup/details/details.go +++ b/src/pkg/backup/details/details.go @@ -173,7 +173,7 @@ func (b *Builder) AddFoldersForItem(folders []folderEntry, itemInfo ItemInfo, up } // Update the folder's size and modified time - itemModified := itemInfo.modified() + itemModified := itemInfo.Modified() folder.Info.Folder.Size += itemInfo.size() @@ -381,7 +381,7 @@ func (i ItemInfo) size() int64 { return 0 } -func (i ItemInfo) modified() time.Time { +func (i ItemInfo) Modified() time.Time { switch { case i.Exchange != nil: return i.Exchange.Modified From eda7a5d6751a1774175214b1d4ba5392679bf758 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Thu, 12 Jan 2023 09:57:59 +0530 Subject: [PATCH 12/38] Update CHANGELOG for v0.1.0 release (#2127) ## Description This entry accidentally got into the previous changelog version. It was added via https://github.com/alcionai/corso/pull/1987 ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [x] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [ ] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2c79930..650b773c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Folder entries in backup details now indicate whether an item in the hierarchy was updated - Incremental backup support for exchange is now enabled by default. ### Changed @@ -31,7 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Folder entries in backup details now indicate whether an item in the hierarchy was updated - Incremental backup support for Exchange ([#1777](https://github.com/alcionai/corso/issues/1777)). This is currently enabled by specifying the `--enable-incrementals` with the `backup create` command. This functionality will be enabled by default in an upcoming release. - Folder entries in backup details now include size and modified time for the hierarchy ([#1896](https://github.com/alcionai/corso/issues/1896)) From 5d70b4b3a6e4cf60c95bac6997bd4c5c586de496 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Wed, 11 Jan 2023 23:40:49 -0800 Subject: [PATCH 13/38] Disable kopia incrementals for OneDrive (#2132) ## Description Kopia-assisted incremental backups do not appear to have the correct file size set on restore ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/onedrive/collection.go | 14 +++++++------- src/internal/connector/onedrive/collection_test.go | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 22ac8d746..4571b15e2 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -35,10 +35,10 @@ const ( ) var ( - _ data.Collection = &Collection{} - _ data.Stream = &Item{} - _ data.StreamInfo = &Item{} - _ data.StreamModTime = &Item{} + _ data.Collection = &Collection{} + _ data.Stream = &Item{} + _ data.StreamInfo = &Item{} + // _ data.StreamModTime = &Item{} ) // Collection represents a set of OneDrive objects retrieved from M365 @@ -158,9 +158,9 @@ func (od *Item) Info() details.ItemInfo { return od.info } -func (od *Item) ModTime() time.Time { - return od.info.Modified() -} +// func (od *Item) ModTime() time.Time { +// return od.info.Modified() +// } // populateItems iterates through items added to the collection // and uses the collection `itemReader` to read the item diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index a19021ff7..66378f09e 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -179,9 +179,9 @@ func (suite *CollectionUnitTestSuite) TestCollection() { assert.Equal(t, testItemName, readItem.UUID()) - require.Implements(t, (*data.StreamModTime)(nil), readItem) - mt := readItem.(data.StreamModTime) - assert.Equal(t, now, mt.ModTime()) + // 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) From 69d4d553030bbbeb5e032d0e652a11329b450f81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jan 2023 09:25:43 +0000 Subject: [PATCH 14/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.177=20to=201.44.178=20in=20/src=20(#?= =?UTF-8?q?2130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.177 to 1.44.178.
Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.178 (2023-01-11)

Service Client Updates

  • service/kendra: Updates service API and documentation
    • This release adds support to new document types - RTF, XML, XSLT, MS_EXCEL, CSV, JSON, MD
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.177&new-version=1.44.178)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 7a55c7c13..370759216 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,7 +6,7 @@ replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.2023011005 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/aws/aws-sdk-go v1.44.177 + github.com/aws/aws-sdk-go v1.44.178 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 diff --git a/src/go.sum b/src/go.sum index f17de6887..0929b338b 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.177 h1:ckMJhU5Gj+4Rta+bJIUiUd7jvHom84aim3zkGPblq0s= -github.com/aws/aws-sdk-go v1.44.177/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.178 h1:4igreoWPEA7xVLnOeSXLhDXTsTSPKQONZcQ3llWAJw0= +github.com/aws/aws-sdk-go v1.44.178/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= From 35363c68fc1f816d1430b2d9efb5f4aa68275ab7 Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Thu, 12 Jan 2023 11:08:30 -0800 Subject: [PATCH 15/38] Remove credential provider option and revert to kopia upstream (#2135) ## Description This PR removes the option to specify a credential provider since that option didn't pan out. Also reverts back to using kopia upstream (switch to HEAD from `main` in prep for an upcoming fix) ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [x] :green_heart: E2E --- src/go.mod | 49 ++++++++++- src/go.sum | 116 ++++++++++++++++++++++++++ src/internal/kopia/s3.go | 36 -------- src/pkg/repository/repository_test.go | 19 ----- src/pkg/storage/storage.go | 18 ---- 5 files changed, 162 insertions(+), 76 deletions(-) diff --git a/src/go.mod b/src/go.mod index 370759216..134be6ff5 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,15 +2,13 @@ module github.com/alcionai/corso/src go 1.19 -replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f - require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/aws/aws-sdk-go v1.44.178 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 - github.com/kopia/kopia v0.12.0 + github.com/kopia/kopia v0.12.2-0.20221229232524-ba938cf58cc8 github.com/microsoft/kiota-abstractions-go v0.15.2 github.com/microsoft/kiota-authentication-azure-go v0.5.0 github.com/microsoft/kiota-http-go v0.11.0 @@ -34,22 +32,67 @@ require ( ) require ( + cloud.google.com/go v0.105.0 // indirect + cloud.google.com/go/compute v1.13.0 // indirect + cloud.google.com/go/compute/metadata v0.2.2 // indirect + cloud.google.com/go/iam v0.8.0 // indirect + cloud.google.com/go/storage v1.28.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 // indirect + github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 // indirect + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect + github.com/alessio/shellescape v1.4.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/godbus/dbus/v5 v5.0.6 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + github.com/google/readahead v0.0.0-20161222183148-eaceba169032 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/magiconair/properties v1.8.6 // 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 v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pkg/sftp v1.13.5 // indirect + github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f // indirect github.com/subosito/gotenv v1.4.1 // indirect + github.com/tg123/go-htpasswd v1.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect + github.com/xhit/go-str2duration v1.2.0 // indirect + github.com/zalando/go-keyring v0.2.1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.11.1 // indirect + go.opentelemetry.io/otel/sdk v1.11.2 // indirect + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect + golang.org/x/term v0.4.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.104.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index 0929b338b..0dadeeaef 100644 --- a/src/go.sum +++ b/src/go.sum @@ -17,14 +17,24 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k= +cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -35,31 +45,47 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 h1:sVW/AFBTGyJxDaMYlq0ct3jUXTtj12tQ6zE2GZUgVQw= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 h1:t/W5MYAuQy81cvM8VUNfRLzhtKpXhVUAN7Cd7KVbTyc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 h1:Px2UA+2RvSSvv+RvJNuUB6n7rs5Wsel4dXLe90Um2n4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 h1:VgSJlZH5u0k2qxSpqyghcFQKmvYckj46uymKK5XzkBM= github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f h1:RsXSF0CgmtCMHTew/O7QJJjUoqb/X3rJqI0qRXve/Lc= github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f/go.mod h1:yzJV11S6N6XMboXt7oCO6Jy2jJHPeSMtA+KOJ9Y1548= +github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 h1:sdTv2mX7pjU3JyOTktjOYeT/bjlllCzdRqzKFjgLDvs= +github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166/go.mod h1:OaxaNlTGWmknTg9pwAYYTkaU/cXZYeLGizE8aey/CXU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc= +github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 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.178 h1:4igreoWPEA7xVLnOeSXLhDXTsTSPKQONZcQ3llWAJw0= @@ -89,11 +115,15 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= @@ -104,6 +134,12 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -125,14 +161,20 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -167,9 +209,11 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -184,16 +228,26 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/readahead v0.0.0-20161222183148-eaceba169032 h1:6Be3nkuJFyRfCgr6qTIzmRp8y9QwDIbqy/nYr9WDPos= +github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ= +github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -206,6 +260,7 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= @@ -240,21 +295,31 @@ github.com/klauspost/reedsolomon v1.11.3/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747h github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 h1:WP5VfIQL7AaYkO4zTNSCsVOawTzudbc4tvLojvg0RKc= +github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= +github.com/kopia/kopia v0.12.1 h1:Hl3Jzi4bFfhkTHn7zHR0geYnvDXLLSt2+snRBSATHjE= +github.com/kopia/kopia v0.12.1/go.mod h1:k08Y6oGJ6iehdKuQtH/pJRKnEGiKLZ7fYBWuRG+dmKw= +github.com/kopia/kopia v0.12.2-0.20221229232524-ba938cf58cc8 h1:z9P5uu53BWBRZGVhMTqPDwlq15s13RHc8AqhORX6/hw= +github.com/kopia/kopia v0.12.2-0.20221229232524-ba938cf58cc8/go.mod h1:yzJV11S6N6XMboXt7oCO6Jy2jJHPeSMtA+KOJ9Y1548= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -296,6 +361,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= @@ -312,9 +378,17 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= +github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= +github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -357,6 +431,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1 h1:lQ3JvmcVO1/AMFbabvUSJ4YtJRpEAX9Qza73p5j03sw= github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1/go.mod h1:4aKqcbhASNqjbrG0h9BmkzcWvPJGxbef4B+j0XfFrZo= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= @@ -384,9 +460,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f h1:SLJx0nHhb2ZLlYNMAbrYsjwmVwXx4yRT48lNIxOp7ts= +github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0= +github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -403,6 +482,8 @@ github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oa github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vbauerster/mpb/v8 v8.1.4 h1:MOcLTIbbAA892wVjRiuFHa1nRlNvifQMDVh12Bq/xIs= github.com/vbauerster/mpb/v8 v8.1.4/go.mod h1:2fRME8lCLU9gwJwghZb1bO9A3Plc8KPeQ/ayGj+Ek4I= +github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= +github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= @@ -413,6 +494,7 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= +github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -425,8 +507,18 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0= go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= +go.opentelemetry.io/otel/exporters/jaeger v1.10.0 h1:7W3aVVjEYayu/GOqOVF4mbTvnCuxF1wWu3eRxFGQXvw= +go.opentelemetry.io/otel/exporters/jaeger v1.10.0/go.mod h1:n9IGyx0fgyXXZ/i0foLHNxtET9CzXHzZeKCucvRBFgA= +go.opentelemetry.io/otel/exporters/jaeger v1.11.1 h1:F9Io8lqWdGyIbY3/SOGki34LX/l+7OL0gXNxjqwcbuQ= +go.opentelemetry.io/otel/exporters/jaeger v1.11.1/go.mod h1:lRa2w3bQ4R4QN6zYsDgy7tEezgoKEu7Ow2g35Y75+KI= +go.opentelemetry.io/otel/sdk v1.10.0 h1:jZ6K7sVn04kk/3DNUdJ4mqRlGDiXAVuIG+MMENpTNdY= +go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE= +go.opentelemetry.io/otel/sdk v1.11.2 h1:GF4JoaEx7iihdMFu30sOyRx52HDHOkl9xQ8SMqNXUiU= +go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU= go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0= go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -438,14 +530,17 @@ go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95a go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -516,12 +611,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -540,6 +639,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -598,6 +699,9 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -614,6 +718,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -682,6 +788,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -701,12 +809,17 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.104.0 h1:KBfmLRqdZEbwQleFlSLnzpQJwhjpmNOk4cKQIBDZ9mg= +google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -783,9 +896,12 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 h1:2TSTkQ8PMvGOD5eeqqRVv6Z9+BYI+bowK97RCr3W+9M= +gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/internal/kopia/s3.go b/src/internal/kopia/s3.go index 65f0f538e..3d0f3144e 100644 --- a/src/internal/kopia/s3.go +++ b/src/internal/kopia/s3.go @@ -3,10 +3,8 @@ package kopia import ( "context" - awscreds "github.com/aws/aws-sdk-go/aws/credentials" "github.com/kopia/kopia/repo/blob" "github.com/kopia/kopia/repo/blob/s3" - miniocreds "github.com/minio/minio-go/v7/pkg/credentials" "github.com/alcionai/corso/src/pkg/storage" ) @@ -32,41 +30,7 @@ func s3BlobStorage(ctx context.Context, s storage.Storage) (blob.Storage, error) Prefix: cfg.Prefix, DoNotUseTLS: cfg.DoNotUseTLS, DoNotVerifyTLS: cfg.DoNotVerifyTLS, - Creds: credentials(s.Creds), } return s3.New(ctx, &opts, false) } - -// credentials converts an AWS Credential to a Minio credential (which Kopia uses) -func credentials(creds *awscreds.Credentials) *miniocreds.Credentials { - if creds == nil { - return nil - } - - return miniocreds.New(&minioProvider{creds: creds}) -} - -// minioProvider is a shim that implements the Minio `Provider` interface -// for an AWS credential -type minioProvider struct { - creds *awscreds.Credentials -} - -func (mp *minioProvider) Retrieve() (miniocreds.Value, error) { - v, err := mp.creds.Get() - if err != nil { - return miniocreds.Value{}, err - } - - return miniocreds.Value{ - AccessKeyID: v.AccessKeyID, - SecretAccessKey: v.SecretAccessKey, - SessionToken: v.SessionToken, - SignerType: miniocreds.SignatureV4, - }, nil -} - -func (mp *minioProvider) IsExpired() bool { - return mp.creds.IsExpired() -} diff --git a/src/pkg/repository/repository_test.go b/src/pkg/repository/repository_test.go index 39b9cfd47..c84938615 100644 --- a/src/pkg/repository/repository_test.go +++ b/src/pkg/repository/repository_test.go @@ -3,7 +3,6 @@ package repository_test import ( "testing" - awscreds "github.com/aws/aws-sdk-go/aws/credentials" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -11,7 +10,6 @@ import ( "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/control" - "github.com/alcionai/corso/src/pkg/credentials" "github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/storage" @@ -142,23 +140,6 @@ func (suite *RepositoryIntegrationSuite) TestInitialize() { } } -func (suite *RepositoryIntegrationSuite) TestInitializeCustomCredentials() { - ctx, flush := tester.NewContext() - defer flush() - - st := tester.NewPrefixedS3Storage(suite.T()) - - ak := credentials.GetAWS(map[string]string{}) - st.Creds = awscreds.NewStaticCredentials(ak.AccessKey, ak.SecretKey, ak.SessionToken) - - r, err := repository.Initialize(ctx, account.Account{}, st, control.Options{}) - require.NoError(suite.T(), err) - - defer func() { - r.Close(ctx) - }() -} - func (suite *RepositoryIntegrationSuite) TestConnect() { ctx, flush := tester.NewContext() defer flush() diff --git a/src/pkg/storage/storage.go b/src/pkg/storage/storage.go index cc5585ed1..029e29596 100644 --- a/src/pkg/storage/storage.go +++ b/src/pkg/storage/storage.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/alcionai/corso/src/internal/common" ) @@ -37,7 +35,6 @@ const ( type Storage struct { Provider storageProvider Config map[string]string - Creds *credentials.Credentials } // NewStorage aggregates all the supplied configurations into a single configuration. @@ -50,21 +47,6 @@ func NewStorage(p storageProvider, cfgs ...common.StringConfigurer) (Storage, er }, err } -// NewStorageWithCredentials supports specifying a credential container -func NewStorageWithCredentials( - p storageProvider, - creds *credentials.Credentials, - cfgs ...common.StringConfigurer, -) (Storage, error) { - cs, err := common.UnionStringConfigs(cfgs...) - - return Storage{ - Provider: p, - Config: cs, - Creds: creds, - }, err -} - // Helper for parsing the values in a config object. // If the value is nil or not a string, returns an empty string. func orEmptyString(v any) string { From 8af3945ed1f46736e24205211166c80c3113a8a1 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Thu, 12 Jan 2023 16:12:52 -0800 Subject: [PATCH 16/38] Update to fork of kopia with patch (#2136) ## Description Fix file size issue when the file is cached in kopia. Fix requires updating kopia version to a commit that has a patch for the bug in question. ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2133 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/go.mod | 51 +++-------------------- src/go.sum | 120 ++--------------------------------------------------- 2 files changed, 9 insertions(+), 162 deletions(-) diff --git a/src/go.mod b/src/go.mod index 134be6ff5..6de239001 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,6 +2,8 @@ module github.com/alcionai/corso/src go 1.19 +replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.20230112200734-ac706ef83a1c + require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/aws/aws-sdk-go v1.44.178 @@ -32,67 +34,24 @@ require ( ) require ( - cloud.google.com/go v0.105.0 // indirect - cloud.google.com/go/compute v1.13.0 // indirect - cloud.google.com/go/compute/metadata v0.2.2 // indirect - cloud.google.com/go/iam v0.8.0 // indirect - cloud.google.com/go/storage v1.28.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 // indirect - github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 // indirect - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect - github.com/alessio/shellescape v1.4.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/felixge/fgprof v0.9.3 // indirect - github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c // indirect + github.com/dnaeon/go-vcr v1.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/godbus/dbus/v5 v5.0.6 // indirect - github.com/gofrs/flock v0.8.1 // indirect - github.com/golang/glog v1.0.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect - github.com/google/readahead v0.0.0-20161222183148-eaceba169032 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect - github.com/googleapis/gax-go/v2 v2.7.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect - github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 // indirect - github.com/kr/fs v0.1.0 // indirect github.com/magiconair/properties v1.8.6 // 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 v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect - github.com/pkg/profile v1.7.0 // indirect - github.com/pkg/sftp v1.13.5 // indirect - github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 // indirect - github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f // indirect github.com/subosito/gotenv v1.4.1 // indirect - github.com/tg123/go-htpasswd v1.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect - github.com/xhit/go-str2duration v1.2.0 // indirect - github.com/zalando/go-keyring v0.2.1 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel/exporters/jaeger v1.11.1 // indirect - go.opentelemetry.io/otel/sdk v1.11.2 // indirect - golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect - golang.org/x/term v0.4.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.104.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -130,7 +89,7 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/kiota-serialization-text-go v0.6.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minio-go/v7 v7.0.45 + github.com/minio/minio-go/v7 v7.0.45 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/src/go.sum b/src/go.sum index 0dadeeaef..97aa7007a 100644 --- a/src/go.sum +++ b/src/go.sum @@ -17,24 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k= -cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -45,47 +35,31 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 h1:sVW/AFBTGyJxDaMYlq0ct3jUXTtj12tQ6zE2GZUgVQw= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 h1:t/W5MYAuQy81cvM8VUNfRLzhtKpXhVUAN7Cd7KVbTyc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 h1:Px2UA+2RvSSvv+RvJNuUB6n7rs5Wsel4dXLe90Um2n4= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 h1:VgSJlZH5u0k2qxSpqyghcFQKmvYckj46uymKK5XzkBM= github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= -github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= -github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f h1:RsXSF0CgmtCMHTew/O7QJJjUoqb/X3rJqI0qRXve/Lc= -github.com/alcionai/kopia v0.10.8-0.20230110051604-a7105dc2a75f/go.mod h1:yzJV11S6N6XMboXt7oCO6Jy2jJHPeSMtA+KOJ9Y1548= -github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166 h1:sdTv2mX7pjU3JyOTktjOYeT/bjlllCzdRqzKFjgLDvs= -github.com/alecthomas/kingpin v1.3.8-0.20220615105907-eae6867f4166/go.mod h1:OaxaNlTGWmknTg9pwAYYTkaU/cXZYeLGizE8aey/CXU= +github.com/alcionai/kopia v0.10.8-0.20230112200734-ac706ef83a1c h1:uUcdEZ4sz7kRYVWB3K49MBHdICRyXCVAzd4ZiY3lvo0= +github.com/alcionai/kopia v0.10.8-0.20230112200734-ac706ef83a1c/go.mod h1:yzJV11S6N6XMboXt7oCO6Jy2jJHPeSMtA+KOJ9Y1548= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc= -github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 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.178 h1:4igreoWPEA7xVLnOeSXLhDXTsTSPKQONZcQ3llWAJw0= @@ -115,14 +89,11 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -134,12 +105,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4= -github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -161,20 +126,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -209,11 +168,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -228,26 +185,16 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/readahead v0.0.0-20161222183148-eaceba169032 h1:6Be3nkuJFyRfCgr6qTIzmRp8y9QwDIbqy/nYr9WDPos= -github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ= -github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -260,7 +207,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= @@ -295,12 +241,6 @@ github.com/klauspost/reedsolomon v1.11.3/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747h github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 h1:WP5VfIQL7AaYkO4zTNSCsVOawTzudbc4tvLojvg0RKc= -github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7/go.mod h1:eWer4rx9P8lJo2eKc+Q7AZ1dE1x1hJNdkbDFPzMu1Hw= -github.com/kopia/kopia v0.12.1 h1:Hl3Jzi4bFfhkTHn7zHR0geYnvDXLLSt2+snRBSATHjE= -github.com/kopia/kopia v0.12.1/go.mod h1:k08Y6oGJ6iehdKuQtH/pJRKnEGiKLZ7fYBWuRG+dmKw= -github.com/kopia/kopia v0.12.2-0.20221229232524-ba938cf58cc8 h1:z9P5uu53BWBRZGVhMTqPDwlq15s13RHc8AqhORX6/hw= -github.com/kopia/kopia v0.12.2-0.20221229232524-ba938cf58cc8/go.mod h1:yzJV11S6N6XMboXt7oCO6Jy2jJHPeSMtA+KOJ9Y1548= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -309,17 +249,14 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -378,17 +315,9 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= -github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= -github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 h1:xoIK0ctDddBMnc74udxJYBqlo9Ylnsp1waqjLsnef20= -github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -431,8 +360,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= -github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1 h1:lQ3JvmcVO1/AMFbabvUSJ4YtJRpEAX9Qza73p5j03sw= github.com/spatialcurrent/go-lazy v0.0.0-20211115014721-47315cc003d1/go.mod h1:4aKqcbhASNqjbrG0h9BmkzcWvPJGxbef4B+j0XfFrZo= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= @@ -460,12 +387,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f h1:SLJx0nHhb2ZLlYNMAbrYsjwmVwXx4yRT48lNIxOp7ts= -github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tg123/go-htpasswd v1.2.0 h1:UKp34m9H467/xklxUxU15wKRru7fwXoTojtxg25ITF0= -github.com/tg123/go-htpasswd v1.2.0/go.mod h1:h7IzlfpvIWnVJhNZ0nQ9HaFxHb7pn5uFJYLlEUJa2sM= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -482,8 +406,6 @@ github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oa github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vbauerster/mpb/v8 v8.1.4 h1:MOcLTIbbAA892wVjRiuFHa1nRlNvifQMDVh12Bq/xIs= github.com/vbauerster/mpb/v8 v8.1.4/go.mod h1:2fRME8lCLU9gwJwghZb1bO9A3Plc8KPeQ/ayGj+Ek4I= -github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= -github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= @@ -494,7 +416,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= -github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -507,18 +428,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/otel v1.11.2 h1:YBZcQlsVekzFsFbjygXMOXSs6pialIZxcjfO/mBDmR0= go.opentelemetry.io/otel v1.11.2/go.mod h1:7p4EUV+AqgdlNV9gL97IgUZiVR3yrFXYo53f9BM3tRI= -go.opentelemetry.io/otel/exporters/jaeger v1.10.0 h1:7W3aVVjEYayu/GOqOVF4mbTvnCuxF1wWu3eRxFGQXvw= -go.opentelemetry.io/otel/exporters/jaeger v1.10.0/go.mod h1:n9IGyx0fgyXXZ/i0foLHNxtET9CzXHzZeKCucvRBFgA= -go.opentelemetry.io/otel/exporters/jaeger v1.11.1 h1:F9Io8lqWdGyIbY3/SOGki34LX/l+7OL0gXNxjqwcbuQ= -go.opentelemetry.io/otel/exporters/jaeger v1.11.1/go.mod h1:lRa2w3bQ4R4QN6zYsDgy7tEezgoKEu7Ow2g35Y75+KI= -go.opentelemetry.io/otel/sdk v1.10.0 h1:jZ6K7sVn04kk/3DNUdJ4mqRlGDiXAVuIG+MMENpTNdY= -go.opentelemetry.io/otel/sdk v1.10.0/go.mod h1:vO06iKzD5baltJz1zarxMCNHFpUlUiOy4s65ECtn6kE= -go.opentelemetry.io/otel/sdk v1.11.2 h1:GF4JoaEx7iihdMFu30sOyRx52HDHOkl9xQ8SMqNXUiU= -go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU= go.opentelemetry.io/otel/trace v1.11.2 h1:Xf7hWSF2Glv0DE3MH7fBHvtpSBsjcBUe5MYAmZM/+y0= go.opentelemetry.io/otel/trace v1.11.2/go.mod h1:4N+yC7QEz7TTsG9BSRLNAa63eg5E06ObSbKPmxQ/pKA= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -530,17 +441,14 @@ go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95a go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -611,16 +519,12 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -639,8 +543,6 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -699,9 +601,6 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -718,8 +617,6 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -788,8 +685,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -809,17 +704,12 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.104.0 h1:KBfmLRqdZEbwQleFlSLnzpQJwhjpmNOk4cKQIBDZ9mg= -google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -894,14 +784,12 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216 h1:2TSTkQ8PMvGOD5eeqqRVv6Z9+BYI+bowK97RCr3W+9M= -gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From edd08269f66ca40ced34ef6907de125e389b3758 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Thu, 12 Jan 2023 16:35:45 -0800 Subject: [PATCH 17/38] Reenable OneDrive kopia-assisted incrementals (#2137) ## Description Reenable kopia-assisted incrementals now that #2136 contains the bugfix patch ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * closes #2133 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/onedrive/collection.go | 14 +++++++------- src/internal/connector/onedrive/collection_test.go | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 4571b15e2..22ac8d746 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -35,10 +35,10 @@ const ( ) var ( - _ data.Collection = &Collection{} - _ data.Stream = &Item{} - _ data.StreamInfo = &Item{} - // _ data.StreamModTime = &Item{} + _ data.Collection = &Collection{} + _ data.Stream = &Item{} + _ data.StreamInfo = &Item{} + _ data.StreamModTime = &Item{} ) // Collection represents a set of OneDrive objects retrieved from M365 @@ -158,9 +158,9 @@ func (od *Item) Info() details.ItemInfo { return od.info } -// func (od *Item) ModTime() time.Time { -// return od.info.Modified() -// } +func (od *Item) ModTime() time.Time { + return od.info.Modified() +} // populateItems iterates through items added to the collection // and uses the collection `itemReader` to read the item diff --git a/src/internal/connector/onedrive/collection_test.go b/src/internal/connector/onedrive/collection_test.go index 66378f09e..a19021ff7 100644 --- a/src/internal/connector/onedrive/collection_test.go +++ b/src/internal/connector/onedrive/collection_test.go @@ -179,9 +179,9 @@ func (suite *CollectionUnitTestSuite) TestCollection() { assert.Equal(t, testItemName, readItem.UUID()) - // require.Implements(t, (*data.StreamModTime)(nil), readItem) - // mt := readItem.(data.StreamModTime) - // assert.Equal(t, now, mt.ModTime()) + 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) From e57080fc3941bb3319dcccb2b481ce5afc30ebc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C4=8Dnica=20Mellifera?= Date: Thu, 12 Jan 2023 17:13:20 -0800 Subject: [PATCH 18/38] new blog post on backing up with Corso on your coffee break (#2006) ## Description new blog post on backing up with Corso on your coffee break ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [x] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :hamster: Trivial/Minor ## Issue(s) * # ## Test Plan - [ ] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E Co-authored-by: Niraj Tolia Co-authored-by: aviator-app[bot] <48659329+aviator-app[bot]@users.noreply.github.com> --- .../2023-1-4-backups-on-your-coffee-break.md | 87 ++++++++++++++++++ website/blog/images/coffee_break.jpg | Bin 0 -> 267446 bytes 2 files changed, 87 insertions(+) create mode 100644 website/blog/2023-1-4-backups-on-your-coffee-break.md create mode 100644 website/blog/images/coffee_break.jpg diff --git a/website/blog/2023-1-4-backups-on-your-coffee-break.md b/website/blog/2023-1-4-backups-on-your-coffee-break.md new file mode 100644 index 000000000..b7a9cb952 --- /dev/null +++ b/website/blog/2023-1-4-backups-on-your-coffee-break.md @@ -0,0 +1,87 @@ +--- +slug: backups-on-your-coffee-break +title: "How to Back Up Your Microsoft 365 Data During Your Coffee Break" +description: "A quick guide to using Corso for data backups" +authors: nica +tags: [corso, microsoft 365, backups] +date: 2023-1-12 +image: ./images/coffee_break.jpg +--- + +![Filled coffee cup by Kenny Louie from Vancouver, Canada -Coffee break, CC BY 2.0, https://commons.wikimedia.org/w/index.php?curid=24336256](./images/coffee_break.jpg) + +It’s 10:00 in the morning, and you need coffee and a snack. +You know you’re supposed to back up the company’s Microsoft 365 instance, but it takes so long! Surely a quick +break won’t matter. + +Wrong! While you were in the break room, +your organization was hit with a malware attack that wiped out many critical files and spreadsheets in minutes. +Now your cell phone’s ringing off the hook. +Slapping your forehead with the palm of your hand, you shout, +“If only backups were faster and easier!” + + + +Regular backups are increasingly important and must be prioritized; even over your coffee break. A recent study by +[Arlington Research](https://www.businesswire.com/news/home/20210511005132/en/An-Alarming-85-of-Organizations-Using-Microsoft-365-Have-Suffered-Email-Data-Breaches-Research-by-Egress-Reveals#:~:text=15%25%20of%20organizations%20using%20Microsoft,data%20in%20error%20via%20email.) +found that 85% of organizations using Microsoft 365 suffered email data breaches in the six months prior to May 2021. +And it’s not just malware that threatens to corrupt data; downtime can have equally devastating impacts. +[Two out of every five servers](https://www.veeam.com/blog/data-loss-2022.html) +experienced an outage over the past 12 months. +Data can also be lost or corrupted during poorly executed migrations or the cancellation of a software license +or by human error. And once it’s gone, it’s gone, unless you’ve backed it up. +Think you can just move stuff back out of the recycling bin? Think again. Ransomware will also clear your recycling bin, +even Microsoft recommends [emptying it out regularly](https://learn.microsoft.com/en-us/office365/servicedescriptions/sharepoint-online-service-description/sharepoint-online-limits). +Use of other tools like 'holds' [also have their limits](https://learn.microsoft.com/en-us/office365/servicedescriptions/sharepoint-online-service-description/sharepoint-online-limits#hold-limits) +(and really they're intended for e-discovery), +and are no substitutes for true backups. + +The question really is: why wouldn’t you back up your Microsoft 365 data? + +IDC estimates that [six out of every 10 organizations](https://www.dsm.net/idc-why-backup-for-office-365-is-essential) +don’t have a data protection plan for their Microsoft 365 data. +Why? Because, historically, Microsoft 365 backups have been slow, +tedious and expensive, requiring complex workflows and scripts, and constant supervision: + +- Companies often face physical limitations of their storage devices, such as servers, external hard drives, or other media. +They may have to choose what data to backup or compromise on how often they back it up. +- Backups can be time-consuming, especially without automation. +Often, someone has to monitor the process to address any issues that arise. With their to-do list growing day by day, +IT security teams must often prioritize more urgent work. +- Manual backups aren’t just slow and tedious, they’re unreliable. When work is busy, or when your employee’s stomach +is growling -it may be pushed to the bottom of the priority list. + +Considering these challenges, it’s clear to see why an IT security staffer might put backups on the back burner. + +## A Faster, Easier Way to Back up Your Data + +Fortunately, [Corso](https://corsobackup.io/), a free and open-source tool, is enabling IT administrators to backup all +their M365 data during their morning coffee break -or while their lunch is in the microwave. Here’s how: + +- Purpose-built for Microsoft 365, Corso provides comprehensive backup and restore workflows that slash backup time and overhead. +- It’s free: because Corso is 100% open-source. Flexible retention policies reduce storage costs, as well. Corso works +with any S3-compatible object storage system, including AWS, Google Cloud, Backblaze and Azure Blob. +- It’s fast! Corso doesn’t use unreliable scripts or workarounds. Instead, +its automated, high-throughput, high-tolerance backups feature end-to-end encryption, deduplication and compression. +Corso is written in Go, a modern programming language that came out of Google that has been purpose-built for systems programming. +A typical Corso backup takes just a few minutes- and you can drink your coffee while it’s running! + +How do you backup your data with Corso? It takes just a few minutes to get started. Check out the [Quick Start](https://corsobackup.io/docs/quickstart/) +guide for a step-by-step walk through: + +1. Download Corso + +1. Connect to Microsoft 365 + +1. Create a Corso repository + +1. Create your backup + +And here’s my [video](https://youtu.be/mlwfEbPqD94) showing how the steps take less than 4 minutes. + +Yep, that’s it. With these few steps, Corso protects your team’s data from accidental loss, deletion, server downtime, +security threats and ransomware. Don’t leave Microsoft 365 data protection to chance +-and use your coffee break to relax instead of +worry! + +Give [Corso](https://corsobackup.io/) a try, and then tell us what you think. Find the Corso community on [Discord](https://discord.gg/63DTTSnuhT). diff --git a/website/blog/images/coffee_break.jpg b/website/blog/images/coffee_break.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0c043227e68b00a5c5b68364375fa7cd393f0122 GIT binary patch literal 267446 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

Dw&X?B9)_96PH3 zd!v($O8laze74pBMB!j`w(frU*3n)4>kI4_cO4Qk!uGcBXKv^#^Hxy4lE1463DeFx z>MV>TxbbQP40c*8Ylr*SV_-VsAtLC*CqiZ7#w?pad)I@1IVS3bPebjdNECslH!(6HcOtIXo5i8_p5r3{r-Odz(7C0 zY351uc_%YK4-+lmpjNKqGc>7vM-+7gIzLazEy(ptuNK7lYDz%nDF&h9~9z?A(I-rjuRx4J@dzEjm7h%5@!OHTivvPannB4Tl%{JB_%*-KkY)B zt+H11gGJGGI}~7Z@n1D+Qg?0}d(2i@?QJZ+6mgC_)$8#>64#}3)Xz#30Uje)^XR0H`45*X^0uMlH0UK&s5sk`x;Eh2TyL2 zk`H>zxu_BdKvK63CWAMM4ny3`O$@w=NMta=J2q^1is5Z?ea!)R;v26rQhgxfJkqOM z)GG;8llqDt{q!T%IQOA)opv7>{tX?Uh~I1iN``yIF;9tBxb>L%tYx~8^ClJ3CU}}5 zm7xoOL`OqZr!cs}x-`yf_bFJ~;r0WJRg8GD;U{d63GY9xW}Yeu{N%?K%SDFPJjsd4 zs##}xWqMGqjq83TxOPtBM@p*W#Wx7X;~jHZ_U~B{CO{a9v1(%B655iLblfIE6rzwa3F3*h{vj-;ROG-7P6HPXuPXL&LXDDPc|(ZGkoDcKV2IvjPFCmRYWuaK{F% zqio-aE}B;=>k?*+w((us);&;9V_3_5RSto)f?_dUI*M8(q=gafL;8%XpdgTDi!0*&tWGt3qeE+cy~0fH zIj1YUQtjX_3}Oar&h2#-x}c2Ycej!KRX)Vs8+OlAt)S}|$3qF_xWb~o+XO354dQqCHJyDMlS|I{fo+p~H_?p$l zN0&n-fOQ|4?@@Ae`>Wtih z3KP>c<4eC&jHmjLqA&+~osPMAXy_?c}Z70UaaioU{20Hhx1+R)NomN%_b|ae(O4-JjkDDLB^suC-)6%{|Rt9P;>P)(w z$ygxO?OitJ0TWuI`!h4YlnAGaPDNOfm5DjU7vv>!V128i)D9@rvZe!2U`n@bcp`(i z(b#+)$Oj!n(>JY@4nKOBWk3RHi#zF{9yRdHZlmCvE5Jh8N~hYeTKDjJ%Iyj*G?0Ec zuWC^#FhtZGC7sC;E(|Gtg4y;^Us+#z)AUe&hM~bEiDd{{SOOPt69VhuZ?bXKVie z!QDFb3sPDD{8x(jyZBDMJ#HItN_oJn8E`y1X8c)JT_eOUQ?%|+PL#3NN>?)#s?~H0 zyG_H%R1UFO3&vbzkhBq)%`Y63x?$u^c6V_*puvgFaj88B1kP%<#^7X5b4N1V#yIUw zGohW5L`NiXP7wqHAAUtBwgm$d(hVM$L`3n4G?rz38(gsN>Vw25%sLKlB9yEPf7Nz4XcR&V?BxHtz#>g@eV>n zPu{DzT#nxK590@3yn9tDxz0(Cy#s(sXD*eJGxIp02;5AGHFJ&y5<%%yZk{nb(wt~w zncbK*@c<~sMAXyF0hsGuC?rhu>q=^dt&}(dfW#QhD!O7!0nUCY;+ll!enkTEI%kSq zl8pM9Ajt#jD%2gINh9k=I?p(Xpxj9rCJi0vs-ouJg$c}6o@#cC&v>QEaHNPoTCBXU zf4wG5d`4byJRS#Hg345TbJm0Nj(c%mdD3LZIiMI4w2!Y!3gJA@F&Ux~m5x9`ot5~c&O1yi=V*<-j_JNr=|rRaD7Biy(qd=Kp$E|7?}rNBB>99nT$}6 zIRww?RemBmpNgv~jazrE5Q8z)QfA(h$)l~_kSLQVXq1B(#dM|)W9TZTogCE4{WDRr zQB$HP6CbSr`nmn6$5j$$m)tCznpvX!GsOKW zaz;TEpt>z9+rg-ZXX2?-fR4hdE`br&uZdP37Ai?lFnjl)67*t!N-3u|3{bMuu(ZGu zNCu=70nA73Ri#I~9xBc``qhO+s1u&FLV3iDP{~l-J-Dla;CDYXXck(m3Fo~A_>3r! zYW1rGOvO^{lt##;)SGLgDJVdl4wWSfM!S(;4&7O9G>+R z+JYh|Xe%>MTa(13C)$E`*d$C0^ICOT;Ll!t=yllcBN&>dS(yw$h{>bvm{^WL7^XtQ z6ShS6iYco|=sC&nLdl?J5}%v(Vn_VZ%v<`5;B=*hWwz~=0ArI( z8D@Fp)lD=9QJOWRfMDmQX^2y0sBA~?L<9qf80Mm!K<$dLCI=k|kwgLAg9blZ=>jBi zJlA@N%~qxIAbslW61Or;48;L)l#v;umT?5g{irAN^A$y*CE{{mNEMpBK{6nyPujJ& zC?tMde9~*naHQ}|5O~!??}~IqYjWZuF7R`%_D2 zWhn=*6uAbF;&*T-6+_J>L$p+yaz{?WN4;6Ldnony`chcfp;gVHRK_?o1DbFuG9-+g zR)tLtqCl>BGdqOmry_>Nh}+XrlaxRmXx6NtlLrTgrfplofbB%LY!$?9j8kI8v2tz5 zCje$DMUQBXqMHkDg9F<&BWeUlo=?pci()lfg&rb$#eMiZ5RfCKI5xwtJYuK#K_lCl z{{VU#uv-%*)(q|IG0;`%w+T!bB=r>3TM`rL8TZY689b+-iaH07v+$xsLFqGAHEfVz z4wFngp@3tFs~Tvl(mhzrQzH>_kp^u+8Azkpw3F!>&q$`)XNNAX)D%&*KjO94ejc~8 zNK?adfaKRp7mJf8?2H*am_hzbm8ORMz#dz0IFnB8yegE9*9VGed9T>%t(2gRz<~m> z8t3s94TT}Kf$%GDqvPOLk(riE`Q1SqKPOMq(syi0NbO2BFBe)`f*ac%#dy}S0DI|N$RqiP}6gV+ioFzTUk5f_9QJ#9y)Tj`pL7upytXtYWb1}&^FnrY+{G>_j zGt#DlhLlNG2TG-inQnGe>jg_mMPv1JXNlS_#cctsR4ysa5(~7yAnV1tulYI?U@JVhs^m>mN`T3?c_R2#^+!X|1 zbD9_>EqaLx;QZCr+LLKiEAqhxF%v?T_7%`Ot$$ddVjGeN%@*#^zyN_DW~+T|xKp+v zNB;m7)-8V&^KEX8>j|F5twmWmS3s7xDDwz%2^h^>i-v=P&q|R__J)*kG981oCLu3+@%@+!2t7}EiyydtKsS4VEx+puV{{T58ZYF!v{{RqO z+dpiiTNJ5VHtiTSjbXJVFrm3anWoC3v?)0@6=sDzy{ac^`cXIRrrTj?$tSfhLLEZE zM=9&tk8QUuh%YFSo}aBFl1he9Rh9A?-R1zGLsfg5-qix^lr1XOUJ}+}$Yeg>5;{BADuWczpw8D{mn6?M2zzpn#Z#1m!ShdwbAx zO$4#YNQ*DHrnf*Nf_lwdr$Ua@5GZ@L(QcBF37&da&09F+DYF2XCv8%e!=tw0cNczI z$@0PpIf6}dQB(Fu%w~MK0x}2CQ8w*EsCj82O{iy#(=AN1b#4Jx@;K@BtsbJPGx{y; z&ndHVoz591CVuoO(vqhJ+=6@4Hmuh>W74CD$*m2hl-|wClA)Qb>m$7n^pjf^Y3+U( zpo|gkTC}*+)+z~E1SG`g^{hU-cCA11r{oF?O4jZaur|s+EO)ElY^TTVHX3WMH+D*- zAf9Q3`>Rt*C?I~dU5XUr0b)2D^HgdI4YZ-bgNlw;%2?@9MwI)DaII>@kY!Zi7SE#l za|$ImK9keutz~Tm!wXxeB{F3|){jZl7NvhvJtyl#vZ$~g7bxuATi&=WPSSZYO!(wgjuA*sUE(+X@OP?NGbAaj**x5Tx_!r55^vTWBOq?apa?n!8ARhZh|-?xy9E zOG5F*x3hPk_?qyqV~SA{0E6vX5B*7ZXtb=UN=_@xw4HT^?n-ySR1}hXR>eBY7mcMT zDV@0;sra(bhuyI`aKo3{dNz^cR;~~{)3nx(-^6yS0EYw`@dOkopD)r+QAOKbI)!Xf z)I@SmTIgf?oa619Ik`TYj!xp}_hv6yxq^ix(cVx{kzONjsOokpON=;NNaP;XxA5PN zTDgX=f2QYSU2HyPJgIa&NHn3b)gWVoBWa*8Cz2(LT6@H6bJc}h|_u1}dSDt^|j9iI|DGWRD; z%ij1d&g~?I(yiTu*O~a6_*#clvdd&i>0Zu_KT@(-Yy$82qpj>5dgTq=wSr)X=D6|a z86_Pvr6m@hEuWMxKZH7?ZI@VSO9MPsTIWN)(;QN6m|7s_L|61;+rxIMwh8jV#}$?F zAMmS8)tgvVxJfgS0=;BBX}oFOv&>}AA5o;J{1fMGCIKC$qPv}>%=0z+55`}>7a8*% z(`4t0@b3`#Ggi=^P=4LQfPXL?QYDU1btF1D6qbdly~%?!f&TPkH=qojIVQ7CTrC8o zk5>?SRg7IKQ@H1})u@8xZmh52F}fp>PVMzD5;mFoSD)KnD5f$<{V9uA#6k56E+FzY zXHo_wMq(;Ij|7ki=e=cr9l9W&tru?o0f~Y>Yp7}g{pt!(N_xmOl2SFrA1tLpCys#7uYnjb zU2))vlb?zmZWBF3(p1Lr1%4AV+LyXyk>96UJ>-`p2%)@^6_}T&fsj6;AF^XQ}+hm-<>r9IUqf={kC-Y~gN^xy{)G9}{U>UzCGrRPrwz`%ws@ECP zGq{^^bt(MS2Or%%x{=nAH+|3_(uqaEIjVC9fJ4iXlY>fbjL020qn53}h|LV+JERUe zR(VV{5i|3ihJ;q&dghzGZ8MC-(2iStlR?Zb0BCT}Bo4H}hhhMeLW_7Y5fx$+2Ctc1 zMyE^QbS8LI#QcBrKE{{W`DAfNZ6 zph-*v7^$UEjy`Iw;Cs{p(2b@!jLN@XB?Mspw z7CCLe1fSBaE0Nl0R7^pul0PXVgB>Yjf)GgO6UAJWW2Q1_P!2%HyEGs(+%!_IzcsMAvnTr}2kT2+(@xk3pK7aRG-TXD$2{{wlq?a09qU-FElMXG^Ny7! z^jZAKRWSf$({8iCQT-^(Ru#Bl^IDYXvM`yBo$B`Voudh#V^N^cy2+I!W@p;87K8|c zlhUHjoe&CoQH@#@s6dQN3Jru3l^7Ypt~|92LWNB!+K^8maa>RU9%sB!rUO7OERcEu z(uHwjl;U}#odYEDKUx)Nk^)RqV$g<)(4{K`$)eb3oRJ>mn&UR-c4?aoU_j4NM%hXw zDPUm!Uf81K%Npl#Ye8AZQ^rp7&K^^EencDzN4Dm%W&>4^M zRmi%bn=gEs??txu21PGv3QQiKG(tpv!m7eZI61gr@saUeOcR5XLm@^v&0c~A5;GH4 zq*VnpBt&z_qnd1(pVEe8ZMx%a6Sb#@*+E&VxU^uGGZ#pVxepbxQtb(VLhnTHGw&lGk?2<%~J z4vQQmUo+mU3xFnZ_@=PG1O7^?rp|cB- zJPI+g6q)Ctp7gnd8TJ&#p&p`mG?s=Yi-TZ~M8|598Y2o;J?g}X0L1&6AstS7cEv4) zWo@;Afe|!7PScN20tIvphv0Os$Wh6GR)A#Foa9u}l#(E1d(}xj@gDVJMtg(OoP@SW zHVS1uYQ(dSGZktQNhkirU9d<5ANiw5Q;;{72$98Brg|qZI&)7fG;5tjXbrZloNgkv zI(LSw^wjycJ5=OMn%#rM$(yA}$Ct{6KKn5GXNB)I1*Giq+kuiMwl*Fbv(nMM{i;~% zMR!*5b(WTri+zGH1m?V(#UI3H)};oPt*4l-n~#m)tDClBkLoa<^^HAKvdFD7`P3L7CsDxPLeN5u>;3fzPl1N0Tfo=N3?B5~2cSQ#Ut_N^bd0J#O$1>Z3Qj|o$eB=rVy$y(mAjHL zo|LJ2n+>F>tmZn<&+AO>J6s326!elpw#O@fVo02GRIgHPADMDUm^GEY*X5#?)MuO; z%HC^N3Ul+s7>{Zfks#JK`#0BG!WuzR76|}|2a+o@b>hYEQczX%G>NO_ya;(@2!S*+ z@Rj4HW|ZPM>#TTSN8~I$w4Ij%_ykWnGdF$GqYu*-Kn``Gb@|+0)O9elzWzh1Q^n(@%7Uw7&*h9Z!61#% z%+SrHAw?r`NjQwqOL5i2sb?MW6=P*+0&&JgA=up(+!qvx1d$vZQ=3~9n*=_cr6fdv zGu}lbUDIdMq5Xlmy5~1dp0iXjFmf@5!P7kYN=M@cOD}E(F`+?N=*DwYPD0T08H_NiVd*98AC0^yR$Ymv8|kG9-J} zmfqdFRIS8>tt4eC#2)q1rmhz<1V8|ErnmShMSPhXeJmgZl!=deZ((X$Y(hj&6yX+o zi7~o3fmgL?XCeUWN~b5Vw1vpFQkN$D=|9mis5&16}0*LuMN!fdYCAP>IpJ_xH+he$**gP0fXoEuE|kq(p?~jnNu$ zn_!EaDD{e09gO4)eR7j6wxSjU5AjQ_Qu;0vyYftb_%!n8!~Sx-_S8W+9~DDPsihA$ z)AB08q{r5_OuKLE#^J#=hVdtc8-2nOqny^stCS>dLbnr+Vksu1qAruX?mei}`2oc- zK82<)PEamR-^(%0Yn^C0YrGVK1^~!3p7AR~W6PN%`+179PgBn`wPqAX2WlBThZxeN zWp!j+t>vY%N)9;pryd#8o&jEY+<6pr^Op9;UqlhXqV!wl-cj-(j&ahdNfE?kjSVG5 z#SoDhlQg3J=5*(Wmf%UBsUH z(siZ2+KNob?^w?e>UT)hQV}5}`n`oR@KedWXe6AI#BoJm*}%%ut`n`C$)LM@bAT`8mo+Wic`D) z0E=XOMok}G|9a| zhX{ufXWqP4`AJKN@-6}#Byf4CHGio#VXKuWgNZ#WuD+qg)S`0V)00;j9`og))bz~> zbELCyh+0RrdF6+TbxTH2UuZm@q#V|s$Ht6ZHVVBejDw2b9JsPB&V0Ol*!}_=@Y4x& zsYwL75${;<5dQ!VwEbPBrsCNOC#+_@ z8v4P+kXo7*VEv;v0E`)|mxz1^W2`c?+*=`GK>q-31h6Kq0j$-jpUzgnXVS1zvB)Bb z^EUuN;=e}t>-Yn+%zsKw)dTrbE5bE@3F=xM%A2}jK`A|}EUr3Z*7ksySy>n$Q@bV# zw!n}MekpaOxxpaFGG?^eb-WxTLH4X!Bx}bI>l;G@1VEXmZCjDZ+I}ilR??K&DiB3Gt6iJ`ZUF(b7e z@R`RZg(wAC2afcnG=dVR3T97QDHh#h9`$9rQlL}bnlQjI_RTUNv;x(jhyZuR6Zj3p zfF_x@ZU7=bS|iF3F$T0DbRsqzM;#>QGeNy(IEvFa+IHg@;(&dwyaUpOgp94lsmvz= zmc498c|EI7a@ZTO{{Wg*a)37gCJrbVXwSIO?0A|2;i(;g#XWhv&T~P!#Kd!-Y73x} zGdC>Yn8?A!Cb(>gJ8%i-gIee9OmI(nQFCZHi9IUQEu${t3RUq^W%89qd(qbx;CxI? zE_Ed*7&PR?G&;1tKJna|b#HPE3HhvpF2^32r#5#oL>y4bkt0yrl0^0GSBAo|5)Cc2 zc%DzhQ&xyDd4c-UTMmj>Y?wI^J*ZXMg-@phcc%doIsX89iDQ}VR*Intt&VY;rA!&f z&&@wwxG_UHY!65sXwghYrlX`)Y6U+}YHH&UI(?`|Sv^6cKrjU;pPKj$!_yxXYTC|l zeX2Wlz!|HcPNf8ZW9E9){E(!W``18`%wmX2WQ=01D=C3E%+4yWBzC5YYEvC5pDvt` zrocCMt5T>GF<6*A>!Ea3L4p^-HF`K1rOi4(qT60`OiUJo0^rXP&2X(g;<^7zBgwL%Vh&1|u1u-E@P`*R?Nl z;zWfQ=ADfSn-6y6PAGpkK$0Z$K$l~a9qLJTe<}Oct}MyOYihm9#wqQZwtz=9nzUAX zR)W!dQY8`4knxpn6U7?RNu2v)gSAYMOvlAJw{L|U`_ib<7F3o`>sFz%AOIq0w6Z3w z48Ri|scj8KO|{lGnZ(t>ymx_CHpq|ntJ2a=b2PePSP^8XaUS(j?DurfIj1PMk@-OP zt5a%KFf;5ZlnQb)ZfV6_k8w3g(svSe^qSYE&v42?`4u$jp#K12rl<;YF~djQfSEmx zYV>It{DbXU^l6}fY#!#P{J0YU1NElx1Jp7p(qsEc>Bm16FXbRi2**KN59Q$Sk_6<} z6{qbGp^th80F=y#(||Y-W4AR0SxF=gdseA-aU(qRq1R{xm^k*J;2M%MO0^(oB1YN1wyoQU`|rl>UR%{gwZ!UtYGs;?$hkt2wV)`9C|z)#L*f^yn2 zG19FR&?8MG7!VI!(>rFS0UuBEL!pwKD}kD6Xw+m+0%+eruEQC%5y1KOqY-RIVtvhD zX3iwazMNALY6yUsoQ&3IXlfLjXqX&LNet~*Ngu5={5*FgcCL9KPk0?DQ)RRY(@~Ie zI>iuL6M>GA-lk4fFaYZ@Uj>Y9k`8l5qzx4mK-{bW%v6##<|jOgnwK(Qd(=|Af+C86 z3u1W_F#w8e!oA{q(v}EF9D(siGUnKisj8+`j+r3vcL~XgAuWNy=m{}P8n}!}p1mla z#R4QBZfMXAg(yd`?NqHYNXLBCKNFGE#(LCMa1+59J%7a+M^u>@FhJwIaC%N^lXH$t z_o~-)m@rI_wM1Ua9b67N&osW`L{>WhX_>jM7DS!!90OU+PTQuIK-OR_;13un%dH}C2d;>^&YjS9yw%} z7^`M%nIi>qV75#>c;H2{3yUT(Ta6#V;k2dgM2uu7ig?wYncg;-EQ?ST&T369@M2buAq|I$nOTA3Y1EeW=~q-1AH%t&1b#X z`UqB|a`FiC6g}pol-;eeWbG%J=}>8<5)|G?TvHbH1Sa3hDV2NGH(?|x4>;1uSj6*2 zJR5nP$vIG;nqgp@GQ!m83vy3L#VK&14>*>Orf~;>Le{JxG(1r3NgovM&AWH4+)~f1 z49bQo`%e$r+-d2$T8h@LV9X!!No?6K+^*a#VMQ`{iR(=3VD9LxX>AuP^0=NUzL@D{ zgoYKsthm) zQj!cP%uy{g1j+iERHTDeNVT*Aqa0LD=YdK@_QepSxLOGULH#P$Ey_@F>QThdAgIqf zr@!YC6hQv~6!O89r2>0IP*jRgGt7jYMY5tfF$v{qfC|_rWJW?e3Om^B>q`; z+r;isOwSWqord1m{{U{1ddf!vmf1M-uPe_J+J%=z?YwPm!k*zG2^CGYu*0e-#taZm z6M1X+f#s34N)jYhdVhx6Oa5slXCG>{e*|M>+@YkEs1Te+YQ}XIojXF4Beh+9qzCOb zwKD*AtgW7dcFoy4h!eo6Z7_7r2BWB^)v+UVev~DKrq#>9Hkc>d-mcZH{F@=?B&h8f zBifMYFE{v!;O^#$?1`Ia_S>~9Bid?dZ--WNAt3rkQ(7Bli){jtG=|RQw%ba93LiAoYA98)Ao`%@iBbXC*RFX&jE{aO6f}gb zMId^328Bh*b;a@r-YP_`r4<7sw>1e=KCx{Z$Cv`MHGTG!)1p}zoIt>#w6zz?Na>2v zmLaGlnf)m0(6;s^v1BxbAjtq^bfo?va@DjfA~y`ypUXfh1_3fFEw1=>!FcqQ0JvVJ$lf+h0&b4wQE#u zDJn+u913mWuMMur&HkVbCbDB=>2ZaXBm<6>r&oyCv}`3RPUF+*K$1?_U4`oU4WrC0 zZly9roOQ1u)b!-8{Y~1px;Y{%d8v4uJkpQ~qB>%L@a?-d4=LwhqI2y<$;(I{Kaw5{ zwqBh4x{?9Ro&`7YKA?i)Lt!#ABbp0b)vat*wUPq6Czz$yMC$PClLLvMmAwZVS~~5s zcfc%vY3W*>Ce1&F4Il)l_v=|uw%WQ4qXZh#*y=*sH;ywj$hGn=N&Fj64e8D{=JyHd z^c4HW4?AOHy38n;CIr$gBgJ;>0ka#rp47up)@|NzV%Cx%;L{wV5jQpA;l5^>P>6>Jf`k%TMAf6QY2TICu@Uhlv4+hPc)mAw6tt<){!AXiWz}n?8QM^^Aw`zx7C6y`+)}42WZN`*^1tmm%ewF1~h5Fxl4!phVNIdaOthHsj z+I>d@2Q|>2mk0Kv67i(FXTKCMEK>vzOpYqX^mHcA&P*8_tH>?9Rd}MyYm(7C_Z8}T zKCI@T!*m)cJfmtmh#7?IF0Pxo;3QCFCs@gW_SR8Riq^7PSWeGvljMes#=viABj+D9QoMIB| zWQomg6ojmFG&8mhtQkn3iWH?6aCA#eRFQ~w*_+ftfP>st7sY?WjXzp#eq#yTL=_He z(6xJ45V9>s)dTmS?QXyUtHH6(${f>U`}2?kRVNG&*8VY4#VWDsUA0{ zwdnx=0L?Z+B_c%qDlch|X>S(pTPRfX-mh*-M?XPaSw*IGqa^w$b&&>1rB^J;&nK-u zTon4bjAntjXef6xS>%lodou5_>Ytp&3RxonW4zXZ>q(Fx&~95nJ?Rc3(*$W7U=Dq$ zi#Q^A=4epaNckUXxh{nAVy-$Z>}ZD~J7+P?T2>79npTT|nVBM_tC13T`KDMD80yAA z$)I0UjN|sGD&!Dz0whq&e>`O1VwEYOiX|_%x@1O9d(y|++7dWixQq=O#SQrg@k51No(?!u+IM{kNWfdX-n9{&Io8@AXu zk9uyP4;iXlM1v!xF#^O-FhS!T=wjR}+*7BS2kk+vfF~6S14DV^;;#tA4wY&d1LC@1 z5A#fl0hKK{AaUM@AomnP)e+PT(1#@R_ou3%EKW=wGw)p80|tHSYez6KRVZeK#3ajG zUSf-F=*DS(<;=}omB+nTK?a4$F;Yn69_7>=~{h=Ja-77myg z;CH9ij~qewtd2&=#B7*65PplZOY(J0+!-2Ow6jz zc@ynb8$|yAq>rs>Rb~j70Q*&Iv-XJ;w*jjmlen{iJ8)@5%Me6Jt+mCbAcF?8_qMP} z0RI4rON|awu>eFU;&V)GCP7zv9JT8h;N71IqH$gb>?^(abM_G#R;}j2Xc;c;W7HQLc0t`n|YJbc}d`HD(m#9FQ%x0-pr~$&d zQxZU%VW@&R<2=+(pa_Zg;;{b!&Pre?^TlyG&oIBKITUFiO|tJ`Wl%rOQoF+tRUcZ) zBUJ=pVE+J`kkyYHq;XP$OnCj&rzhS)rB^o~iRv@W0_FHnf^+&%PF@9Z2M}OTy#|;= zujWrZ`c;h%o#)_Dy>`zMJaOcBY7b!_zQgFihzt3PT0 z36s+l-HZ;OHAz@QRK7}Jn1RsMPF!ut#Nq`Fz6r?3IH8?#K|7%5G{q1bJ8^UQvSK)z zSpD4Yg$~urmt!aEO5SouQcf{aQ7JHm+?5!d^NiJuJ?aupMk#7n3Lwe$qSzDkr8F@y z-K&$dgA~P=1_xi#lC-j(u|=W$fPVB11eo0}3HR+%E^f&igka4p7bZcU)~#v6qA*Nx znoxEa;wE6pJt#*IPh58GMj;Y0>de$jMBz9d;+YU6#ZH6Mj*;Gjab+ezn87m?+2)nN z1a>s7_6eL2NRnrYYK@BJL_ZclkY~4euZ7;?A`W9TD%v>aek#`4S%IH=X8;_-+I+8r zA4#Tmt-EmQ8f67;69FJeHAab|-fI`AwXtlvwU96jBzHCX7s37lvGA3eZgm}w-Vr54 zaqKHr4-Q%1>|@X6tV{PZ{tf;b`C%8j_|GAuz^zuf;)@*u)kwG6ji8KTk@&m#hQ**; zHp@(j5DZ0lcE92`^;Z<7tih98GWj?>81Tip8SwboyvcIOq%rZo@fFL}sj}%xPZj5F z>+ZR91FjMda%hX|R_>COJGe=nVrd)JDPrg=m^g~^ne%4NK2m3>!;2Oeaf&p{buJ)C z=OB^Rm$=(rVNym!<|3;vsivhl0C72>irlz`v|xh-8a9zhm$3R5c2m1-aqmXmxDuS0 zJPD=jTcx#^CJ2d|PvU)B$!q#6GI7pmqwS5AT$!S3e7OcsLlh0g%T}dIS%@B~9Gc8n zc$djy9#ov>Vw3A$ApCN+wxTDkM>y4@);D_}YJMj|P*ZF{R@lO8BXM&5%G9@s%yU7G zHxdumnmR(e0qm%g zFiEI6apxu{9EchE)wI(sZ$TP$!WvG(B&!um<8$Rr5$#k3ZB~6=(^DC65N3oVi+?f> z{GJFjCB@1anS{n7xpL~8fRvwT>s6s2h*DJIWF&?Q zfacJg^!BUMYNELl2c*@>ut8WPi8Q;4>?WD?g`>2JtNC+h)$P~vz)Stac^HZ>aGso*y z$t@RFH+9C4(wirdk=nBg(|qf{FcXz`fm$Wnt-`WWV4U{N4vnn!1xJtetNW2%2ijVN zvXo{3j%pVc?XoukB>Tk~r>!a-EbtOYH0-Ndjb_yj#Cj>zB(O_;F<5nYA@n>JRCx!3`q#au!wzgD6C*rQ%dZT)RHTHM z_vWOrHtvd!SpG}}g{y5^2A(oTCXQ!H3v7n->8%(3k9`D{0<7Rf^r8`_uEZ26B>U~E zSl?_fReTu*X(h5s0004yNTJ>)-1>w>b6Yo?+785&_Vm)5jWIU_PGdRiSH_5?V!K^K z@P4S0PfF0~+Mv_~gSI=4w5fU`N_|Md0&~qBVB1zH04He&IjdGvqF3o#Zf@XaGuD*c z>PcEuH+sPD?ZrR5PvRL-ByBuEtWEZe_;qeFaU}PO2AVD{Ge3K&UasS2RM|YityYb0 z<;qf$AzvM7D;Dch(DyPEJ?qfCKW@rMDvy$Rq2k!bDnzbrQKs3476DX5#d(*AyhPpG zrL>_~F+A6@-Yprjp;V`fo~< zNd(EoYV?NC($tuUImQi2?@mqK#^$nlKG~$o7CA{IW1er$DL^u~%!<)zY%52$DsV_p z5fypSQ)HyA@&^Upj`)gyr)i^4VQjCr zO42o>TUmuEgzYD`X=U?@Lc|Qrn5vXr*&`%nZ!HmHe3uA-r2-E$;iWZtgXIuO8H`m| zcIv&uqF2u9?XH;ZvI>#;e`=(^V((|kdAmD}!v=b16l0!k>e8lGIv<)?EtPS15Aiea z&1o#!YQ_r47@j$*QLvo91Rl{4^D?%?jDy7mVPdN_kW&esgQY)SsM3@Wgr_3}3WA$1 zn9LrvIG}BzRnoU5eo6HKfl|`0pJ~vQ%uial+kPE>r2>Q3lQr^PPnZfOqK3=l1F36o zCiS+850;`N)8@4%tyyjr>Q_$Q)l%$RaVb-Ym2{r86PjO`=vL?nGAf#AiJKiSREBqy zOb!W}*7!@uOS8XAgcStHBe<_EO~S3v5ZRn$aYgCcpWze=ut>>|(u*ERW0G-u1131+ zi%j(&7VG0z)L6J=6b6Ko?s`^w=f&th`&S);3`jj`W4eL4w0Ft`D`b6Z$;*0^`seke zG#AYtT)8q^Z?G`ohBTEQq&zd?r?iCyg>Pj?)IZH^m*dv8WTlq91d-~m5o+``rzI)` zuW0G*M)-Q=L1Fp!~y2owz+lyz$Eip{aFl)AW)!pb~Wp~X{U>xCV5{XFVy3a4y-l1d;Bu6 zqB#@AG)sX(ywSTQ>8>XB$!CG)BpyX=^y{&6Zii50D0kwu;>+r4N|}o`FHdoHgUzF5 zWn^UgQr%BMX7xU?200xmfxJUt6_}z>w78H`K?a*BsV$)~lUqg|0O~*$zuhtIJ*5Dt z10riJbK*;3bZstxLxZW{*QaXsrS`&5B%alYz3@v{pNLz@+Tq}V#b%td;(x;0{mnSz zgC+W=OIq;Fp1W?PB?(zSl#cb}I{yHMZ!{-VpoKeb4+WbD#x<;VwjoONtnL>|4 z+MVjU3pb3ZDnM9Jtr3ntI~P8s^Zi84&%?>bxSyAUxZF7T;<%-5Ao6Q_;@{!(wu^G( zN=(nGP4qSAfl5Nk78He~!jPenTx{4UiPVwP!H1S9)w_dI zTN9H6@mYA9)QtPBb0R&cMW<*WNbD;_<*f7+6ZU?dF+$=oNJhU&F^`I@xIqGE=|{h6 zK{7EBLAc4t&(gC_(D>}3PZRl%9~EDlC-gPdHby{>I#qB&phn!*M%|iH(IVl&>(-`~ z6TvZ6AYcMMYUHInoC8dQ0!b1Bjnf@!w;LxENJ%_L*1l2%_T8oXQE~2CilOgbC?0Jkz+? zJgC~+Ux>-}rfl8Oyx- zGHL0EHB?y0s0lqPvatpxs!QAuVAM*%=j~U~D3bC)JXLCmiO;n~$1royK&lq@(n&Qe zswAX@@l>e$k@To4;{Dkf88r|y+;^f`MGp{@FJ(MOS}~@{>0GvK9iaZy!Ip#x z&uSh4aw!ewe@fwIClgEqKxBPId{K{@TZI;#iXC7X#~giVRa=7~fN9H(jCDV?3a0U& zrYh5-nz1|VKu{FD*5JwSPo6+C?b??--y#h4#XV%9PaTU1Aef^q9W%~qr8Sd?#dWmp zC%rCNEI7iAC5_5KPq$7f?Y-E5Bu!)(bfzHFTUV7lnoW*IzTWb9G{uL)j%KqK%QMWx z8hvJxpbUaL(JhhabkgQWC#4+9x_e@QwnsCHV$*zMiRPUc3Ufh>^T?(w9@&}!v~(a( zYCVud2*mWJq#{nnEUGd(^F}tQi2^7Fco_Dh8aO2JPC^0m(1F(#TWaP%YP&lo zco?HhQ%5|%V>Qi!XO4&3u8Hr)ed~cTN$JvPfhh9K5)<-k;@IoQS|Fr<=DJWu2NWX^ zF=Plx0-&}B(u@)_89Y}MoyU>!6-A~exMS1zto{1~X@F~2aNneuoS*tB74k8eHJ)ae z89Rn{lLY+K89O3KRyxsFN!!Fp!JhQEFKWkM+Ojzsk~CIN0|Sx=6Hb}9K}gTPz^r>0 zTQEvv{Pv?;)PNHw>q>G8jT6@i*iTWKM|pA}kWU#ini2g>W)yyDJ>H@e4n{Z~>3xSs z*SEk)+)jJulD(+GBXAMks=K=jC)LuFJp9jL_B3c)8;wIG3HF*Po7q4UBei7>(E|iy z-lm;XU`X5`W+<+THR9|ZvY(3Ky2Q^tdK$|A05>ThNzGV|NVp`E@M>9fJ$mek0FgXm zb3nUwAg7T*ek82M2sj3!rTSnVPc&(Q(PFP0ObI93+Nu}IPa}8up`20v*(4wP^r6(Q zVrQIH@&}038j7bTPk!}r)D&U~j)IXTTn;MasF8!k3&}w!4h`ut0=dlhs}pc{7(SWJ zDnisGj2a=g0fRe%{{XW~a}7jyZsq|zLHVY3t_&GC$gJxRZ8Oiz8eq-FQv!Z5^Hs{i zqfu<|#(y>p(*#`!^qH*77g-`=PAQX5sDT8Ir@dX4H~d!N5)AaJopwRnKT}mSg($(1 z+9;&4yc|IH6vV+|%Y(Q?OOkUGa&8S6cx0|ujZ2$-YGQBmLw;}kR*ToF`4Rn zRO4j28Z_?ZV2JtaO)Y>bGlFRys)@nJS*NzGm2J*v1B%y7aaNdTDrXs^T1Gl8Df zRiYvR$I_T42;@({K~$nskg@ww1PGBqJ7+V3s(#{9qCrUSiWIZvE2-x*O^$^(Vij17 z;E0NI;qM3D_=fc?Su%D^IOjx)d*QDN-FS}lTiPdT5`U=YL=X3`bJO(AKf$_hm2Q-U zM5y&9x?VSq^KJBV{Es8k_Q$b)f#Jvnj;<+Zg{{Y9I#54u1mR?!pmk->Q-D~nTB23ms-LY!> zjGtIa$P4I1;MFB?u=bc*Km1|R^IR`CA@+Nj+m;qB&%tlTN%+OoK?}6 z6iGenhyXyGim(IygpB7E@{maYaoE(dBrU&FsQ&=s8?%gcqtmBq7ObV~v_os`IIDcH zsN5sHFn>|Fd4S7`1pojex4jprxYZTMcVGmzN2r2*>#W;CiAWyNRw~smfUKmB-A!K7 zJe1_X8Ja_qtpt%m8&pA%4Hnw~853U@P#`Fd=5dOerQ{T}vBXeYWvV48P6Q?@e}~vY zioaSBs~3Wcid2XhJt*%b$5XYqk_^>71-5W9+P0M(@fCVOA_4n1{eWH}wbHRbui$tnsS=}wN zpqT*AG;N)dEgwpfNSpNI zC-kjz7HdWPosEo4<~Zc0j+P~5P$*@BjSp*wn6~f zm^HT_h?KTs$>lb*A%DYm#U7T^J^ujcs+#cQNswBZ_Tsi8*m7!Nh*ZG&t;@uV_%Qyi zOGb9rMnfOiF~%sPron(Q(9^wuA66>GmXIWoPLGcigufzfikQ)mQPR8ok+VLTG{^DH zq(I2_t|IUrQ%v;nAU#Z?#b_<4$N+!&w65>N4Xs&1a(-)2UU2?Yz^a$#sBsvY31P)A zfTNo%eoQsrg%g-ZKGmJR@bay<6eRj&iLX^E0A)N-H;lJ%m{LdDvSjff*R%gDPt zV(!gI+zunKtmBfGR;7{v?_R;HXozv8EQk>&73Dhhk!i&Qq=@FY`LVoRos2mnlmg>X zDL@HVqB2Dg#p6z}w3rJirEOaW#BRv#(wb@7G$@pr1~73nl-nNF8GCIq_V-XynNo{E zQyuGIY+q*C0jWyT1_-SVnWi^rsq)~Su}z+}L2Qx7w_2_s^P>%9Zrec@0E0NFYJ%?l zfZG74k&e}{7HvE-03>y*t?gN5WI{^1nEh%jWc3d`V@gw2;SUXq9GU%S>kSFyEIAv5 z_3A5MbEdMj1$N+pO*BgcJff09g&Z*3pl6n&W*<<|QF@(<869K2F1OJYr;=Gz4iCk8 z&Ci9xluIyVQf*7Z*C-p?LZtF3vhn7=1xGwytncRMjXX%x*WN`we|bixFKoKZ+2#3-xNjz8L)e=SQ2DJdrz#RUDYAqh|; zij5NNSlXK_mqySenR+1?CbnWC5ByD;LPBn8hsX?CG*;^C$gQxzp>wCp*% zg7P6Ch#z_|LK;rsRH&fr8CxMG)W8Efuth4@d{CV=@I0lc4hmD!hqClIS_lMzAd003 zD+MYN0G!ZTPPm<63oQbX5}m>-T7IJU;vH?q{{SgD?@Q3nfx<77X22?y6T}J|((=QJ zNJ5TAB+W(mc|`9e9%kt&Xu>-hvW**Tje%;CNa@X96us#zh)P7C``(a^OKP?# z&$Q9VyM6Qou#yk&L&n){vt_30?^~%dNKbi+&@0DxXi8ZtE~FeuuQb2aS7&UHv>oIC zd)3-foqQqJIKk(NZampBeU5}r7Cf7hv-D4fwN&dC2z9mk&J=^vwOUheX~|#rA!_`+ ztHX3Hd}&HTt<*v4CkD4x9zJ>7hWzH!u;9V3N8m`K#J+SvxfdZ z*yqO-CY*kM-1J6n8+jpUP*6~iYO93#;G|$4wTsgA-Ii^ops5%j^sNq+tTlYjjZwPYm(xk-G5ZtL>;1dd_zaL8T|m)79kNB$jv%AM;w&xRnm|Rn4L<*;7jZ zg>)jX)8P4I)BfW5^IT`NWUutr?%OaXM{4pf9Dff{bXRj|pC;e~3Ihk`y{_)ndMYLU8h9#$Ac2V0@#|`6VIS8KO_dPYOR35fk(8S8dz9Sh_Xg8YNlL%5arwJf6Ug!9h5pB124$5D7E-AM)e1ma zfFxCEe8h>!ue{-dgIxI=;>sOnc^rxX>tH4$=9#I`Qz^%_2>6TKGpF|ae*d1>X#Q27!=4v z8Av0{0LNZ4UnzV{j%X6;3dyg$(-|<6GZm&OqdRCJZV-L`D0P{hlZuL#NGGhxsP0M6 zNv5O)lsW+hl-?j2nsDrGKRBg#BlBWtWQ$DAU)zNR1N5xj?Y0RbJ?mj~Byc-TXYSZO zpay0Pb*U6onU#Lt4+FghTPZUi@meP>N{$53>a#!H0ClZVbc8WeMOY^k#@*bE)oWlN z=D8a{0xFouj_qy^AoSv!TfTOWVOfiJwvjydruOfG1kV%$2k3u_>2QGq0h*`I1DX}nlLMOj@WlJ$h^DPul>s7SwHDo+ob(u=u&7RpBLEKceZ9F!kb3u~ZCMDB#A0ZM ztnxejQ4)^YH9Yr;tp%e*VgS!h)CH}yA9{Ie$RGnEDbN=n`$mS*!TQrC+9w?1skCS4 zW|}hCA8OYFB%aTkYiSZBQEj3l5squEpbul*(FtsTea~8TAy!GWaqYzx+~*s9Du&0t zGn%;vjAEz|mX0DwjL|7iS{1br1ZIdy$4+wosY5a|-nzA8{%B`j=t%l;LfC30 zZ>kSoDJ|*Tl>&LjD7%ZO{dlC;S3m=|B6+OSW|$#G`H(@+=}4{}+Du5t2ASU7C)77? z6BU`gy8tO8r$Jd7Y}A=!*NQ~RpREY3T&70j_sOKUSG^`XQfKeU;E_K{t_-ImSnjcd z3Ose`OWoB<{{Xw%dHU8}T8ha4D?j(GQk%6nsLl+EXQnHd{mbH`$j32Mt>R@$Q1Dg5uPfrcLO*Fu%KitOpjK3)s39Y0p2PJnB*Fm zQ5=l*rAuL_K?x)g+LZaDxkZVqDg&ql`nQ{b< z_>)4hZna<#m^0p)TiPi5^EAZ<1CUj^<})cY^3FsK2#iGmYi6lTfCS)E`x_YAM1j^R zsw@`JeXYLafljTh49OA)yw^1QGZK4ZMr%WDVFW-ReCCZHY*2LYNCXmp#V)$D2+Vh_ zr{KT?1fKM(P`3aP5!WJ(L>oN%`GS)%AXa_xu}VjN!!2$=xKC`qUJAdM-BKZVbuux_s9AMM?CicXS z^VWs7W1;&BZ*0zZ03OvEuxvKm7~){}qCCQx2ak$^8vtMf7?LZQC!sOLT1Xbpi7HpL zQym9Fy73mG-JOCr6$x602v2$%%EseSyK2(Wvg7ZNl%#QApnNmo`)>y5!KA^vSlsi6 zlCIwZwRrwJ%`e!=pFgCjc3EYo*!V|EOBSdBKuXuuN48gLaH7&ZB={{RX$=NQrvvc zS*+FSSW~H56eMRDre^i0PSmIw6`j@XRE4EXlRLBR-n_JzFL!69kGEY0xoE3}jjD*M z^-CCADsLN1nqz9MmaUa1*BJ+jqV>e=oS5z2pTQ+!`8z69si0CoBQ&Met&5H8R^*N>Ev$k5n2{D2H05ywyUv}kYP7nG-P)VeNj9WCC#W{{is5pWk_w$%B2VtP0hLx!(|`^cNF8q z9wJ_~r`y^VTR>$|rUxme7{Q{6$#P z$aF~DaDHgqo2w}xdlfk1wyFv1Jt&3WD%pDiIgE-L$A99&e6oP9X0?d0Xdw$)1f&D) zRY6AD6sp{*x@^~@Et!%{)I$2#Z zR@zmYZKMF7e2V$qF`+666s3!MvhOO1nr&|S)9G;ZiY#-3(~Sc-&237>xB7ctT9lA6 z?V9r66awD(6C6RWRn_j2+XU|QM{4m;5Z)W7mVjam)(7K{p+WP#rcuT%jJAT15ZL(glqthYJ0II5^|%`foW+%_7wb4 z+XfF`YU#mzBb_;J*-2|~{J(0k3~kR&Xokk@fdjp9_Y&C0HL5t&RkJLn+oVN7J>$Jp z?ZO)oG`{lfNF)O%_o{3yU%XgSq$lRH&p*426|wQd?d2z;*Y2EoWHt}6uajo;vAIg( zpIYguX(Mo%=B`^QNgH{mN5`Ao*r|CDT7neX>B(mY}kSYO-E_XRO{OZaYocJ zp=r+}B(=7K1PY~3Bu>yMN0+{K^Fxy6-#{`u)_0%qjSCLD3p*7viWSBZqGBjt#5k1z zuxF^F5U{lKnvuw-qTV+Fef78us4J*HwRrD~tri?s#1w*ZYuj#`+~2Oe@5Lz%&g26< zee0X&LgQyk!JSC*YkfJ^+}}A@0u6068wIGRbdeos8#{3G^TFmKwN?mQNl*d^7y`Kv zNbF^ib<$b4Z6#m`%_q9ml9q{N&Nz-L=a{8oDDbybva*r^j(S(t-VwTGNm8iTS_V&MGqMz?hVQf z)J1RJsU)RlGZ92-x6fKQ6oPhjiu%>FeB>>>^bzk*tu$XWj3*ro)kR6WMajh~V{090 z$CXmXH*XK1Fpf zfd`=!E!Tz`^#%kFYJ2!LU6&aWbo9L<746Fxaw%|7%6#u*uT{x4b1dn3 zAL63FlFGF8oA9?3*+P@hp1(DrvvDjS6eo%iw=LF_`mp@68h}u%x9Mc7xM1S=V(`TQ)XgfsraS*3YOZ zX;K6a%0Qe|*80Nj7G*n!N(sxvG(uZDlkJMn_^ZTg(yi0@J7qdlFj6K>RelPqJu^V@ zRn4{H9&JiV2QYAJ(Ji$#c!g(rfN>EK?kn@BfVG?5O)e`?WV0WaJuBBd4Sd~}sINKi zLd<%z*1EnAktH>XZ%pR-cv4R!R?+Adh_(tMiN&ACVv8G>`AjJfRwBAHccqk zb)7Ab7TUPOs3B+xcmwZ>(p)ay*6(cYE&x(NnXf{)a&`Qy;kQ0E*Jvf1=a^9jR**QS zmUl_GZ7WY&NR~nVqhys6#Ra4ri(N_P_wQjwVDgjgT2@B}zXnNSjMHnhY+h{_ESB9! zP$Dx}M0whNuJyYp-)aQp{#x9Xe@ROAtiyxKcH?l}_=Lbcs`K1s8AnW38j_S=(OuQ6 zcZ}{ws3=JyytBt12xC=cX>|o8{#AjLp7rTkUA@McV7G2iLkU0XnH}oe#u~k2dvK&E zV8v|5IC)s=N}u4%ILpS#adwZ#7mVHLSE{qrkU`IuwQ!;HTsI?Lo zZvOzG(3ALAc-X!|N$h`0{P%Rl){%VehN6Ww^J*YS=Dhcnf>>uqdj?qLha?)SiA)1O z6-wXSFscD=naTRiK)os>`-e5ok~YPLd5JOsGEH$pQe(Gz3_c8Jnzb&HVEs*JXwaDl z69#!Af^|DcAF$?*T7l?JF;pxA{$`3SlGwRJJ!9UX=0Oli`J$H8#ZsnVm=p2RwME&Q zMSG0k$m>xq5ywirG)a&vi)g4$DS)6S9f6K%Yw9X9?M&WijDR!KwJf+td7RL&=vw*d zlS|w{ClMx%cpkDSHLeUEdewr}f~}FBze)|{r!YEoqnvgCn271#mt4Ck_~}JuaR7g8 zoCu&+LP!HW>idT}`t>3z^9KqiIRU^fqvBr1UOj`Nv{A>c>X6dF)>aUgr~M1dIFDN0a) zIPFjD(s7Qop2?Gm2DFw;q({FL5}=UI)rkUWlXmbhk^X9{R1KpYc&4nQzGJ7-lr*U< zW_iaws^HXQ#~rHlwj&W7)V2h~XSH3D$ua;Ssy>35Td$8+M-;UqrxklkAPyi^sZktk z(OWnb#Wi6ol~6{$+h zt|1Ba3{3mg=>mHWJ?K!PI_Ki9LbxZIy8tOLAOHdRtB2}&6e=^Gv&9hFq{kItmV#rF zJIzb-^OMCwU=!D+eK^U}Sq6f@UbRG@H2B9q^~YLT^ZUkBNjx5uGq(ihW2I7+lBhBBQ;ikM%Ls7FVdE>9eGhvAmB;&P2th=BSY*Z#7Pil&+B!0D_ zX3Q(tk=mKFr2`O>s2^%RD3;7pbfv-CBduCArKcnxX|3P#paC01O;Wu%C`lj?PX;I* z42`oCyA>zw4I+Ea=j>6{MK4h1O|A*FT5BbcLFv{RBwXX!>+Sgm4CsYZ?r0)xzd%^hu{fF&tBh{ZK1p-xQBTItH{ zVD|={T50O*p5`g-wv0i_dht$J(y{7K9Ak<%6jvr9w$q{j0CqI~%Ftp2QE1c3QYLsG zdM&m*u^}-ZwFM&Wh1s@{2kA8FkN_O#{)$q`fC`Xet-@~5>Cq}dWZKxyv0C@6GlnDHZ{{VUm_=Dp{bbD8h=}-n>@mpUJ z>Z!B0R?`3s39rk)#9t9Vd2--ELfVKu!LGlPm3Tfkv$LP#Vf|;3?&!dHpIAEGlXoSMbz`oq__M26$FQ=h<5gJoK# zun!9v?{ zASM}jHEV4J7z64m z)u#mHWbsu8fHDCQ&%D&O%c9XKAYdH8p&elhT2`s}2CpuFp7`dz4sHNO2Ng=z$aWFs z!+pCXwed{Y)u8)&KkDHRiBp;zO=4%H<bF{yz&v|X3kKA3@GwjYyH3&;(qIC)=Mh?+ zH$hs~1Bo%$+JRfxdO~YWEZsVSlB9roQx^11Ma#6J(%M^*>OZJ~UW=l5MJgd~LO>H8 z>uYJIHrPs9Y!lwJasaKPdO|H52w^0~GuE5w`WuQN zYlMzq3eYD{Z7JYjSAPeG?daiV^F7_!gI<#Rq@^iR0qIS&tupa|09uZC?OJ3ogdQiX z8ojp?CJ~y~4;C?Y*^YiwlB9vzw{V~()d{FOrXdnjoZ^@V&=cwr^ITGal?;mA!YvrN zNihSd4Fx14Bfm;Yw?R?bBm7pAQ7Kv#w+i;GUZZdt+zHxx39es~lo`|Uo0>B|EV^}; z)C#fC&lTm2tyec|4uYU(BDH=f)Q!u9wJdEM0($XT$C!Dfg=vk2Im|~j=JQXK?DV){ z#siycBr*gUIL&FSSz4P3F#riPk3?$b;?*m^PQ}mVn$=jg5T8jSrhj?_$soIX2>>9% zk4o8SI&7dR!2_)+(XCCxsl-QK^|rRR{3{|Wr{Uq}E>EMC<>unJqpDtfw1@_}p|t=^ zlZxYtN9T||s-{VO*iQictFb3qM=CqnHkt)xN3Aq%sYt5PXrz>tWC2AsAuy~~oEX~d z&6g$gR(&Y!X+!rANm{Z@3Tt$|sr2TvSA@LaN-^(C@~7#o4e_DL?3*^~zI{7nrg)0h z*xH5}N=V2ZXdNQSEw&Z?$20Fvfgo{A@bGb5<=YkIW^#O2NQ%?|$21z&B+1PX;hBR& zt)2C=BWt51E94(`;zWIFOS9f2)$H4lJ-bn0K=!QB#OK)OlaE4`TP3{2)RSz52?Ch7 z&{imD2r!cbdr>?}Bw1S*KWZ}xf_03L$>9n;d;*jtCi+ADDht>Q^Z_fCT1)&ML6LpL=bvU z6|B?rVgU01Bw*H(!%a&~rc(=ulUIHpYR=wIN-RDSicUpl^BkQ-39T)JZ7sGZtrm*_ zKm|Cf+d)Z_h~|iBg(yHZ(vCQor7z#;?5 zN9j^n;`Gol%_Y+^Y4EFTlIvh6tp|9QZ9n+YAaxbId64QoCJa(*t2DTRm4P5;D<)qS zHBr&8AC)WjY{^^eyPpldAn`S!v2Ck?DT6%LcT~|*l^0s{DrAvG>33q)B3C5WE-mAq z8J#&LIC6mf#!CwY0Q*ta>JGpoppoxFGk5r;j**;ER_&6qt^ls4F70%4GIhSfAJY=x z^x)D*oPNc?770YwmA6tz2DyD`+w2(-D^&4vT%o3AhF2HHdqR{HUZXznDK-9rmrB$S zIKZpX=2<-norLjC!=dC#02rmk$2#LK$39qH$D_*ii!*3SCj`MJvTt2D%f5hM9Q)U? z>iS|X5O)D56BXs!^_j9#j@50FBn(#*$-?l;{tnlTnmJW`h;DT?DSLr1N7j}*a;IE` z5C_LKW!jch6xu;RJVg}NjOE+@V+n+S3Zk$pk-4Q2daj*eVOw?pI$%Wgjs@ zq^+0BE+?J6YHVFP<(gS=mmNXMy42FqcK$&gzh9kzN{2}4N;O{%-0RmUeV0^>Wgy4R zK2u3p1z=CUDV2@dEnV6`0Ptpqh4vRFgjV`9+BKfTz1+<@u()RAggygG0CH)S-m2TL zhC_amKRqT=fpOJbr*f0|U`8noanQ)OAxDV3AkM74#l@vr;J`G)K+)RKZGg8?-P(Do zt?yQ+eB^?p1#Kwy{{W(yT0gzjw(>S7kZ3y?^mkFcO`~af^p)?Jj+9e3Htw|A+#8!( zL<7>3>(=gGmJq|6P?csgfkw8rk+mP3qnJG^zDe2|w|8%D(o)i!Y$|6UQC7E3TR2o4 zDng*7jMf)W(gvY)-NP47Hh^-8t(}k!GQ%$Vi~tQ*?59)_XK>Tn*W$@gP)5~H73_W% zy|>lcD1Fc_8Xk!`AXknNZEXJlmg9*-gy0@#o!Q-uI1))l>4FRz)#LddgziewlRh6- zG_=pr7RPR%{{ZONc}ShanKRab($)^9<+I~!o>*}ve&W2d!u}|B!t6N9RvlRQ^RWQ?}*Olp*%u6XE0SD zmmkY@D~nZyrrpKMeTF;M@I}L8Jk%f4*?!ctq$XpvGI-?Y9GKlT{{H~5Fq~9eCap6e ztZ9h4Wnd?8Bk5i-{6p|UHR}i6VE}6uAi5*-Tr>tx{{V!T`il2IHLGibr#R;4K=cK2 zMIqL#UTMT!*sL{j$=urFbMf1_?OJj|K4vX<{{TOM7^g22E&M<1elJp-B(&}wPb7$u zL#awhG6c-ms(%*zAl0MoYnnSH7T5dMTLb?9h2j4I$l3NBk7Hg`RlH0To*>tk$sR75 z>0s27Xp&bx$E;P!UP%WuGDa%ENsMt>9Bk5cb3K|sX)MMhv}9jYoZ`w}U9 zXf(@0M2eD{z~}EngYCx^cz3=#Q())`A|(+dnW`LC5;zIt>sP5l3}@nmaP=`1>q+j{{;X%ELeoHZ1Q2uaN-hjcnLR4|x5`cqMJamhl3+ze zVQAyd?UOwzZOzOB#su?Rxw$qp@ZYmet#uX46tII&oDt$Od9YXvUHPMAMqE6C{>}Scb<= zD}`VfsUg5>k}k`V*y}y&O4r(~SLTyihh)w!~x;KGeS9pI0Aha$Cm~ zGp*p`KJ*MEXC7mBZ?!?YUsn{>#(hUUdska%9Ofc|!>c~Dw;O~A_^kz-1SK#z9~3R4 zBLt2;_@{QQgAgFi1F+IaD%p~p^Y2YqYlGgYw`>UNV?NZ`mT(D@J?bdgP0>nOkRU|# zsTN7cQPL>%u^0kPS}kXZi2KuhJqz^(1)>QZ{{TwrP(cuJ-k2`i9+3l(d(~x#2DiBso54uNfX=r(<^rf9KfIuf<36VG4-hxL~Sv{4xeH0Xx7{l$uyO>NC1OIH*iN@ zYfNNklt#%=P?Bo&N#}}Gi;?NiUcG3PU1OM>(^C}C&9j^t_%uQceGM)}%1l9#L?Psu z$m6{+F>MB<*W3FH3{46y0PCLBQm!ELGeu|`VJ^4;JVg4C#71QvYaT--o62Y={Y|h^osHFz1wm+Q)}Up21msu zxP-_GgWjJPu@XrDEku$526(9V$2W4({3nq)+*#!3P8g z6Pc(u(S>3o>om5;rA8l6wx4s-D>rpx{-H1+*P>n6g&ZWu?MdFz)=2b_Vl$e!whWb? ze!C;P5DDuv+e@(?o!Q3(b*%}}CCSreaTNa}1<&A*b>Q6l)w3c=O zQ3)9xE9*-Qsp0DE01C;}5X9`wrJNI<|(zZA;Vk|P<2`cpQo z0F66%7E z?)el;cclga?K4cP1Cg(`E4;a8+SnDm^1>34nNhFMT27YFhIB@)8zXvCE~+4@XOmu4 z{1MRq0GeN1Yqv?8dq3$hyQ`#R$FTkD(!5cA8Mbg0at~VTcsNq`Ka-W^<>dII%0G)f zDP5h4{{RlSTD>Vg{?++%^7~NLuE>3BaSIYs0g>Ll7x=juDt z5|p-szy_DD*rjT51_$PVf4ivotL|2Zt@b!!MCazQVviL@%#V{gwJ0fbz&JFA#5#(s ztrX&Nt^tZ@v*oj}s!V=r!+4(U$HG}`6Sic`%`#4rQCkr|c5BRnx6}+hwGHI0k-brqGorD1r#$1}GbfR2oIal#RfG z3HPs(A^=unHS*MECVC2rTPxyzYd`e}EiQUV6YVwfE>b`X!imIC>1k#2o; znI7O!(h0DIx!bJ{=~2J}J}L=$TTiA>da)#_P-KdgMC^i6uwf)^_@iy@N{CXh1RiVa zcD+X%WMdS2p|o3)R1=Asn`NMT#O-DnP~202KwGtbqO_gZ=|tGrvv+cot>q~40P9;s z*|3HSMKEFkNYBj@Dx9_^)9gcbzHPt@>Y3?B+unJEp``5R6(-m$BgzDja5Mh^dQt7N z6R@Ql3_*<0J7W@|av6C}iBGC>dsc@@(+buUoWL3DQ?t|EdIMvBO~lsU!gHyd=gSoR(+=Es#=6LvQ^27+vvIiZHDKPA|zAYD^F(Cw3Idqz>+afEp*pzm6R6K z?m4cW9~ruTCnqnn@wylE`0=70jIb3XKbpQoYQgpA1KQMBar zD%4iZM~5uRTA7$Nq*%t&+X9#At$7V4APk?i6>{RzU;~=;-aI`#X&!Gp`Q&96Y@i}R zinB;q2Bz5ZCsL6II7%80Fzymv~xa1 z4?VP*>DIlN;9tXXM%LXPS`s2L#a<+i zYP)b0fDIB60|vX;(&M9xk-G&@^?HQWt)Lj1gMj&fjEcA(@gl9p*;k{bAw4R!9Pt%0 z2{Y5RaGkt!M?e*>BL|8N+!9YFiyo;8#S3&I2+boZ(CBtjsROAeiXFm8Af3MT@KFx! zGDr5sS+jZWD+HbNZHlhN7mX_96pRDXuhTSdBm%LXMk&i$dSDpO#TJIcV~#3g@orq? zOrA|gj6pvRklxhtDuB+_Nq~MSm`>v$2=7&`pk}xFMowT(+in1+Bdt zw-LsiN0owSoYU(@m$y6`QKqvhKB*uBky=Y+6*LLN3ftmExT`UFQm3G28!b2%M2cYC zEihu)fiMj<)mbtQ#I` zl^~%<9@UE@tm}!l9`L&ld9OU=?UT?}GsRk9DI_LI>lMBMAq1%8aasLz1>2BdtK4*} z@}Q-Slg68lMMsF;+J;vS1A~%3dT*jjR^?X;+L1V_HM>_9w-iqwlt(q)IhP)2CgFQT z9(z}qhNOCFzEMOqm)P>RAc7PnHJBIPQ6WUnDg&iG)=_SdlG@-S5zykYc8)W5c(%zU zN&Li^pLz+m$Yc`YGt;gKy>rZPEwu>=CX2mVlWxDmLr;HcHC?5XAxc|3^dmHD7H%B6 zHwfY<^{Q7?Q_}>~jm6EPlXRWW4237J%{R4WwpsrG+(A)K1ofcXN8nVY6@io2ilxh@ z)=CoOub|`aM6`-($7pRE7V?uRZ8HfoHH}}5^ye;%3t?#iaV008deXGDwQfly4rv3; zTe|Cnu#Nu!>I0goY>?B~t6xRpn@aT8%X^0S;2MRR?_JYY)7K?;J7oBMew^B$OSJZc^q`N7$`X<%7m|78)42U%H*=?t2 zTDPeQkSHZP0UMG>TnBk4s+71=#QO=D8j*Ld5COQ<+*OiyuICAhn$ zn?Y#EgpvkgvT5AQs7h3!+upSHcg3I*;7-yxidP%5+o9%=+AYcQP!uFdt;dIM-hWd< zZPb)*9<9fi>GN0(EpALHGL;-w!$h=DSC|K&dV~!Am9NEehDfwy@)DF9X3JA@;@y=y zb8??Cw*s4RbO)tHjaar+^VfinEF_^EEYDAR^AF<5%S>&M|$K&@f!WjJ+xywxS?Kb_l8Aw{TsGbffkTPbx z-M=$WYTb7WagC-F7H5juX_~FQ>Of)SJJdnUSE2E|j%h}EX9LOcxO=0tBk@&(@82~TVCAn_N*PNF6f>ne%9r~_-#g9Wh(rk z(Uk%6DDg}5-bd{>{*UARdJ{GJj6b|Tx6l4e&xo{UZ!PaM`?Qb3-++>$Klq&=k;&=U z_Z9hP#9C9@E~w)Vr^_i>LcjW~7_Zt|{fK>qjkr*+&ws@o5Zx_zgnv_oke;A(`c-{H z$G_qszfbZj_;Ngr4U%F$Xr$ZZb?hoOa~oCaS_~e)6wS8qAQAMhHuTRxg{oku1W(ea zbp(^1dsDTr6epz!^AK`55zR`nSgAm#CTbzbgV&FWCDcIalM&jej7Z|Efh_xZBaBe) zE+!#3C}r#YU|fjWPimbdP%`C@6w#Hf$E%g=O;LzU3N}uj}ke=Se$X*xLx8623#cM)R3bX&S{(ql;Rp> z+iZ_>+L$m880NlVq4~HJb4=hucomx>8x~l~atZg%FlQi7991ne1RrWCD2(y#6^WWq zLuwsJqT5f^Ops3F^KTrB;V|OIMoY6mu{{ZYOL|%kR89?w! z5NhD~3Fj1yT_1{quHfZEHPC3vnC#qwIs;urz`!RFNX^;l`_;@`&J5KT*`;CG{UFmO z6ba_DcF&Wxom#(+YH}`yq9C4Xw-GW&TIv0Y%!Q!dGFqGO8s@e`@W}sUs{{Zt$W34$`3GMcwA8tAT zK>PdBC5(J$U;#xS3=j=*cLg(;`0G;u-#{S0cx*}F0mO<`boSL4DUP_MJq=36Kg=Lt z{1Qkj9RSsWB!RgwN{yVPVXEZbVgvj1xa4BygY6F*V=5s#W zP%mm@G0#z1>zztCR!>@7?x=Dk&%GhdvC0V=H!tK&#GbSh*M&npxfG3BkumBA>qFdX zfgp{*(4^$Tx$J0`(r`%Y z28on|87C=70y(V(hExoK0nRA}zghU2e`KVLdy35=(KUKt0QSdf*JeeOe$c5AFXBByMZ8p2X3_D&hypkkApzfg=l!& z9$*8SCA$){#!nR;Je z{{SodU`H@0_HDuy$l@u}mOfY}4R`KrZWO(Ip``t6!awbs+)3o>sM|PbBLz)D5>0xXSQON+^3v=$?_MwfqIZrt>_&OITSwb60f$dx@1CRjfXrx=H z2|U%wWI+T+(wNvPBHzRJjB{Kz^C4Vw^rqfSka3!cXh%-b6oxe+8@9yc859E8P*B7f z=~_jyAOa#y2JN_InIta(3^8M6D+MAAiaw%olZ^WTbfW_5i@*6rIqU{onUMFDT+sZHBlfSZ=kHmN_{1q*SgTU=Z%_Asr| zq$K5D@l!3P?G4-o6gY!Dh|l@2ojH1$9Ny19Ki>)SpQ7ti?IELeeFwU$6)A1 zJGm>3$Pg4eQ10D-NwKgj4o@W1+I_be^BYN!cBm7P^rNPgm!LLnoen~kDCtBsYNk}B z1r_@qD#lbCB&5oZKGaAXNeUGuIToT?->D$PL{$%4+$q4MI6wm+Q4P*QeNk65jl?MY zM{L%9TwRT;Dq$*809+MyBvpx6K_JYYy(;j6a*z}4Tv`(Yid&VisE9j3+^>p?+?mdM z#a8C+vXe3SYl#XlRfTq_m&sOq6}4al$UmhpXh^_J$6BF)R#UY(qMKR}l?5y$o|BrU z$RNMIyk*!4JAJ6j3%9Q}AdSXgf--AEYp4D#u$`NM>l6CZ26RzqK@EhF{n3$3bLAT1 zMHhX9Y4!}Z66L!|^9YkP;l|2P6yi!xaX>S4mK6{;_c{8}Zw3P|u(?-}TNQ?tQvu>v+ zXperh(U#PdjpV@TTQK9Z7Fp3I&_WLt0@Q+Hd7`~Lgm$XkLC(R7-^CCPNi)qss%T?ZU*8aYdNW=TQFes&0x*#HsSX(594(Y&kNv%DKnOF%cNLsQdr70wk$vbvBv)&J_a}yx* zO|-(>kEAfESv^f>gQ*T7zL7i{KTGo&y=~Nl>=+`hE~JG?T0J(!l3Pzh8O2`vKtZ3A zLusp1jRmBHlZrL?Qaxak9V^%4r;kUJ$!c!F6#=r8nH}njB<*?vq;e|M$|Q1{1#T+d z=X4aJX&GM@q{ri2lZmHgMM=$Z)R@Ia(d0UW?J{DCDGJSX@wF(Nxh^s?x}&LNOMr~# zJ5t>|qw#4inFqaUuk4XC>uWCUN9jGT(R6#ftL!c;a2Q^S_OG!cFip7>qx-`L7 z$gsxsa7Vow-MU-pKT3tT2xuS=tr(O*&0aVz%ADIEMmtwbfyC5?OfLhabdOF2YcmwY zh&cGDDLe|Xc+FB_N3>NpK`oGUkRdsaXh%}1l$;*4YJgOM%~GcJVO;dA(oK$wqJ;im zP|R{FLKGyJny@z?TH?{LlZZa_Dk0c9+L`S_zTiZGA4(~ANC;9~Bi^%@w<}tRx1M^6 z&6^}~qed)pn*`Obme?V&yAWtyFHbL!;)x2PSks+y<9m(aT=uOUrk0TgcK3?oEK~Aw za*@?}VS^tP+4Y5@S_zRck7G>s!bu%E3ca)?gzekv9^BA(S3x=Z*LE;iqH(1+mPIcD zSIFD(RjF#)U_@|De9Dwn^P0BhNolfG5feb4$X_%f)!i#cK{3gShLXa+D!4pP%?R^u zUtI(s80Y4m*jujIBh3SMYQv2@xw-rsvNekrAClKwgb-l&gIS$%&n<-k1P&*qX>acv zLMH&ldDgwFCvuVw+>`T7^72O{tCrLmQi4`EHRZWm(drawYzb~HFqaeOD9#j~ou~_KDdq`z2r5c|#@c>T zn?jE@B}TzwsKiMYp1~Rc4?(vV#PBQrb&wYHy@#tb!W>#Gtkb0YP0U)w3v6 zs1m6kp`_j$x78lDKl+Wdd7A)AP%8+hu8wP4K0G%JiLg@S-wGbud{9G&+|QjsYMPUA2lfiaRr>|6dG>Ay95(s_Z$ zwMy#w%X{PHLhZ_Sz!?;|4wC8GqE#T7E z3Ps9Np}JszaqVASwEdl$j+m6CWZ-n3)m^FlPR9^b-uZ&W zZDk|@^G9ix+OD5@I*zQ$&_?C9rO$d&{{T^~Sn(~YN|KpW&VBmSZLVd=g(#&^K<`Gl z_n&T+H??@i#SSX%Cq=lpd8OekEc%nj#TrgdkhaoWG?q67Ht7p*%PJ-fX|#=B@Nke6 zB#g~uS8DZ`4XMDPG7~e3I?m-xwp00)i0?+6p({cfjL_@Fnv*)Z>|WFoi?!IKrGiA1 zNzd1c)p$=+X6Dsas&J*|)#kRbjCB>~q@iUlJWr`vrdnR2#e?Z_pZ-@A1P`7mxV)LK zBh<+5`xTP}ULFutN2@sEowa-MA*URj@N*y$0)52>w5rk8ZtT+EnA*}XHi8M@{o-kO zxwvB4w-UcCggjG}hy{5`KHVv*(p|GGytLf9>J=zx^obbnBD<4!D_;oY#c@>rP2+3c ziI1mh$@s`yT39=Cc2Alhp!4OKMtytakD6K0Yl)5^6wjIX?oV-DFbqb z0Uc|1pkBN@?H^GQd1QpCBq$y2oDZnu=}UY~W>&(=QV9@ZCTpt+$I9{fFQ4!7bEO%0 z+3EiP@;@W$4F3Qw)k##P4ibfcBPWAJd7&p0F;0AWrUc%)Lvb_BW7)bYWjp;U00Z-0 zOEfA+smD8wjuiNbjB`O;TuORoga1KY&lBYa6V{fpgHH!240ADoV`F&5r3iW@$a;hQe@6d)C;n6eNHF?@O+%cg)8l$kTq zwP~O>JF+Fe^P`un#|KduHzd^fZPXM|RBsjGXlLqY~ILJm={{+Mi5x zqmm*?9ji7)W7df_*byIkASWDG$N)s-e${$YF^a(@vqEJ)QG$KBs#Xe29FEk)H!1e* zR6Nj*ze=SNSPz-Da6a`v#Gb0E9RyH?Qc2u?)YYI_j{H<4{qay$$$`ZN4E<_oK~ctQ zs&m5IbxP?cy)(BZL`E~kEw^AOi4rNLx=Lg3K*K|eRKNu0o3kn4Qj2APl!?VLZSE)S zNoaB%Tac(H?@XC)PijwSz26k_)m%?%w5+Da7S_RGIK)#XY(aoQIO;J_vOqD4e{R$P z!S}5&iJEKy7DKKDTljFCMD&=Z!rVaXn(1JXG3+VyBE=z+aR5$ehMOCS7@~_YF&P-C zA+QgmW+($k5F#->g%XG-5KUGS02qTcXd}|8j{fy%rh;2k0hsMIXjf4~134Lr=~)07 z`cVKBT_UJ;V2BvTYNfp*XFaG#pWBhgT3nqDg4x@MAmRr>NH368#xt5^?ats}&$TVN zStRYOGiKPW8QaMMP7l2zxP3@b=6)+je1c?T!0lP<#>gb%GuDARBSmcAlqx4ZNi^Em zRhR(5j`f#Xys;fC=5EVm5JXNhR^>p^xK5-ZO0X)IwF3|pHJwH2=&0u+sdD;ePuS2m zVciVwwHTjNPgvPaCo zsn2>Ok}BIa%pgDqw@M(kk@M|Jt#3kr%#-aD!50D#WA8L}h?6WWgYQr*fMOHsIOamk&LF++3ZW2E%(^5o0jAs=?$lQ}ZN?ej?G};ipkeNC7p~IU= z$Of2RAi?Nk?@2CP*kBw@Wt3=qU5weg>}Dvjav*d2R#)+1D#^`UjX^R%QRAfobRP>f z9&p+xf+S|DTZu3?Pe}%mjY|N}Ku1{>_wx{_&OR!f(P{&N5@eChedU6vJVcI>N)f0C zlB^oD`G6z-nq*{(abq;C}2Z6zqT*7ahOE;x%;$^QUKM&*90A8PT}0f=0WrGAk96j%*E_5=E;Tlhu~sWl80`C~s2yRy~DQAv(PeqjD9UHPhcpy^IFh&hvA zrMyV6E!I?EabK0ciW?2%&IBb&Y~e$bUW4jxyjnQFOjqRDXqQg=28*9gPcZc#TbOBq2y~DkLFzm@$ef{Q)c^Y^hBtFrmmjMEz^SK`L~6 zR{sDbtkd{=M%z&;+8`BjJ61==w@}J0w{23-ew6KIXx6-jX{razO zUPEWnk&;GsgUCNL+f|@a-LXSzY{vOf0tDi#Qf=70CdEL4f)<6RzcEz2ND0_N6&RkU zn(eaGk1F$cP1|n%Siv3O^G24g%j~2y$as{lNaYCGIDQaSpK(lza$S?4&mn0+OK#?Rj%b@Sx~Ufgsa+sc?aOLNm2OZOA}Em7$R=Wu zIlh?0jE#xZMM=pw3#$otzEfFkQAhY*j2VIq#4F(!!0LhAkBAi;Z0G*o>rZiwzLa`uk)N7 zh{2|eSos@x8ImHMp(#-K=}~aB&?P_`Khfic&=1PVEirSJh>7BOH6|^j*`kNsJ zdc{uBo#-WBVL|DxMD-sMO`xSxKWZy!KqQWR>bB6Nx}xER&k;lfh$HiMt+>)i$tP^2 zLX1q+r3wNDYvi%DQbsF|xI~I~z!m{|2Gw_p&fnV?*;Y2~>)x~%AfA||m!oP(PaOPK z4BN9_7fl{(u38nLdt|6g5OX!<8l9__cRSr&f=CCS`fJ{`7Lnz+w+DQR^8J5KQi|Md zDN}JJKp#zce=8k3Jx_^}^u-p0ID|k71zSvviqPATn}(bsLQ{fz39M62yJ+%~3gnq1 zt!Ww78@IMYmkx&dB&FgHcBlsPN%&061hrBhUD#x(=9&XPb4W)5IEyCl(xNX z&@S!LOHK?E!S^3p(dqk^+8l81axw?K1%TF%N$~y9Erjln0E1c-DJFLg2PUu@Md)oQ zQIspHPa?K9S3iqrka-*t73sVj>Tyx|Jl~mx#$1lA5aCH4lbleua#OhicJzuq@A_#F zXV`S8A!!L4z)HJT*~ZlwAu4u5NPPtY`-lggw z1#yU?@v?FDNUtIn??EbRBn*nr>sCH`2u=tST166}=mR~C4(bBLD05qK!DWXNE-Acn zH#RnrlO3y?Hlc79ZI;{se=>|salCoU z#L4N)r_t2%BOfOpIQETkB5UB}5$!{zrG$ucF_B$UD+&uko}ie5Fl*37rj8t~GV-Ye zO%AeK1oil?C4Op2%1GvV*Gd#UO2|Dab78@JlBkxmT0pDrwLW$A`qh!#blq82x)B94<2&Z>F3ge>LZFxscO!)PQ-;{psU80aQOvFBu`o;8Zuyc zrg`AfB`o<>MNsIhSl_wXpJ{JjI0Djk9PSkM*GNjXkqQxsqgt~=UY(;H3NW}E5QraI zyj~@4Zpz8z{6SDJ;1GmC5=Yvk6o%6W1KObd+DK3f$n0nzIaeeE40ZXf7mpsIAt_I? z%ZshC2XXi2s71G&Nh;^sqP9SEEZ_qK3X_*fWhF^4MQ4Yo& zcS|kpR19L61glU7c#wLEDVtO>0-zPg+La|N+FC;*63W%Pa1{kdr1M0$;#A2h zFhTm&cW)uEN>0)aK&-BF*M0e9y~#WC*(-`#9ElOZAX4{_OBN4Mzmgi zL(JS$)j!>ylQ^ZrMjDZg(*Y!tYFAVK#P=m8s* z7=g&3ubD%MFa&k28GJZOG$R+6ILhqsZEHbAlU`~NN@T)CWUp-K|d4DIwmL2>apT_dzm)iRw! ze=zPVt$rTyY7MrQ-UD$Xg%jR{dcx_u#Ilv8N-2_J3Qts#P3pd$xJzs2B)YxwIrv_@`m{%o!-?1`DWz_S=|{C)@cK}&ZgzrV)H;NwhG#p zTY51({iwAou-xULJARORKp2Y?<3IpJ=TxaOtllT5F>5 zq5({ZDLp}+O)A!`^qXZjd#pEaZtKLT7|NsLt!I~79=k8_<%(_IXHc-YTh{J38&x14 zxd*L!PMxha_*Jglq_~huQ_Nz#?VE%4J7ZHWN?0E&I5*xXvIzjBg&Ta<*E zimYx)=$RuKEsqWBw@OCU5uL=IfcB&9lsvSp@w}&y4q}rFmRP!K<##D%t(z5YuTJ8B zxPP@V^N`ly(DHo16$&#T^`t3ZY36=4b7CvnssTWqf4=WR@9PK)w4zA zrL@Tj9G};%Q>7F)ZkH}y+?n*104E%K%{#u-k$K?#UqfM%!u5vDs_u!Bi4c<%{{S8s zxwnzln_tvMY2;B$8oi!c@9|eq@h-n*rs!n0r(Ov|WCd>>XWG9*Y3O+_+MD}#RHfVR zD++|D3=OBDo|WK#!%yL{@OHOxe}8t}>*LVb+6KcgRq-7w^iH#_vhr;$_ob3uN)QJJ zcR?QFwRmYt6UjMSv}f}jPB6zE0yxSVdA8UdN|!Kq5N0JH#|CBz_^ri|Qgrr($=vE2 zhZWX()(YeB_ZGmHkg?_|w}W#j5j&@-o}KG&pSOhQQ$15ezm{pNy1ri-%tCa2h^IJab!!i{hp7`{{X~0QdC@0QcRF> zUM+Y$mzEHcl!L{7i~b?md5m0b=&8?2{Qm%}wf%Gje|M*JlaH#gKxX_YnC(<&K zPh8Q(*bL}Tc?PO{0uDIhtS$m@4OHr+9LTDZDQ$@Fn@Iu!O#AU!O+}H~MD(qNtZcsKqWXV;a$yaWJwfoujl-R|y=<%}|ujIq6*py&2jqX&Xp1(#ddpe9$c*7?Vb{ zl9(fo^tT|CO&z1&kUNN@lW+tH;XZ|>#jlO?6X~VE) zgpYLnsEOI;NMM-84HDJAv8JtQ=j0!y6q^DE9~rH#Mp{5DwY4J=MFEb3y;j9nEPi2q7{|RzAh?AaHutlF9K2n$VbuD;=a`q}NP@ z%>C+?UEVNi<#aRiM`Gx#?T$ouH3-QOT_z-g56xG+0sxHr(1cuo36HgOr2T57IXz?A zteK2e7IYICsI5c{m^Ct@2ek_FrNfRdXK$z`Hn>br4zmyr&1!%_f!>KAS|n5zh|Nv5w_ph!D)#J1gT(!)_H7D4C%v6fsF2wJ8!357xEL z+seN#Xd8n3oocg6AZe zHSu)B0!$h02B1=-F_|$@T1h8qnma(zGV}~%xtfog0Es+I3aKraKjx?9l5xjc9U{=e z+Tv6okG(pym5h6ziqG0MWGPV-n$cPbN%>Tf^j_?NYx8rKDdFwuS3x_eZe^ZV-Ypr_Iiqt;v}5@wdfia zsGt!ljP#+Dk}fH;+wAozNX{qiPi^lvCkO3bZKdB6v~Sz~Yh7b=xCt_1u}>`MI?w3 z%tdO0Y|kX3ExvPVNIfxGElK#?a~$(p%|SkuP7Zq3D_Ou^+|M9Y$_!oB^d7Os|+K%b>yoLVG*_Sv4f;6>ikKxsobyO+ zu8N9eANi*&IF;vgcE?&fuxf%5lZ25ZRq1Gv*Ph%On<)gD$US(YT5J$F#yFtiWace- zovk>cI6*kYeyh-JKLg?2D$plkV#$`s9SA_0{LJeq^3;O|bSLgWuhnW!#O+AJKm+Yv zzokJbP5%HxiS;@1QD4{o41bAwGF5NZxw727!Erc{DZINU&;zob(Oi+$-@ z+W;5>vu#|hZQh{&0P+PXZ9(M3AGi6h7hGK)!F!d9{8$j%mY1!aj>wTcAR5Ho7aF+! zwiHt-lhEhwPPKdYFIl%~^vjJZMn5u07519OhIJMn@q>Dsx=XgNf&95?OTqAx8vOVu z-y4Q8#}PpqavQZ1HF9>8snN>+05V4C?OnKeNqfvF^IKFB0Dvo+a(@idh+fxJ>u{tH zf2Kj@C%-tVU3Tr2xLYB_Ob|wX)u)QO7M_T^e6o?{H-bspd)GXa-UxLCxa@k=O~{Z| zpSMF*Ce_E3wJ@@QKi%z2NhSCtu$5@42l_853XTbnot`6o`wLqV4fnbM(~mvk_g2>bSD$ma4MU}9|!`_XQ7HTxjF}+w-8D_A_(u^gI$W@ z3x4CvM&c12>J}-ueQ|I^jzOn(_6sC=$q(%wgaOH-{{W|=eZx-;>8nGb^*UW>jy|1h zSgq|gJf#4c#0mcZy=uJ%-EFjmhQnEc-#{q6MZ~F?Ij}E05VHa)NWn8UZoY$^+R5pdDdZg_?lyOCi-5@hQ zF*TN!;Vz{KNGeY=TNYZD0F95rqjc8O2nmWynwj0R}5vn@eUzx1*A^5jan3EeBvp7zUt) z00M_Ss%md|N|a;RP^}i-mZ4klM;uWpN`S{AgCSA?1dgV!N{-NJ@M0ufuvBFy0;^oi z5!0Hvm6QYK3{+f&FVmWHV4-!&As{D;U3SLyqmv(caiHW5>>fhrDdFL4}+#; zbp}XE(71@^D;<2mx@~NwTR{<*oPA`prauB*5k$cE05dnN;e_2m2Dgnd9N?COZGP& zf0qR&2qpm)#mN^5*TXxw=BFF^Jqto@S6X@+WEPrbXKN#zdv>igwc~cy(&B=Z0zkq4 z0J*H5gQ=7NyQbDcq;BVGx%!Tj)Y|!pX>Pof1gsE}GOlXhS{L)PQKxEuh*Ehk;X~uC zdJcttA7sGev9Bwzxer~b41f}Yl@kPG_L|sfxBmc!2q_>TAV~r_rug}u8QGi3#Ii<@ zM`J#6FZ1&%JuAIpdvZ^Eq(K3)x$WOO5Oq zR~V?{qKkrj;dKJERL=SOCD15HKjO$+kOuQUK@gVi9k_v`t9iq!> zUi@t)Gta#^Y0orz2{imUxhi6EB5 zAQ~Sh63H76$H?I?*qxI6#t@?;y!E2WWhQxw7S`Ohktg#CGjHVslTX8H3dCfmCq-&N z19DCe6I^QCsnsdPsLo*Yt4Q?vv5ZwqdDSQ-2@%B6y1QU6*dVLP5Q7!PK7avHsG*fC ze^Z{7G`4`O_U+b+lqPXJ%k_sE5`+xAd#_pcA(h!vKzZ%}Sf?mvQ_7hhB8f z(3H%3)Es5?pF+s?G{r$G;{=MV`g=+1L+M{^zIE^}MT&?eXe241D7x{ZdAP@-Wim?{$wXDwXL4)q=J9xQXV5q}Q=oYmNKaL)S~VP!_<)IZas2kWdS75Kc(x z6lxo4)uI#t{{ZX+Q1=(@BLQBNr_~4TM6o5gvU-i`2%L*Vou)w@rh>WDQj$O;XwSG5 zyH(YH4uiCa250G5O;^OO5?WaL#7D$6eN( zE-bdQxSiNB`qyqeK!B6E&CaS)WZ(~vtuJoLSBPoVwdSM+Y#XO{&3XL(M6<~B9zTy9 z3u0^6pVU$|5*^^dkbVCEN~~L^-7N1OXi<;{Uz#~>y4qH>kHe@z8%NSAT6LAp+o4`k zkC!5P@imzwnXSDHtQq_psc9rBYBCQajtH$Crk>-fQrLx+90H#F)qWk&TpLJNnzFK` zD5ur5^!it!==xJ_l{gQWQ-vh|0B^ij%vfic(mDCFM-sMIqv$2V+Me80f%hGaY;Ek> zw@P*<5^`X1T(?7P6qN8p#eAn0qP#+SM@sbmJ`8vx*3UPaJoC%;Ci{T^lplYZ2{&#p zAi)(%mcKJ-1z6doAN#qmJ9k6rcitGCk`p;te&cl%GH#@*-e z6b0?NJhhaK$|H!+6gly%xY`+lZxqu!4_eb+Qq9B6IE4a2o&r)-2m)h+)83aQ&8jUp zN}K2-DJPorKNRUnQ!Wyf0P=lc7+4&L_Xcat7*puDN(+0+a2z8G?mk6$$wpY_;?HA_ zYa|qr2b9}xl`s^w1wk8lBdu3-{oS>rWrU{T!lDCYgv@)MwI2~)>Kb#c9Tvfh>t#0& zN%aMF$H!{A&f!}2kLTZ6rpeSfC8^!HBaiu@deRLm+ZQqU*whaDXF>JbDRLq(O)%A& zXI5>bUbY--XemKQZbZj4GuuI1jekx?5d&$SAXhY(tt_tGx^Zh&%ee^LNr(xq;y*-9mTWon`!{N<^p)(lm(wER*2E$Odx4uyGno6H; zLKZ}npsKzn>0A`6d&ALdG zwg4*Hd0Y-YVk$O)Nt!2blWAve>9lg>Z%EpRAsx+VZ5-QX+if^s;DxAUj+v}pkZvDk zt?G#r>kt$q^y%8PHVattcDAO_TLYfGkG)@Fnxc;tU)t#USK{?uLF)$HaVK*s^(X@n z2PSy0Oe(l?-$@!u7+VkCkmIiZ0Q|W_VrE7Pf`E?Zy!XU@6w^Fiujo33@K=?mwv?%c z#5VfSvZR%N_A&wQ0=+dlhNS+gwKny#bdhe5^`mGToLS0_f4Lw;`&UcGjy1_Q_)5RO z^e|_X-x~ewV{pZ@FTCJVl&7{h8VK8kVMDZq1M^0BYguyE+}oDibq%&$L4oU&jQ;@O z;Luj~*G~DYt!3tbNNlDQkPJa8?ZNh{j`r^D?=AeWq&T3YC(=@>kQD>+oWZSl@=F$F zD60Pe_hwn+j$BR>cmDw9fxK;MwBE2*0^T=GEk<+shXnQ=D_MG(Y;OR;kO=prUME|2 z_O@?o!6Q(-x&fgQ8W;fo0L^G_z}TWyBokflrpJz4zq?=A_He$VCoEXM&;Ab@@$g*S zwowRLK?*0YYW(}Hwo|=BB_xseuhKun1gFY*Q|bwbug*Hejo=t2NdlGUui_ef2YAYZ zVM*NpPDk3Pae_(5N;=d9S~8>lnEO}G00stsaa@NM&W($_keTBn=Dtz{d;Zlo+)8Fg zPLvwm1eln{YU+t+UTrGi$ffUE1rlRF6mWE_^8kK2)u91Ltn?HNBxfGe=Y-GFf_BGm zwmb1!)vyjg9`qyD$%7ym5;07J0!BgGbtWBM z<F}vzhzRh*$&Wm8Hl44rZ?o$o~ME7Y&4@F%JNcCyK4hpbjIN1#3O= z6g!R)k(wyP89N-YeV8ZWn%iD(jQ*9AX;~YL9)@Y9o1{S9ijqhO*{`;^k=~g$_<#wl z#l6^pYkz*1=3n;yYr1xwPFq%{?J)o^yX@Qb_^; z%`HNdg%~xq)GSk@fEAHd|(sBVeA&6AE_0kv$)!vnZTwd3r4Mnghz2kd7!2wM0Kp&SA+m~ z$4V_X;X(-{d(m>=>R_+OsM8V>R zLczh0rCJXo6Wq|rmlDORXxoV%)Dx@0ApF(tk&d5Ppj=+%e0HpvCq|gY%A8z&VKqXL zBphH>q>^|M`qvVP21I<;8Dz6uVMNf^d5Qbg=xhj)HN>eY!3Q-jGzCcGgF=|GQsHp=Z9xPNO5yck8nFoOpO_yjlOhg{kLpGzR5$#N&V)R(>c9A$gO1xR^ zA~11HlWhQr5%E&;gc*??Xc`!VD`1EO$(o^7&{TK;aOcRq)8hIxIdPi#3oj8C5O!Whb`8s@Hr|LRW7H~ZS zB&{sY69f#@hL)f*ou+$M#Wn&7AQEc9rUQ^M+Igw#gXqzeYfe2t_f&p{nKfP$Rs-sEblafm*-hrhg$G=B!4Hs1lr1 zS&Def?WKWH5zJnK=kQjEX?XY{2XfFP2vKFl zSepW&l&H=tM8=otke-aCiVyD|VEL?ekC9r4TX|1KO(o zUQhvyd(lNGl;ves`|;`9)YH2+NLMp7y{l6*1deHqt3?Gk80R#^(6QyJr#XR)(?*F< zo;{5vxAcHH2Z~_RnC3IhUn&Kl^S1=>3Q7Bl{Yv2c!oMml&*(XrI|=x&)fVoHi*}w* z4k0QZPz`pzmn(Ds05ADC-%($0`TqdHoY$Kxws1#6Yx8IEHql8^mJzg-SEhR58BrPA@gZ`o3vsYHe)zPNPM zxS9P+$`{XVI7sCFqLJzjxo(st^0c?KDWvp++MQ}e4QelxgARXCV>? zGqyOT#+0@_^eAE05qO}7mK2Z9+llAk*KZwCm${N)$uZKdxzctxw;NFqCjx(3gRLbk zr`AZ3yBN)FxVD-Sld1z<^Ubz|sW{05r=>Kqbe6_8i0MNQK=Tp6C+2k;SfpifBEQdN-?|+KWrO(UG*x17gr@DJW91I9ZxD1e1iT_ad-QvFQ25&?r13dWK$sHe&wQ_){3;fMHeomu-R5~?OSU$(&CZ*G^n5rr~N0t&1UQ^ zZS3HZmXt)2PfF40cWk7fHtJMWk(o79tF);F1^G8NEg?*!FaVQ36t~0|2TeeI063F~ zoJ}2QwV|c#1f8TK5uQb7n7GyNFN1gJZA@e>DoES5a6zJzN;C{^#Vu=hI)kmZ87iII zaqm_qPhay?r8do}2_tz>%o=BW?X8>bB@HESsE|M*2`7ORc9(3o8uQ&7R_eCx1B8yB zGg+jX)tXf??wbv(%4tx_0l*lqPVksh&KRa*Wr1+5J(VE)b+1bBn#HM| z!9b@4B9999D-)gj9Ug~oY7(@PNg(77D@5C}&jkmrAaPmEI`K}e0ciCoWS$TF)`w{F zfF@EPVArehq`0~~uaKRqD{Q9$Fi%+&;?*Hdgrv?h-jgzs=K52#Pcxs2Y|^1TMQg*i zW<1K39e5O+;0{dHEhHgxpHYaOXorXim?bfpp_?&0k~sIK&#`dmu(&bMPuhrRV2Pa2 zE-1=Gd(q88_9Z7I!m-Gpi?&?43~2-mM;@7}Eg%9@(!N3fi6qAn*0`k{`Xt%AJJoKxxAK@Z_U}-~r!iDqM3O%#0oVgl!~iQ0>+$5#o;wIcm$RWeHi@L5PAUrFw1Uqan}~LY5Dt z{{W}{j8f~y5)rY`rS5E$sEJN1i<2Func2sYrkXrKn~R2B0WA}PAghS$Ppt1!;u=EI z7D9|IXCVIowO8WJ3hKsPDRF61iB8BnLF4}WR#wjW>nBpp;Zssh)h`3yee0ie>77KB zqiFS8Ek&a9O{6qbRC1nYoYsTQ9->pV0XvK+7(D*~-qnxr#qF<}Zd!Bkt%(X!AgHWl zh#tc~H2vGKwvy|q2`;8o+(82nexjuzPqChtbUzlCR^Un*JAg{JZ_irvy%Sez=hCGF zs$>k1YsM3N(EmlkY_+IKD^~B_+j-fsr6% z+N)Tcjw`lT4Xp%01an<@gosFyT9lNcW^0QqWjlluB1BctgegRr9ezEkrc<;G%=D=# zcoHY1(_^Fq?1~+2+%!p7IUPIK#2wHvTvpUdlL9)_S8x2!t7P6hG8;5^!S6kWJpnJ8f2*OaP3Mcy4&=N6+mLV~kvXrwsAkwDDoB!|tSEa{ zET2=xwG(D9r(}DPyua5H-sHGJ9FOr>9b3fiElLb6DpB2lKPVhcDZA8MSWA~BK>;c$ zg=Tuk+LAq}rMt#k9%2=Af2X+wrcMAV9A`Dg@;uq)>CxE1;lmg1G`iF;ZjgnnkW2+2 z`}Cx@E*H)Z_hCjx-(&Tn7RBF@c!wTR9$dC+ag=-4DYqhte5Rl$9%&4+FL_xu#mI2fz3t|Kp{$=fOMqyOL>Q(k+{sk;whc8fyn53)kihDE?B8q zVQoqr0p5mn0igrZlSYS5_>7t7iW=+ z9WZe~ljXWpXKCs`+KdFag(7`ImyPO}*~fN_XNi&=ZA#jeAP}sm4l`awt$?QxPz%Q4 zOWr`*6112?r?0rLV%MAglZh!(log2)j>5c;#1`)jj<&R^wXf2EV`9k6`*$PSyuZ{F zSu}cor@My4>ANH+dKTMjR?<<0CmfHiYLLsRaH;|nu?iW59{&J7)KZWw8c*UBhYF4r z9@q!zR11Jomf1l{H=yp^T5a}cS?1CvzRCrs4P8PhHidbUEA zSR|!ea8Dp-8w;J@RXL_?4nU87&XolI>E>(rBoCHK3=8megCz9U6boT6C3U1xB zDLZy4vpFF{)jXWieM4LEN_AM+%gAj&q#+?lSQwqW#B)a6Tr;4v<$fmG-r?~KI^v-x zIi#AVmu&vF)~UMU(P~7iYlm+G)Fb;(x6LML^fDilS7xQ1ISRT%YPlr&fwAckz^X0U z7KnEJr56Z@WRWD39+G})ci)I%u$+lm!UsVas;rv0)@}g2Ysef=^ib*BrIC)3o2j zT!-%#6Nb>Bg1J3O;Pk8>o8#Sf=J{>dXaNlQn_+1<1EE(RYAJsW`BLakMo*;qFX5!&N$kXuRS}|7D?KkAtR+^Eba=KwNtrMlH?&?kU4|gjw@%T zUN@$)r{8rg2Nf>5vJ#YnUy|*q~XU=8D!)rU5X+EMI{r*l~SvX;opThqD&=-m>r22d+S!MZ` z_Fwxv@0%v$jxX{3-~Nvy{vgVkbz}jKd9Tkpz}e=ci4)Cx*YN`3DYkGA%yMhNHC3g} z=WtZPFlVn7kIr|L=y(dT5!03hi!;h+A6nz?V3J^SgH-9u39&*Gh$r_o^NN`g0XVK# zJ)J%x>y?syBpiHDMcm`#nlj*HIWtQgImRG#qU43Ru(Ub}l_*bYEtL_@4J%|F-jVdC zOkfGcCQ=P17N9oq6F}VO`?wwGvZ4}4KNPanoKp$~KI3^|;(l z4rwjJduDoetzF|M)w*NcaY!#2f=C2M6I*c3^k>NdIWVka6(FJh)dxXNd7iZt@hSfR z#ceZ%09Dqwv?S&*c&Mk+1d;PoCP;%o6%>F`CNe0tUXCh=lbqDTMCbIXdqE^JZs33b zJc=!wgTaVB=}UMZbrpJ6K?EPTtnyYd4_mn@0wO)C{{Y1Q0Qg^eNQWc|J!qMvG#R*U z<1i=Qjb+j}$F(j(6gnCu5);Y$)5TELr*|Y~p!6tzwFcXzJmR{ir<@AW320Q4uuHbL zasU~u_1(Xyf%Ab{M-JrYx89W9pdXl`aw#E8cKqTdo7l7j0UTz8Z3GfH_NG>Gy)ppz z6c$|%Es~VMbXr(N*fq0c^M*VmE;KoRUzLr?Fh*mjtCMpeZYOuSqgn#&DfbB+^GjZPYNi1` z#;PVB{?_V8?1ewJ|L090@84+DJqnrb%+rV|tG zS}T1pC=40qfjcg!#SIi}!UUe7HQ(gnG9o%mR`>a0U|>i+I?&H)`%eNUOw(?VEh8w# zl$4Vw26ASfS=j{bfyN+nR{k6a$&dS*I?(_`ayw9|SyUZoYa2oL$Q)7P+tMQ^{YJZL zi2_Dxhm*UJf!Ncd8y4kEO6z1yaUJMY0+9n0{o6pOf%;HR*{I1J^pQotu2v{6sRyUL z3bnhzw5DUc)vj46OvvZ}=|QgDX(E38=#-p6;-naFN|Pg)oE~akU?EuN-!!dBQdKIL z>0MLBAm{D-(E7}^ii2M%SE@uF)UoE$2|W)=gSSQu80q(+)ag)}B!Bo+YE}!46*$0B zi5pBFYIgSAV-bV?YpEqlGG{rF&%GUP2YmIQlMW~**31>jPJP8RZp|QJB7g4`HM^lb zbMH+Vcqt%^8eWD0xnwZ_4zce;uE28?-*QY!hc#1*OlD$wj8=K*G$}U0Cj$qLl_1$b zA^``J6hn>zPSY`np%n6PRg6f1OXWinvh;E`ow3@9OM?LC-jxfJjDzpQ)Wyh?1wE=M zmf|~0iCPqxf}tNBzH9W~gs#(n;f)^Q1dzKnx$Y7H{XwtF4P22BLHMuHzrpQ8@!kkw z3L|}^?y!D0ZXcqBcD|Q7%H#P*8|pCr8kgal3-+Zm{-oEF`14B+u;$?@AkHh;Uq<#v zwPJipVjp2(j+N@+?}Lfw8{su0@YjuPRi$dBX>CdcMs|)ttUmbj8z})WN?`v0X*m(r zIRd{ye-ZQ`l{n&)J4rDgZ}VOctXL}D`QQCFGMWC>a|iwF!+Drq9IW>qCRLXjGJ3fR zyec9c+X{~3uhdp{;TK+Z`&e0-gVwZ~%p&0~C~QecAtR<>ip|}$;ufXbtPepE!2{dg zv*Y`Xa_H+?g5N=I<&xTfgS1D@MJ13<#U)+AR8^LNwb=x%*OhvgNGTv}Ep5REozpT8 zNFsu`vO!aYvZSm+fCfa<_{WuW1-c1y%eX;9-1I%_+jf%MK?qD$R_Rh3LLP+#^#VWh zM4$qpl1EcCsXEdagBwQBat1mXt9MGjC^3`mUp9@&1gL$hIhQ z0E&bHHz7o)cfmFDiBgiBk_WuiiAl)h6NxI-doI&eZgR1H;Kex+L zyu3j~W4&s$WVoLsJQ9~29-t2td0diKTxqk=S_&->Ejy5bka9&Wx!#wHZMC=r!ht(P zb2R42B?w4gpsG#`O$B_y+;K@E5TDEfas^zKBBJ(XMx5pC=zi=2Y=AtXvjoS*Xl&m@ zZlPeM2}%f13iK&n{^ErUk2B0dT|hw~?>?C7peokfvt@goTYh5rKp5i__N?@Vqi3Tn z4lHj0vN;oiW4&*5fVn|$VMGZ7pz1zFdB&5hHMIqne93?yNSfX0cgk#}xU_`s3n`CO zn4*l8*u0T#9*?Fvgdqz;ilpu$IVYOaq$sqQ5Cna5SsfsN%0pri*BLTNn$}wq>nYr# zPY1Nur12%mQRcj;(;|m_m%vmJ)1=del#&OiN#Igq$w*0Bp(iQ-0B_QmT0De=?d_hp z;<`AW7e_xTwP-QM$U(+OS`{dQJ-Sh!m=Kj`iXCl~&mhHUn1~d(ffA#cz^hx{!jh!I zxt#G_1FHIWHCmKX;0gSs=Cj=nt3y`az!Si(DgsoZJ>#_w%J%|bNyi+~2~P99O5+ji zYh+&}Wono>d^44gc{N+%D@XLiDsT^4z1BAofMR43QE?zPV+xD`S!Wl}__PPOrb^Nv zb{%SMxE2XgNg!f56~}@?Mn5qEJH;&yIMaeA20oOYew*0-Jo+4M-bpdE@g`_@SSm>a zsf^cHcku`Vk~1Q&btB7#0vD1L4_eWwD-pQ5GkT7a!|r*JB?oYhvC@Tok>wO2z4$(p z)JOdlpt(RTN_{KnM*ziU?iNwGwWz7Wm`o-jde=WJV+YaD#V0)&4SBb1mkUatK|Yj| zj^A3wYBqM7i|bRZqCtW-51~El*j=(0Xj-3SE7fmP3ODRYSS&V=?k@{0hZCzTKdj9}o?Cux= zklU&mLCkIL1wnR6OXc+^%eX=aON3+&neYB9KI8W(bqgo@ssK2EnKF1F{8lS}ChjTh zcx97%;wq$@H#sg9HQm8G@1g3#PQpbUVXW83~|LqV4i%Dm);(vn^heMtlv z$5F@9p4vFOTXY+;fB7LwNRSg4N$z0sD7ci}BXgtQ3wBA~Awp9)oaBA6TQ3jlW!8yu zP>-2|oF9W;L2r7p$C}RVfhWrbJwQnZJc1AJT74SyxX5)2QVN!%y(+@s^&K%?UN8VP~Nld0s6iR_95C%DeL&@rUwEE>zaHP*`7y2 zA`a{d=bko^IUn6sKK|A0PEM=Z$=>{Ujv?z_sSmP?I5oE(@!crB;npttLw3Kc^91}BvjwwCQ ziksaDQd1K?u7kAFrJu&eHaI+thP5s>l)&1Uk8UanxI~gnnCV_?dE=JQ>X5ae3rb3% zO6mUqG`Zg%7L>iK-jS6?c7y&aB)+SH2B`frG`l@rckQFnlLVf;P*)nu4NBIi0B0lq z@m^v)VDsfCYSsw=6o4~5XdOdUym^qRY283LdpG&g3kmUZ#QIpWD52r#zWACU1r~LrDX< zt8P} zCwAoiSp; zoP`{1lm681$40fXAqi~+6cDw*!5n6`O`+dTfqz--@^znEH~O{Drn zZVBW4>vg4Qj9G0cKv6!Dp~#LYy|NobrKvDJpM6)xciWEE$ct=1e-+`@!)1oKRMfY>{Ulgy4PrCWev2u#STaBs5I(&!mU3Mf(m zN`T5^5mIr#DIrlN1d1)CCLv;LjV?t4_EnRY=ca5@sNL~;AkZ?!3(%!sPWHlzh)%!MA+H8=_( zKtxf)@Wsn)Sn}YjanDMdFQkH?6p#V@!lib>QUZ`*#}h*_zt7yb+Ia-@q1uqmN+SJZ zW6ioynT|@2COTJ)>-&=O+frLCyCJYiBu7yk8uq)w$c`-9RgFfJu%WL5lHjYl$EJUBr9Oak|sX%HI4ES zdv#~3Tv^&bZrD*sen|%+LEy$m%{QA7!3=c^Ry9jhrtP%7B#<_4*k_Ud0D93^mMRur zPMR%+(`!p)yn>+-z>Y9PQnpcity1RA#i4|u2z3Q5Eh{amVCT}Va~xE4E6sA=>6Uc6 z+Xk$+p`@*Fl?O-w0&^ty{{U)Kry6z~V*dapuS~X8;|NBs<+^SWyUlT>LuyI#8L z`x+Nq*R+d$H8xsDh%9vHQ6;C9>IzW)(?7id;u4F02O-ZFwC|hdk`-#+$5f+_$~o)% z)+wLi<-PL^J}%vU(B;H9$wJeKi0eF6JdQmgMhaVOd#2j>t^0*PeSY=Ly9iNmTdsWc zkWZEhu_}QdO59y)YPh?$V@_qYw6>796tT5Tg>sb%OuT}Nf5hWmB6LX4HKX*nT06N*IAirX5lPe}CbD^FqDo$aXk zV1R-_^#d4=)jeriRnEDhczw%7UEMz7UuZcfWt1f)TgV@ndQ1)`h0==k1}&dj-ECq@ zSW33ec`5JvQ)}HrPq2CW-G1S=?~4exY{)kYZ2-am=pc|jYgRc&gV+B6pYURueE$IZ zvuAGEdE0B7mHJz%V*eR)w;#NZI#av%@?JxEp*mWQWCNX)CUrCS>F_0hM00mjl5U7@gVVF zT)XZ60G`hoX4`1R8N04*Ldd^Hon-|<|$1T(nveA*c zKgSij*Oun7s|M{GTgsi~Da<7~C+S{$;v2_xO-AzZ_4$uHr7dciBE0vJK6zU`ACC?g zk``LHe-79LWO45`Vw9}&+J(2u5zZ6wMG?@PbIo%W*|VTmNF>0M(9H(6fgtfCr5?7( z9K>-$DB>cB5-qj|%@oj*kq3|{RIUlcPJXpxYbG-u!hw+n>oCB$^u*V{cg_bWiUUm%jwbR$ArzRH^J!b5HBIt@2~p0x#B@&W7CfGQ)J_&Gn|cc`L~IWVHV;xK=~?^O~p^VYbN z+(_vLzTdQB4ILthqoFc;5^+~089C>#dZj9nJk`h~kOvb{qU`xeAQA_-rS}Qu5ByU{ z3Q{1RywdBJ(h9bcq0IEEN`ZS8t++CHt|9HQnf4epUBlTw=^SQ-L#K2DC#_pW4N~bn zqnWCl^&Xk1HfN90ib@o-g#9Rz&@2SE>Li$hHDXdkW9TW0EX#xtnVw>e78^oH01!J> zN=u?Bu^B5tBaHl4Q*crVGx8~$wmVNBaZ6uoZy-NR8Z_VuZP5ZRig{ zTbvt4UPg7q6cR>bze>K)-~d4O>s`8J0&&;q~^%WN8@ZGk6~)}`hS@r>6NtSf?ergA`219`q@_NA8% zq)f!~?ek4tR36l}=2bJtu%ykQutJxC21t|5FM4B`2ftcs`UJ|pgwpp>k^q_ND;s+? z)`hMUG1fU16eO9%qWNGk~lQ!y*f6C z5%X7@0UUfl=9$L8x)dM~GXj6?Rmgn(eW-^mV35l##(S5`#s?R$6b0MpYja zBbtr~1eqQG09qCDpa>D3DJ|RT2|46uvdS#e8e?bmFMra2b5Vo&_L@O)cv7P>0HBTH zHzGL)oJDQFg<68NpB`yNN`~%(ma0AXiBJCk zw2@su3oj2M5BIP2IsRrYemuX|{WgB7Ob}9f(mhh!dK3t!XjH)+b5!247M;Lg^IqB# z<0HpOa*6r#_=Te>Xmb#fAd}X=D|o+2cGBr(AStvTQV$^~t$yk8c9L!wSxAyPSLRRR zCx={Eapqbf#4Bo&E1UIC8=QHgtMtDogW%838pCpB$-?qaS>yJs-Q~*IQd9y=vI+ycJiETEu|@2cW#;a?N>CB zZ)B!jWk&E^1t#GxU^lddAxQ5qLWj8cy|BwVlTO+#D0w6}P*4g=yAi<^YS~Aav_e}+ zB&8uSwt5d?(zQxyXj)bbrFo2}Y#rSFE95({?J6n%0PP&t9A$Q@Q1iUhkq~-US^>$C z2`7liB7vn}ut;Rf(l-b(gGB@BNd!;C()Oca923XQ5*&yMCwvHlSmd@jGENepat!ns zt|4G2)S!_#6IJdCQi9|OjMR;~900i3Q5;EwRZHX|^zBlJ0!&VSd7~|mv@{?Bqtpr& zv;?FAQV^U#=qjsydep5U8`7=N6o1;Hi+vL4V#*M=_c*c@%0kaLj(bz9buvVcQqrA? zDepjRX#W6NE{4{53Iir_P3;M7rwO=iw}MZWTmU9}kx@sJ3h2}5rAlpL>F@^F2)I-MYnvEPaX==j^ICh`~6%&$4@7}f+7a|*R z2vfswG8Mok2kTyWq$r>=l&L?2XvhIyntJ=@x7u5cAxJ`&5)KeZ5EVbC#VIC+N!jW8 z4cU1Ki3SQ_k%f2v0E*b!yZt?f(&s1Qyw^v#Q)+0RKm}uIIFTRkTZ49w8B;wNk<43JN?5Yqi*Kp6i3HLfp`GPT8&xI)kX zKbPCB3h6U~W@p-%IFb@}iR?`Rq^K=jVEyYpNRERIVE`ohpa2AeGe@@yNlfF3idMpm zY|k_CN3x)#rD6wor(%$th_PZ46En1PL91~jqz_JL(Bf8!_m8bXNc;Z)#Wr!hpd#p4 zm2P?*fC72!dsR*{0`clh5${Ak3oaKnN}!0UgNjhXPDD>oYaEh#X0+dER+kNz4oVb9 z2cGodi_~2<5)&InNa$*(ZID|CSlpsGp)Ku|Y~k8>2^i@~i=HQ61Et3#Z{#`j5|2(K zMD{cV#gbS50Ov_3rY4$)_MmS7f^+qt-B#iQWMX7ZK1oRki?Y#MFc>D`9yT~q_-Lcz2Z=ZP!d4O zk19M@BQJ@&M_(72N}orJDGo8`M%6e#I|N{k)NY@2?Xq{i6R<=Q4mtxLNSe@igTn4w z7gp1IP}x_P)RQOu=CI4Ye!}?OGU9?oy4!FmU(?#8?I&sWKYZ6cr;XH(j9SXA8zXei z)Vc{!Q(+rFHaAZnNcN)b^(8QswvB=$x5&;+ANH(;rTceGp~Sq}&{P5zf(Td#IqMYF zS1&J7wDK53){a4a(&QE4jJqtqB9atgd1Sv*JMj&uA^{-FTT`IOhmDF?0 zkNRuJ>|1RKO6^#@gn4st9e<8+MY#O zW#ZN^8*R(Gl{T=Wx^MyQSxYNmP==ahdEjkzz&$`g|AOsm9o+t zSs@@5{#8f=_o*{sbo0x$xFwY~kW`e3G20l7R-*3y*F>~zxYMal;Dr7iCBhU(w@g)f zfN|z*-$lAjv+6G$$|!LYz>jVtfkd&ze3bJ^{AjyLv2NKdGPgXmq!ksMZajSt`&V!8 z6dSg;X3cjyohg5E(h6h&DhKm-#Z_RbY3qcy3v>q56r;39vH7Y=xLdHaEUX>73TJTO zbKCq=T_s3Qa<(qLaDFXoy?t-3meif&0cev6j`$yH9Zx4?4yhz46e&VU8>iZv+xRx_ z?c~UDp3dD(xM(`%`@2_cEz$ALR2G#ckflX5O?I1fdM>`TO;5?JDam4y!p#fR4TIY zt(CHp9^C4ZLEJhXkxu^rqLe8iK&NN`0g!$Ds5Cn3Y1$M@Okj=$anx>FRNRS~t#gQBgNP|`x4TOSsbegG!l^v_(=6&e6s{-|+=Iqc3P*Dq3 zVrtYjKZ6zeDX`Y!KY9J6vSEJvhXo^VBCfNnJcMnYCw>84Bya}46FKl%vAgD|d zcoj7eh0i?%RJ6;gB7hW1M3S2MC{l^s0Ppcvr2$*km;pjZwRAS5$xx9|M#^%aWw)>u z8Hn`bnvo;QkT^`oYP^84wO){Tnu%}!0LiQq*wk^N)U2gInM!>_rF9nSD&NvLH6))Y zTfqQuYW>M6QBDap5=Az_E`aikC!zVEEz*UD3gHTm?xHSN#DfzPK|Xw@q^JO+B$`{7 zy2nL6G$HXF%WPW}ASFtJ)0**pYWcU6Qnu2itdt=^M|_T_+qHTRi?uR_);EbzFrKhF z*OjF%uJd)e6sFVv0OX_|InUC(pP!Lr%SiQp1}6-5$SEu%DRF2_dIE%z?4we*Wv3Z% zG~(9ba0Sme{qYqio<38C)gf?`yVN%*D+M{{-jC9)Qo~O=vKVv-N)mrK=l+_{6}mM^ z(y&#jc2_Kh(hlJytGB#ySKWAi#^v9}U)!?gqC!Gdwn~yX2f2ZYwWkZOC3c}Gd8L&p zTaHphK|OO{9a+n3#9iDzjmlu4+NB_%I7Ub(;C;;%q_!@b1Y=ve)b*=&UcA!YDgg~! zw(u)fMh8GnJ*f+4UpMM!N*2F>P2Tp@&_YnCFcNTQvG=S!{)u|dy>M(l? z*^{<OwdE<~t8$bM*3@@T zJv$$Y$?JcN){XkK9aXD~TW#*T(uv-}dTuGf0t_D6HKY6;wxMHpX?~^t733s(20@Z{ z0($htXRI_k{{Rg=aNdyBvYWVV0lR>szGetCGBJ<86z^AN*7@x%=9T!1ts>1f=u(oDv=VURfhYT*04u4E9Pr7hCD-@= z0E3q)@XC`-_x+Ew_m;abr&w{jyK)>`fORR2q>Oykx^3^U905P@F!W+jyURUFtSTw@rLPXZ{S=0jzD3cM2@P8d^0n)IXKs{^Nd09O? zO!B@iKUX1*dgaCucc|N3DTkVbBWWP3aDqYj=CF5exRfbKC;Fa=`R1JJ7t2+@kcFQt z8H3)jTK2K{D@G8bwMtnr73E#iZ1s9nhYS*AgSh*eF$p9Y1D~~GY`j3(P$ffeG4I;6 zwtBN}0V)w00CH5G=~aFO?_zYal|`%or=WE(_~dYUCDl%7H3HO5;YL`b4gHl)ryg&JrF zU~e=JupZrMYs`d$+Iw}U4v=RvF*Lp62|K>jWeSwk6C;YL#u9(GnqsttCpALRx2yts z(x)2^EJb;242k_KJ$+{&;tg#s6DTM1bDBYXh#XdDq3dP?QL-5zb?aHXt9^unC*rpH zl0YQKzcX1o@B#O&_$18PP@@Tv8RqafzGHE6=!G3s2H6PozB zm{B?VQpCLiK_?aQxIX^?S{^7RHW;PkbrDuJu_H9SE|`g{+g?Uul_-sjhKNu4dYQRt!W% zEos6;aU&FCN+u%%->nKmEV=}H9`$obFb8UaQe*tlEJ-=wRFa~T31xz=Kp=KB&5J^P zvrw{vL=HxAPi-0o35X(aX?7g}ro_nY(-^KU!cqxOIp(xTw@DjSkZQY@hTlXFtr1jY zpSLJH06mQWsS6|mN401#8I>Ke0+QS|5>90N)s*xMR<#4S)1D%UWhGhbF;t*RvJEn2 zz6`>;b)rZcI*WXA4B*w?P(VGR1knvO2{1Dm?O$9s2{^=ks@p|d6kWLkahdJfkz8D0 zf=3^vGSpm5m_pJq`j|L{H)9m3QKuDRbC7r1_InO~$1uJiWbSth!X;nEeuG`J7plqkBU=yrIREH_pQ5@>5?QzGeW;)l>_;6)Kx}wQ;nWs zcclCra|jfd|}89L1WE{h)nmd1Yw4apJ=4*gL0?XBDK=+X;*ub4#qP#0eR#6{{YP zPcg-DvO%_WanD0rBeRTr)B9Bcfdjs29l9f)gQ<#bZbC;f?G*eGrY|e8%Wz6}MSSx? zKu1nNq1kdoPZU<*K#YUj*IqS8I#*&_t8REdS_HGS&fJ`0p4!{EMDgCT zWoZwEv$moNLC10_qYgm!>LQh<1|-CDSEcL`_pJQJaTu-0jnl?OL02i95%Ejhy8$CJ zC!UlWmw+6UkAqX0d+2lN!?u7&XD?W>z zfJu=>H+eEa`3DtoZZ5%+O;bN@X_u(IwQc5zQWlV;pJF6_^nd0_N`-m1@RwWuW#KI~ zN>M9e2HDB(gS38;UXLJ&uc4`Pgl4`{XMl0^=HUJtLQPKCEehTYSB&`I!vV!D)Fc#? z!h~`w->z9ptxQSlSsiOj7c8kI6C)U{Dzd|i6K@pxBk+HYydw3j+lpk$VC|lxwRr}k zYNqa7OQU#6AbSe^t>b?Pl_AuI`ucI+zcKjd!w%ZKAB zAUSU!>f;;+PB-ERcqi2uM8=Iu6yvo$jegPTj_0c%W|7 z@(NUtNKnFuLOrVtTb`NG`&N&q)|I$d1mqFYtxzc`+DRuL6scN`!$c)%QnM0MGe$S~ zmBC8FL~x|dHc)A76)PFGvAros2M|p2S0qT?Ilv_Jp&P(Ra|-JnYT&7qrzUcHdsaq= zCSKUy2buvRtW*}Yv_MM*X(|#zxc9Dtn3*8NeAJy%)Y88X1;S!awFk&SsXLM<(5wr*KMA2qWH;U-)=kIG0(t4J|+)d8l}KxmevVL190Q*S{3QPh)VqB0#$+Nlz=f>8dFwgSqQkD?L{LHnANO9VQKWFBiy<9xa}C$iVNE zP{E?M-jJ_JCm$5er&6Sq46Bn%!U2U&_fBK{z@tWzvd_SSioH_phQMT#a zHsqhBQnm_Aq|eY)(x6h32#!qEg?rKH9zI1R3_J zxR@ho>rf_Ck@r2REpcI|VQE&tI{+%>K^~aT2dx2WrxfD6=-N0lM=43^$F)8ZO$w5x zRdC#50es`x9i zxju!rz%bDW^%TSkT=E=CgtQ9KM99bKPan6I!ARV$hJ$1|=O`@-DLkL^K|Fkz*kq(m z%}H2FRGC7A=cNmIVgs%%ovKQB5&r;rrcO47^3;2Z3x4$w2dKu}(&Um2?2WM8*vsX!@6-ZEnxcBeM_1?y zMbT{7X`skbU2#X$PEsSF?N8p)*K~xmh8DFd2cw4*8^spTj9*S9(RF}YiadfksHKz?#}5>GNIo}K(IAT3vIGKabp zo!;|JkB5>{)J4BH9Amj2W1`!t^~Hw5l$9%Li5O22UX5j;S=(DB7Yp4ZYJ?2dvrq6v zlSwHlK4b$3$JEnnZw*>DN>gv<4d32oaa-ZwWUh{WPd^)aXPjTA-pM;{nJxsKvJ>rt z@rv$`rs@}9sI(BH5}~*av0kd54_h{(TS8WVpU8Oliam=)n1Y@JYIZa=$DjlAWaTdXZk6<)ucflylz|aU z-DW`{JCa5S92y4Q1d@f1D1~8DO_O$z5vG|poJ*<%PdSb&rAYxOT5@0}eml`iWwej! z@HvjPLfK1&S;BHL*S!riT4=0=A;cvLGM*0;?O!xvd0h174M;YmqiU7t;%H?lO)Md- zZXbFU>tUsR1ldbs8K*T?cS|JQWYSkPZ3ub)dMLy zEPW+N1tfv&I#8|KA4-;hqB1j0m}w{p{M>aFYMKx|BDE}Bwq|*F=&6Q)1Ccdb&6O=m z>Dr5I07zB^LFVTvKPI$1YJ{n~7CmkjPm?+P(Od~BN!)OV{b;vWCCJ~C|tHkeR@v0RLFtBy<@`^8Y%-Un3y6GRjQ5GNBH)`%?%mt}Vi`Dq(LGn%Nm zBooke>r-*qOcVB@Hx;P<)Dm`FQDIPVFp=|9N)iO9e2Lu9n)0 z*u@szAZv$Z;*}*yOu!?(CD$&SvS|5)k}#piIHxb%p$vJ80N@fPyxYc_;0(Ft9m_BZ zjz`wHzE)L>8+Ws(;^KK`l^A~!>gamm#kA_&Do{&k2?YE6Qa2b}?p4B40Ng}+hsXWv zRlfU=U8%6zHi00=C!q$bbvL1czrz%?ljWBtAoCsiitPfh1w(TBKQBhId$&($cS*l*?`UA;T{oT6@$x2D}dl(fXX35KN0>a?Fy8JT<2+t3LS{gYSn5lTWgvVuO{Kg zZJJ3X%Tjkiz#Dm1X0rM(gLK1vX>`i&Lu(3CE!-@mp>vr`aA+k`)7WraQ+*Fxu}f?v zi-#Y(m6sBwEGGuh@7ZN+gbpU+aYo0DI@a`Dq~z;zCg<>kMuQG%0ozLl_-Z@8)9;O zqMRnx3DeN!y2`;;*FhYRXX+^>){&?c=*!a`wP@-kOL<(awt&aMH0ImSVU=3|zcIzb zX-5WqtE0um@sj@l?#^YYD@?x1)y->AYTO890H+ia8&lS|Zx-z>!)=YILPAmpNUHRm zIlUsr6aaN#DQEmr9}#NaLo3_R8O?i7r+A;G#mawk@BaV?k@Nho)n!qK+PeP$$&vB5 zhy}Aw9Pt(5THd5?9#JGJPZg>0Zn~?d6coQmpL1SucIa_SZ~{-aO2Osl^=C_?qv7E6 zp>MHubb3{d4Qj?F5F;?Kl|AWX}yP(V`QQC?mGfsjZ7iD}mdCypud z!vidGNc{t(c%2Re%7K&A*3QFJL&%xtzcXpNyY`C)Nl#H;r{QgRS13#@&wtd`9Jp&} z*NZjjp0tI6oOSCwS2w0X$Oqn+**;!t5@b&`H4{Bzxw1_&YG`1RoWU5VIJf|QvqJA6 zXQwk&y5WqR`&3#ZR(#{p?g{?@riN0)fKRm5Zm7>S0XgkO%&sJfnIoR`GsJ-)=e=C1 z0Fj>5Glw3o0ixw-oIsyhk?l#ZNzYJmP4093(eFramB9qWQl7&`XH}n20P$HH=_Jfn zt5G9q0#A80n!87<2{oq+rY9n?BGWVeYlvWyBR@1!+4a#(G8B7zS4Wj26OO@AZb*s5 zcCH&;xQVAM+JlHB)yC5i5(m~C?v?MP|WQB9G|r{FlZe%@&t{>ie}uIGba>FY>lEL z2P1^uxo}ckd8DdEE z_4^9FZS7DWwkjf02GQ?VV9;U^5Q#lHcc8Ca!T9VVrl*M#I0Bd4DnS#|y;=pj5b6N9 zR66@ob7-d&N&KUwKXsC_3V;N~Nt-RvAtpQ1E`?Qz*sz5O1djCX#@A^a{87wl@IWAa zx>GxQ00`Qgfi#IbG(k~ig|q=8J?ZVb0sx60wGVG@Q#coV)1Fc8Q5JZFTN>HT|I5Qn6jmiRf zj(zHrvNVuVf=oyfa7|FFR?b3bM;}sVObF;}h`-u>Mn_s=up2=}%mQ%}#%rzHr7%dx zCZyZxUnvLcM6_X2kRk;WYzhdrXaV`NIH#7a0tNsOVkt8V0Yfo8O*LZR0V5p`Sgi9M z5vxaPNQq84tJ3&R2?wreVYx~7`qY0Hx*>aWiWM}5aIxxcV>_VLg76gNsMdU45PvZ} zGHT>psNkwIPFaM!L#135K~efuN>Ii*;&`9~+o1Al#XsrBWc8qvveKgUERIfbP;ugr zVnFHB6e6Ak%;a|C^{QTYo_7J>m)K-(FLv$)eI!zs^&koR*DoK&BR%UmcXU#z3Qt}| zQ}&jJsMHjr0FLIGT0Bm880%RJS0zM(6){>XM>c>}h#1W!$TW>zvdBm?Ip&%);#2u@ zI}=&kTaX8+fuEXW-Qr0CCmF>QBra%tmw@z`o@R+|^)t!lGHW;ZzywbRB+%>BLSsB} zT}*i>#W-2BP3mX!W8S835_-rLh8oV?k_XnTMzw%Vzx}vs}B7Oquwa8CL-C zd-7{MHBzun;zSO$(|x&K@y%zJYaX!B^tcEdNy*I)?$yjh#LX=ipaTYfN{g4{BxE0X zpmVVJ5xi|686A1=S*zATD&2@Y(@U$sakxb&dEN6QdqpN&W1^^3`w#%vKq$YWQIb0! z@lXw^9GRRG5suWsM3oU7#8=)?T>3~8G5-KGKAK~rCV&Y&4s%~2=*@sEez(68NJmO~_&F~)hUlZVYpOKVIHNmYKt z{vm%4+qr4|%>^XLf-CaJh&(p;g*8R1%L-w8lL}7c!4V(xUOPXHFCJ9SXXE)1&l_Ec z6R{xq$xh+k(FJBk4^x^oq=SG=jDiIM(uqPGY|2kP zK%$!@08*dCGL}PG;db%2Vl%eK>PqrRl5Tm^q2e_qLa|%EzQ>%M+ zVMp@UU)A*eJd{5Al`|(VN>`w$eo;Iatsar7x}>5W--Ekqun&KVxV|i@N0K#;TklfbQ-CG4U<{1> z`_g-~BOCV=q@BGEX|0{&9%)EXPnkIh;MYZiXi4*2B}-C@O3ab$D&-qmDl+5QoNZ?1 zA+&`S+*aollLY%#%fpJbu&`+ll(wLyL~^0;NUkhVV7AS>!Agl=rXxJmZmyMOWaF(R zPdt#Fi6`@XR&G2Uvqke%k4;Xz!ZN}X;<7?gf3y$O*0V^}8!Wc8h$=t_cO$GC@NW!X zY8THb+m@8mX+{dLIjyT+AZdgp4iN<8pqjjSntX+1z?$NFJ*K3!1o>U#G}#3!1dXax zPZj4{ABk<8Lugy2Wkisft$nTH3QCZ$7C?c_*L%nERF>Jn@;rAJ(AAQrmFg-|RBEI;P9LdiATvZrpDkw0%$8c zu&@wtp@Sxk1OhWS{{S_uNlh7CTp3AEsE!EY1zEjd03xJRWR1SlRlEvv-e;v#Rnd2S zgf_jxNrPQ-RT~asfk`QnI?)WgQvqi`>WU>lhEs>iBjf@^2vO#SxJ!wW-U#6K_oit8 z%82-=Is(dZ0CcF}n$b#B#ny>aZW&Q7EIanG?lOP@UV-GLty(Lx&tmB5*jG z1?&y`g9r)`2S@cF3B9pna(Ua%%`CUHobMu@+VOQuv@lLT|u6u#xK-ogel6ne+CJx!yH z+N;fQEKG$)X+p-;A27;i?OA0Sdo@Byg1e=n?UAraBl6ENUTNY@CFUI5;~^~xNL0Zd z@m{XVOE0C;nM^>AqvDZxt>#^2Nk-KvJe8P(9cw3*hBC*?rfTtXN*Orzk22BQ^3YWB zOcQ{i)3s{Rt^N+t3vxWA6M}LDWt+HeLd~0 ziu6wo_&5eq>%)PRfl?Tyo_a4PXkdoSev9Edi`UTWZIankV8Gko?_Ptae-GF!@47(s zfOY{999F+f)9r1WL1}=M0mNh)Ib?-^2ckGN-NE$eMO^l9bNxbdbqxq@d@p5cprFzV ziT>!!Q@eX+Qk*0JFaWvhLz(O;VANj41Oo1;P^8B?G2SK^Lm>fJBlz)-@#u zOxWYeS_@G|r-mgM-rWCEfed?tYjoF;eX^xPz?5r@PtP~*3&P_34g#^m++Imn< zUbcjHBY_n{wRar+iTR=RaY5~u>13Zsa)`(z8sgRmr;sTtH!iJN8xOIXX2>cstk$eq zmQS@A@?v=2%R!zw9Ml|K0#(pOb!G-Qp$mpc2Xy|l>CGW=jgecW$=wipb*kC~uL2

IeKm`cnnolH{aU@0DGDubQ2;4ZxO`Jh zXIxdn{IsZTQ8S*EnReM%ZWO6OdqC#ApOKnb<)hX3*j5#=7KP&4FYb}~KOlkHjb=3$OtDO@CW#Cz6y-Twe5SKff{mb5~Zx&RbrwV~>*3t@mu zL8ZdX^Cq2bEg9Omdx9FhwbTJcz0)@df)pAur3vOMF{ya3LDTMTEp7wPsU(FZDVQK} zG{(b9(|kMy#+uUPZcD}22vewz0UT%VOZ86$+4zXtT(rL~{m_uTzMaRZB9P@T^BkiU z{Qx{4qG@`*p=hf;QReO+K)7j@++%LyHmrk#={}R~TTOdH)AgmI>sqoeKjrNTNhvTe zBo097NGxr%Zw%@_MV@Zh^t#~@3+pv&-Yc-y6ssp3b&H#WszKR?rzJAw3Rx`iuEkwWczj$g3G|V zyE>~bCsJ@Q6(wOrD2~%!seV|}(wnt0_=P1PIG})~5dx*AukZ`guIzPZZc>KH2`#As z&av%UdnWFeZ+QKiCC|YytBZw`v^Ji4^&l7?)tT_tr*Wt-x7TW>k-N~9;y~jSv1wPNjHmoKl8ooaT_vRX+zpLwR| zf1BekddS*l!pRGZ}f<0Y$tnyTbpf&oA zIW^Evusmj_Tf{3F#Xz8rOrChGle9i4g8e2W$6U~EswM{%V}SB{@j<$RpnK4AVeuDS zFVcHbYsBu6ocq(K(GperVvy=s%d!wY|52 zIK@C-A8}PEHd@G#=pZg+@myN~iN_QR7jrQ)6i~VbWc2E34lNE+OdaNedPXQszx_=X z>)A9vn3^SM7SGTrw?FCWRtBdcJ*be8#ZCa{r)u9GjBT)D#NduN<=3d3g_f>Yk;*)^`YF|IOr*ZiQ-N>()SX3 zf$v#dtkRJ|$%&uRs98*s2^BX_CLqr=8p?>zZuzL08ah=cA^`WRTUf?QxM#GKGeaXzy~!RLevIKKuFAy^s5#5XNn0f zkdgXSgQSQ!sp_Km9C)g8k}xP~QjSk2nzh5~k}9-=#6r0fOL;b0fK9oBnWhPYlffCK z?PUA)qnR?CoKI@Ckd7o&$Qe>(o|HR`r8uAYrXE=$c>@NZq;;92eNBHYO zy4F5;?@rX$q=5vJG&0x&F_W6ULZu56qQOdB#!n_F=B$DMoX1n%i%I|j2$NO_IV0`X zqnRTFjyB49#8KAl&e5E}suS1!=4iGfbVe$yWd*X_k4WkMYSlC50zYcD*hm1u5dw8Hqiqt3qVV#o#Fuic00dZ9piDNImKPpn>WDV}N?r zPVuz@c};G|1tShwxQwl}BO@RI?@sM+gOLZWX0RgQx7I*8o@uR?paP?EbBuFCn;#Wq z!2Qk643 zfT0;4VUjuGj6-;nh@217mFc!QNfsgX05|=8sHWZo;PKDB0#Okd1XNJ9048|vL%5fw zhi$B9GDUpziin9FC>HJ~9s1Ok=qWq1N6yeZOBCskQIXV+Dz{uoNW}K{s-0LIst?Un zr%uzEJmQCud_iuMN(nQG;PFWvXsu+N)2G%5kr9*nQrBP{m=qJ4Y6Wci4?KO1Fly&u z_V=ax!bJ4u+5E%up4~nvVAR1(5)Dx0?&$?noPcX`dK1JY+vO#3 zxRLtRt!fARlu7B*kU4R=MoA)Z1BxcqN>LM^uN0X{kBkwbZtSWFJbtuNE{GBg4z#s~ z!2pbrj@4s;gy*;ILb-NEkihCV^(&qzH+3R$F$WRJr8h5@=WP8?G!eg(f(h?6ZzDkP zLzUjCN+)?8t5Sc-*` z#AN5Z)9YlUr65N*is)lpjF~4ujOn0&cE@52Ro}`}+B@{83^R$2PiibO0!|4(6}mVr z7-gBJWNz$BX(g#j(q$@7MGg<`U#0vdtPO8L)9*pc%%y5iWpJVQ2D}dWxJeoJHNO51 zYUJBr=}s#)?)lBn`W)x%pnmng!Tugs56|~T4?TLAl>Y!9@_Q>zQ6nNjrR=JTZE;;$ zBaauN)%ueP%u?ICM7Wrb(wR*0QBovO#qkPDk>;Kt@X8-b)%yPG(JqK1rDL_v4x#ib&=oz!7F;m$BTulf=bw)&NAoAdz8kyn)#8(I-sXy- zD+$GC7PPHq2wa?zU${SrU&9tP792{M3J`e}`8UNr2fXm5>xC^TcFLT`<*zB^c#^>` zw0iFw%7>92_F|uPwpS!>M{&gVuC;X2sBpHTnD-g>s~-W>q$ww8GXP?PZq<@b#FWXI zo;|BFQEO()X@xEDl$8YTQR8<(Q5vg1hiwMeb-;N_apJSJXo0mmV+0w`S*|I!0tGwt1HpmeVQjQG|-8M|bf>Nkd6bXSvK9FPSRz-pKte}G$ktH<&v?nvZ*FI)^1M>(umgxNhv9uz$TR<`&)dZEN9@%eiDREyh?Hi#y^ zM3cwRQ(qP-S}G5zw2>sFVu)p1hDqJhYDrI z04gQI+JHGVcbF8AObTZt^u^O#1_|cO5R8bO=ilkvtBK9eD(Q3PSX)sUmDo2Qq!(kZQ{b+M1_o+-{0CL z03@PF&d@*2dH(>3KM<7|X=RcikPZ!V{Iu3Ok)J)jy3XIxFsr;0DgI)7j{xoK->mT zNELI12?K*1MRC+Ov>5?Pth1DjQHouk3*zcXNZXV9RSqp^SW-zKi8D}8DNx&kH3y$T z2YmL$W}Mw^kBk?>Ejo}A1|nqp()aGsaG&ZVr~rCYn<-M10$?67gHu(C++?LACS=Fj zvr1VqBSd2eF+v~Q;X@=OaRQFEwpv80e%Up3uZGtl#Vp9oMQCjF>_;GRG`v10S-X;= z^7%O6w7@LV<7QPP9^#zalM*CR0@W~a`qijqw1`oXYrBWXlr(Yj=ZwiV6s6?NSVB-c z4zxSBA(Z-gpcSbrbSk#;j5{(Y%6$wRa~R^5yLFP0Q}YX;kV-`e>Q)>;MpCZ5jc3W1 z@IM|s?5w3qlq)3H%v!45W7AiSsagKz4l2#^m4Bbmef)*uC-gKB;pAD zt30_slSFA0Ha`%p!g+!YD3)zxLO~{IyDK8vN{AUVMSzy6xcp}KAv5wTQ7gA$ET9AS zG*MDeWDx*nsZjufIiQsK_np5Pt?9=Sq+@C1dkeXBAcB(r|AAN-@&h~wJ2 zpXrjMo+uofb`pw6vv-yb-lm}7+#_y#&2xBONZTE1zdL9s=Qt*#l0~StaRdVqH590} z;Zk6ytx{-3NVT19HjNRuHj&Aml$qs>S1AsLDM|4`8lJ9;WNqDmCPresuf$#^Q?5E1P>APm zH6Iapo6K;w%b6fTxFl8>t=DeIm8dB4L;!2e`CeSJN~6)}{7(`%mnn7<>)(r&DJObI z5rSyTMYyKOAuXgQ(>#+>U7GDsg(xVLoy37TsHq2FQpf=11B%ZW(W+9kSeII8{{Zfi zl`M=1n&@G@=I$2RY(i2Dg<^Q1t=e7V%SxG0Evf>Pug&pJ(#cJ`ib{8{kccRCR3*$5L0Dd1#RT z0QUhCJ>*l*2>5SLvT-hkgJ)^}pcJPmH$y$KR*ol3PPMt6NeXjgm8?C?8S98{p>6-f*z5qLLW&MEN_iV}HAXR3X|zLfQV;dO02944j^m{;XTo=S zyWVZ?uX5umJA+9(csv-b6}FV83usrRq=*TQeZ^`mH0E^n2b8VO5(jdlOoTc2(23?z z`+FDJ*_(D*Xc4-00lG*4SEP7f!p0l8y&uv^+=QR*_chN7_&ufzclwJV4}N5znF4E5 zeW^EU*#ZCqnZf?Aa$hM!GC!;Kq{%+JLanf z1jK_&wL7HRSv>9}Cll5v<)l&Lo;u z6)PoYA6{xBN^wOnoSx#H38fT_l8ckGJ}t>20o@p>qR{S*ze%kUG-o&>i(^MJF)`D< zNo9&6#Mqk4O;e;wbHrB9MY2;R1dft@tLrT&ovCT1DL~*PNv&mD( zE6FMJhsPR^V=X%1wt>Woa@D~pNdEw)lcL?LUrL@1Dlz4WpRHOh6s$6pN&C`VCGtHq zIrH-nM3^(dsdufyxKU?K3nbrI&M5ye-V7i z8(s@Y$lj21LHc&SLiI~-Ex{ZfD?O;&01tY7{L#^ii$JrkIJDF zP2kc4m7ag7vjttb_oPn(4Dg?N)#}Y{TEunCjw>|tXK~xdV^S}4Mzju<0YB!3ec(93 z=~pIf85?ZGq8QA-{{21RUG(TUE+Y+e}X zKD1cB;7FR!wxuOlJk{IOB1oW?B*TtIhd+!Y#Lp(J4oC-gKGcmaoy5fwA21@8@css@ zr|NR{2+a4chXKq-T+-n5oR6uiP~pi3HJvg1MhIWn(Fr*KL}oqB0G0Z`x(~fvhdN{* zY8f)rLR1Gc?~avPhB9htCNV!+;)JK>u}#^cIskhhkpQ2y2DHiIaf3}9Ng#p8N*uF@ zJWuIa8g^++Sf)1&j&N#|XP!RP*fdEP#dpan0D&YN(-JAMN-Vd5iIbmtHJdV$2u^!b zCT+1Q826)-Y!6R@Car^Fl-m#n1lPraCz1Zt>+rA#Lp4LK6qpguHJ(;JG%g6h{{UCD zOK$9pRfK^*wLvaHIM44@64@^xqNH7mB`|*ZHx|)Ojxy(1P=2w zrp6>MMY#%)4@xDriGkXhBF03VQ3A}QrxEfvrOH$?F$7@;6OVdg%4d*guUHkt+k}yU z9WmC6LNE--;5`9FSb3#C$1n-`in(HKxC2eNsYSv$%vEoqRk}GyisYIyFEq{ z2*Bc>Sn6P?B;rR?Sc_Dv2QYGJ&9PE@kDBCG&a7pl(QK}eq{5C*G|>;F@IeQ;tOd59 zTRfkNZq~Ap04EXGr8+558#N1f7?UMEXl1w#0sD2Niq-;^0bmo)y3}A((oWoF`Cr~*_pRz$bCsjp7=ehA@yG!i2T3f(IabQ=3ajPDj`4K^tQpkj1!4L}RSbS9c(8 zoQ}h_P3MDPf?xn?4ySmOgDV2I#wnvDrA!x4zKMb$PG*yhLRLaZfP2#2Pf#SRB|;BT zNxvRYtdeuYid6A(=s4t~b8V;us7d4u6He`QWP_CqjsfDlsB3CZsH}s;OW^@+)>AST2@zVG4M{3YOm&&B&zgUU{XlNXBZ}YY9w1Ag0znhNHM%Q@ zW+^^rvs+z)k?NBj=-Gb(ndl1 ziUs6wnwt0t;8TNx`9EpfER=$I>qVsIvHk|}0z5_Gy*E+>ZhevF0nx#Q54bh0Nv}}q zO4m&Ciqogs2}-!DQlUAal4qKjF-mB1VGCp^VgaCTEo4H+>qa5St4IqO$e>bj4aG>r zYZ?MBP@oVqUNhr=;mbGexEF4fVDbfilX~B5k&)Vv>N-<*JEbE&)aId@H6d}7@Jix; zHT--0Ful+ob^9h%w(?3N2E3rUl$AE?N^QX)q~O=|-^D%yV)>T=WGZKoHTie=m;4Rf zwcJ=XP_%+%3gG!Z3&!p}9+$=XgmSJk?EKo%K2nptNkoEEI3}jcNf;&-%^j;~m%2NO zX7!SpKbkQV7$r(12uR@99!ie7**EPiQf=0k5pboqxxf%XZBjLTID#A-fWRl3NV~@w zK=MXal9NQ)-#qCFDB1y%VzbIWBb1Q+3Q<0vRP>%`q_U+*R5k~0XeL%tP@x#>Oa;PR z0XPa!f@d@oGQ%wpwA=-$MacAtG6HcL zRJ5i_*OZEsiD9d=YVp;$rhLMaDp}r3W;j$t4{H=O)SDxkemTL{MVsrTKl?{l1pGVmm(6Fn6Dvo zOS69Y#c1hui!!8yq18!F1n1_r+IkUc%MDs66{KYMpmS;Kp!lgH(imH8wif6E(nytk zKJ=~2Hs!@IoU19_h$r`;w70(!v?0}zut^oid`o9-a&3jeRHUi^AKHEC_L|s*7M_LI zmI^lsZ_6RYW=MhYMp)@))Bv)C?m0}%9+dTlMe5SEpr&F-#ESa<#U)4<=sQs)DsxMC z*I}%$pa!lVwh)DRV5$k@;*7J_HYvn7@FQ#(=xGkJZW~HkXdT1|0aHThthlC^=PVZv zNS}$c9B|;?KvF9>Xn^cV$Kira{rL#U^cPQ310Rde*dv7W&+RMp9x2}wNBvX_%6B6{bF7&Z%KEv7Mn zLjblG3Jy;qsW!U`l)4UEDcUy-P>`f3@^gVq8g&k%Xv|e^kb?jdG|F9|6QCtSgC=LC z3ad0B{{S&aPT&S(G;nAGzH`h~Z|`sy2P2U}_7ZJ913p5OF_YGk>bC8=(}{2ZHssTX z)wvn;kSRW;app7<2eJ69dE}H`n{X!Ns7<5kN*AeGl7PVW$u+&Vx-29Nl*bWB^rRAm zf;~X>98;9rDl)KVJWzPJ{W#kB3(223Q!Y!p+)|D)8LAYN!Q8A&oYmnZq=cl;dZ{)` zKb=4c;wx0G^kt1M$a$eWA4nX<3bj3PyU75j{{X?J$+Ktz2nw1hl(y0WQjk5U9w|#r z=r2|=+EyTH#^pvFKuPALttr1a4$x3SQ> z-+)LxDu>=!8>T(-n|7N<=#x}B@K2>9sEVmqDv}g-f%?-I z3}C)<3l|AV^q-|d;ZmH((9i+TRx{RUq=_BL?N+imXr~S(3VfpnH5SNlPG+WAf}*1- zsyBeYPXO^sZKtuRD$rO|r*8td;Q(ZSA|k6Uq$6=4_n@4-OR^y2gW9B~k|bc*VO|9E zpdPpgbODfRW=dsPJkUzHw`~PaC?2s{gcxj=IUq5lB702DM#5fNN$nWvw#wTs7w9ixKTZYVnFQ5+9p zN3msaT-@;i0CW)|jj>C1!fq9Xrzt(?+l@lWroc)>qzQvkFi}OyZMsMf;_x=!#G@o+ z#S3zk3r{gy0^$-rYv?b))manNlB$-jCw@0RqAcs zAsUidO}{&ma}^&DBJ;$FQ#u9Nwo^(w#zIy;XpKqNv_B0ude}wf!NSPnp45xO{wIF* z=37;X9&*T1l9&ler8y;0Y&90{Ng2lMFKjN-!}{gBZxghD+2>k0_Nt)f*vX;v$MY=X70;yJ)qh7UiWxcH) z=O9+kM)0-Hpsy`}S+}2B%;(~tgADS^j1{(I@-oH|9^uz3sA=T&t%47PT*2=?ns!wF>Dbc)F+!=-G->AnlQ z)t&P6%SGHOV7Lt>1`)Vwn}#GXbJoD z`c_P%C2Nwk9!ZjZwXMet9Y@^E>E)l@q#iW{GS=N9$(=orFt`eE{{Xctc}_mP&p2FH zbs4L+5&$Ip17CSWk4PpGXeBnr#t8!XNfGHk9VnYrsSbi7Cm>Wxd4pFph>$wcn;MWq ziz`4$;y(3W(+~v5>qP|vgp78WuZjUsF(17tj70?i5g48#thZ2Y)GuOJw8=`ur2LMx zBH5gb_Ni=v)ce&IjW&_4=r-3+Y8ToQ3cE_ut7|m29IKdiP{vFdC(d=9aD6@6Y?~-^COW0JzXl$I^s) z*()3g<0Fb>w2&l@lTZs;({4w5Wl`MHMDhw0T$I#FVWBD>xzus5z&lKoFn!t?mAl)y7l-R7hAE z{{Wg$y9G48s{p6HZN(&!k1Sk_L)t+k`hMb;UFmypN9kUks|2Vf0F&)XH4P{$fPS6p zWM-L$P-yc9ZFY2utT(*HYwol~uls*W6&fmnpeOsWiaSEg-;jBT0;}@X)@HSC=|lxW zb3%VDKj|N88$qi*>9qqiS|=HU@k<(bNY6Y`?K%{Z$o~Kp&5AmKjdSf+B>II6mG3}1cScW#4n6r_QQQ*z`S0A`q6I82H7rEeKmKEi1-Ln6VJO6s(YO$iN1* zk2Dg9iKN$!Ou&=H8j3)+*_d$$dZbTZfks*TPq0U0ggzeN>sv8 zY^L7BgSRn7{{SyB`C@uSI<~YM0fJ8gjSK;S$7rhL+76O4t?A_oB}46+EvsFJG6(HV zR=@=MWA>_wBl8cfXOd&0(H77Kc^!>P5(FxsR8kLLiimLJbor&Ywud5OQy3u60=fhz z_pVP7F_|REsVPTkoEVB%0IVA5z$BT?CA)BfbMZ9d;UrJBB)kV{^vNT=3qd9$ymVwq zJqZ}44yXedp0vjKX_5&fbHq|d-Q27ZCpjMUP)#sq(=0AQ$4ZLa+z20eq1pv%1W1#g znl(2607V`QR_Nqu808CMdxp1>xCr~u>C?$SmL&GgJXN&vGJ4gv*2z+V0y96QXUUni zJ{VBC-%2XR(e)I`I%!M*1on?=&{@zf{CYZ>pN26FTcu1eMK5eN;1bh7}HKq-rGMs^m(lFsd zBkm}94N)xeByeWBrUFDHfiqRA?EoC(y$X=dG)YXQ2m}GzJ?VAaLD||q zwR@L=L!tW8%gde9>Hznnu1E`^UFx?JxBySJBXt{rBiw94&d_MV29W=S#()=9;t zIx?`E7L&LG7%~rfcW&AhzzX*4X)6oz@GuW*G+*oka*tU&3S=8yfg$1=N`WRPt~jC_ zw@5tagA|pGND@6+i9Yn!+UTf=gB>R{$Ficr+q4IS{l#9i1SF25IO3+#{qx?wb_mR> zg(@xVL8v%V3{M1_#OoKOG7znx4l8M^AdsOmw8@kDSDx!BR`a_#`utXyNJQdhpSUi# zqM}cE>p;Bh6Cb4(`e8?sV+KD;0rVL6lLXYoD&H9nLR$@xSW}P12qv|C|K{M}KyJytmKq~qkrm)@~M(~_TnaSx}+h!r4M4xKp zIMLHnv{vxMh|e?9f^g&lU;;U&FZvQl_a)FT&MS2^IrDFp$ z3)b_}pBbj9ig0mukIQ~O{tnsd%{o%R3Q#|l6Po<3<3Hg>t>Nb!{3~TliNPH`tNLqu zq@m=QOwYApJV)SLZAG^b>tRz_JdXz+ZO5~r<9$OW9ZP8bV|gwmMWv-pEDVAxt7JFN z6Ek0|Jb(NKW&Q5iGD7lYAV{yz9wYEZt>O0wah14$Ny2g~&F1lP;dIgIuz6UsuWK`8 z6}F{81c*wgi!-%_Z+Hk0Bdu1oBk=xO(yjZ6Cy1p>OU)gjCv5ku?k20FMAhBHWdIOI zOw5X4jX?~kE%UKz5i$E#QI{T4!eK!|k5#$3C=W;@gFmGq#+b;++FM_=@Em8UA*ieYbUr>*mFn`9}~ zE>yj_n59W+DQKuBRz0}yR@yak$WqI`g?H(WDsgK}u2-{NWqzlOr9`9{6Ug?cYr5j} z8zo$}pE68AAC{8;0612iYYZVPaGZ*Z`qOubCDerlM&zhef zSyI3N4Cc1yLj|NffE!-WVDzNAZH=&69k#b@IA$ia&+AO-=|c4-B?Q*--4udgQ+duf^e?ki z`e7#}_aZCRQ*6;~7#vS(Cd0(2-<9q$5jp0U-e~1cA1q2q6DEOkM}E;rT1E-P^rXrX zyBwt!wr?$bP_XJ@dr~@{^{&$VNWI`GVL*w&>0UO|%SbYJNa%Sq=T6j;f`qJ;xg92= zl;HFPu|jL3-YxZZ(v*^@Bw!QPnX`Nz2_Tr5uNJr0ly)|ikat#|%fw-Uw7IyC&DOeD zeEBC*=;dbc6X^8jREHHDRGn!^bS#-oVf613m)t-?wt{+ytx`1*;vlFTb6uQ1UOcGe zW$|)jkjXb9K}B@Ki}esYP$_*FgVXh*P@WZ&@mnJOQvR8_UK9ywsRZerl^baD}A71aVk$Cn?v#yBkUu@-#)VSV~rqBduB$ztu1*(wT22 zF;v7L16tH*$lb7@p|Em3Dwh_m;xeD!qNPe;7!|}UtnCwyBvCdBUm+_{2@#KJsI6q} zQ0Y-^1i9)~D#er~B|KGZnnh}Q5@#ZWbtH+H2CYa!)=A@tt4f<5l^Inn>|*wd(51q93MD>y%DB&3$fT*>HZs!^>WPA>M&7gC;n^}~3OHiOT-0{Kuek_~mX zWT$m2)Rod1>8iF9=bYvzt#f(AWKjua1OQ?Hs@(F^#amMCvXhU&NJ^Hm5uR$dTOm@e zGZpZ7r22YL?_L#?21pd9<69dQCiD`@Qh-!WD)RVBM$TztZu%ksB+gAm%aR-cCSo`h zOD7at$8TZlcNySP<+?YYEKJqTs08m?zG)ryu*IRt1Sd2ZCZqQ;n6bG!iWoZMkRt+y zb5Uy6cbSf(HH_8#Y?P;K3YDAzSnJ;u+^Rt^Ym=AGj32XSR~LghuXc}0)cjPI(n2EiLRVfYvw zLF@FDH|qy74J%!vk36L;sR9(btXi6u&e8UQH3R;pQtY((6enz~hT^PH4 zYfEkAG5DBRF`n~UyLQWMAxo7ZSxFqlAk;iBr(E4B)ie^|GGKFByEeSrG_+6= zZ(!+ae(LOItdl3UAaB5(LvN z6^(Tl$y={)w3t{?qTHY@FlJz9-m2qudn%<@ zKvvhUYs&mP?K;Gd%&*q8z8bb`Oj_;K@wy4_W`tuya?Qf6UzFg4<0;WxqnbS7s9DRQswvr z{{W^iDbx4X`fDUK5+kfqoma$d+qQNsAa<`e*SuI<-WyRPj%(GzK0kp9f;_yQe>PV) zqf@SU!3}QpjNoRx%USU{?&O6S$mWvjI*YHY?pHshC3@r}jmwGVxtY9~<$5}pJXm61 zxJ|{~jKqbW1f z6qe@ebE$7R6PT-3{=qvzBef*+3zr+#6P`0#lxRbjA*y6^A6k%|vILHmT1ZThgNm`a zQH+fB997FjAiEo2^RprcwHkDlp)OpZ3oNwYLf6VcRKWcY+KkefLrntcB@z_4!Gq9I zH_J-fT6S$oaZAbdLEsbC2eu^FGbFA?sI3{x#VO^;+7ci_a1J?&7$9JgC+$og5I_KX zPH0e&Ou?dyuxN*ufw)9hLL(^yj%sjYJXC;zJ*d${64_Ca#e9;qD5&XOu1FiB0y!hCeG@;Zp`z>!Y=EwQl~G9~ zo((V}QyhCmQl|O@;(Vk=iy^9gM0b zO3}df#dhtm3Ek<>J*dqw{{T5&L*M*>N>BQSVeeYhsL3d%B6~*)F|9|Dc~x5)-Bi9<`FOn#why-l%)t%nvrIP&E>toSO#KyQS7QjkG*H~ z^_3+vIqo8#FsuM`?NO%5V@5d;NcvIeFp&eATGF6LKT04yh@aM#DA88K3iBOBLwv~; zDY%?}Z$n)}ndU`go|x#CsWSvl4n;$3ka9EDuScq<VGYpbM^fQ`q zYQjfO26I%mZ?+E0QfUlSVl1{GKRRGaL@4h2$+j8McJ?9X;udvt$g483J?6Vvk`^F_S$_XKu$; z2R1guQ3U|yKpMZ$GD3m&p%9>$^#fd6;XPy8n4JL7Su%5&9q4tF1|U@mxZGeKOhD;e z^JPXRodl!{(Z@Y70)$mN%$dj?Bc)BujxahCL#<&vXRhwmq2p9VHWG!$zgepR-4F~O zX@BAXB6z8$Q%@_?-h~WEPNtyofH)LIt+JIEGuA1BZ1)%d@f2;njARKIIW&nt zVv&7~zSO2i)2%sauTQ85_UdRx&>G1EK<2u;)S&YRF`5;ip3dG|A=E&R{{U99+J=%B zCI(J;6`*raLC8=QO58)3#Ei+Osc2sl6m&37MtB&e)*3i(ZVn9KR)woHtb$+zifwDB zr)V&6IIF8J(id9UZ4uD*=~uZaNZ?K>%4~vCHwh4EORI1uW9Eg!K`EKLi-n_ibBb8I z^$w;YIj1-51p^s8;>mG_K*4i`i20f)f&2oqBh#7G(qJPZc%s|9LZVfVit}c*HKgTRM?CeWZ0kZ2RIWu=pzC2Q z>6*LcrC`=CR<}>6B#qOWXsuN)AOO7fq}T740?rKeqFBi+hBEoBuHbjfo@h5(J7DLi znF5(#tNNG_Pc*&Dh)kRwJ}X2qyE0EAp zCe8kU;cY4AJJ#ycP)JAtK$x#I)9yCF83b?*K4Sh-1gnl@Vz`SVZ0fveBSom{Kp|hL zn)AJPR{Z(=>0yeI&dVnbgjbrUnzDr`5O9B5{)2z` zx_IaR09=0zbce284r)4h`~~Fmy|5u){&EUc^)>wZV@fZ(CnjbMewzOP@m#sQ@TZ8b z>@EV6rfW^4dax66fOf0@0M1OO=qt11#X@@dIUYp$!*8SZLJeA+(CA)hgdQt2Q{tDG}ZOL_oxRjsF2Q~daztEa}Dg_|@E6)5&{4=oC z5U)1fJCC(96vIlCz4L##>JjJWJ0W{%@SB2wD-8yANl;!}%Rl3o7vt4U?6o8fFj(UEy zW8}V}umGOIkubtpaY^0zNS?Ib*~F#-R7e@X=AyQ?Q>63>tX;LFHiw@P#zj=IIZv0#RuQbV9XSUgf!U2U}#gpB@J6r)nSde9`k=tF0T>q4It)7f;S(^>^z zg&b|@O~_g&1XrMF&b6gjC9D+)1lBgkR=2nbZ7bZl5CP(}O(T0gte&}{j3Xbl7bf1A z`HfY#QH1YTAM;Ir4gA8;1)>NM0<+p3ssp6*I_Iry?Q(!&C)%{aEv=byNhhHq{5;LN zkg+7~0D?I^=?&v4wYV!7Sp6$$ewUo}k4h1oibZhtp7P3FaXY%f6uH5FL74JCotaCE zx6tV+%;rZ=ie;v1uI=9Jl1J!kNZyZadd}vQl_VL+rI*@x(~4BXV1tP6X)hIS$E>lI zhc;T$7-%X2NaHl>-^E^m3X(ZB<<}Q4DMWCRCMbb$>rI6qlRsKtxpWr@%d^{b?-HfP z_boy&err=Xf3A$iaU%B zm1~W9W_jsDvNr+-2;z!P;$c~>vDz}pQw3JWvP6@LxrV&KF^=`~jG(GaVyuO3sOjHs1JlxIuV5vkjyjM4>ridNQgM>5bi}GsRj~ zlmHVz+py^zY`C+N6IK@blYw6kRzU!Yj{L{ws?`^>sR1XYNi8IkyPowzi3COguau=m z6+)=Jgqv}KP6a{RaIcP)P;p}f#Cz3c@^KPPIE0*R|UlCvblaZ3{8 zq0P!vrfHjR0yE52MwoI<7ojQJ6ERgfk?v1g;#5*(@myP>Mib2&YK5fi=wL=UsU?B^ z#%HZp>hk2AkJ77U!c_)pt$quq`79e*JjHyqg`ZAERQo&mFe*0fgc;9l)mu)eDpg_4 zsR;)a!uTS8dKqtR#DG3%dzXgHC{?-ADMHvo&XcsHNaO+xSl4JCDCxQUMBt#;i_B%#E}1aV26vSs_3 zq7cW6?LzRy%J#TbAfC|_$rk9?M3Sh_Tvd#uc}XiU2NHgqRh~)MNEkVj%}C=&xnyh= zn{;3$B&^5YnWn?iJrY37P=OaVuD9eTa#nI;W8Sn*fZIdLlC=!xn;4{pN_!REGR@72 zw=NdU@(dA6(Rpv6ukh-6P_qMlDAcq|!Knw^yI5{PQfH+bs94+HhLYkXVDd!L`L}ur zL2Gt^bq!&s?}}|@M1n!-S)09QUbb;zDh0ByOd8ZC($$3!*+%&9OKq7|=BBotNePj* zMk_SAb!M2TuY#NHBHv!LVRwxWJpN-DMt=yp0?*;^>yurWN-C(Al>7Whiz0B2c~NHrc#!MVMP|?w1AR$Omw4kn_HARf`2NLpRH49w{Cx?KtUvdj@7BO z(;RBt6qPMwk7{Cv$`vWnp)ZMan|AEY(kzhs>HM?))t~V7&DGQ47dl|J%p?I`ho^XZ z#G2L85~hO|$;Rb~pQU=Xk>HyfG$G4|(&K;+Qh>#Dd~c`c^7fT4%Q@ahpM%6zH*fZ4 zBSr9Anu7^YK}3P;T7O|)fundj%SUf<^{kmHP#>VCu3lQ$qiQyY;J*$5#Z4n<-$pB3yX2v&aemDT)M6%`c{dXqFQ#lR+c&w5t<_(3BQ zIvVCTERNaMj3Wi0`-{MmVBllDBD~ZFGAB8p?==!;GGix!OG}0F03`96WMEU37rOG^ z1d}t)F+(`ylhGrSRyZJibMHee&JSKiYEj~6a^z6)2ZIoEOSo|(f=^DAW6Hn>CO#;A zu;36k2rADNK1j5KfsX$G?@TQlB>J!hOwcWWwP%T^R(?^pLt&+P>vWRl^BW`bl*cof z_oZcYf`ZuDuR|dq2|u+{a6lV+bu@U>uH4fuHAJwZr`y~j)o%il66%w=DI#Yn9K>{* zP_^%0bdH@Xn^w(WcORKRVj`%-Bpk+Ot$9+R9crnGKczY`(6-Kq9pD~2*Tm=Jih)>} zS6}WG&C@g2=}l1r%Mzg`BN?kwpmK6O@tUC^h=2rf)|qKYuvNtB?!3*oLWuGaxn+F^ zu%hBuEtDCM0)0oUaZ^ITFsYx4laDmeBHb%)NtLYRiX5ybmnRi`8z(drkOGMuAA0YY z3bQmfo`RfU`_vqKVg>-A^vmWFh4OMq=B9^27YZEyQfL&eL?prK6(2QIJdb0=52jx< zH*MyMrPjid43rE{*0xqwJ_1*RZ!R?p?Ln^qzeq%F? zSA65|K=In3)alq|(+ElalD8+Jp;Xz4^u2 zmg6ApBfSuk79*c?P+D+8qAF1=S90H&(vm$!cintCV za!%x&q|$OeBvDMN?dT^Xj+1QP`0$)&f~z=$gclOFY%8mfrFJEV_# zlCDdb%z@7_NVb^DK-!DtN>`|!bKa@bnk9Rbl9B8qHF03MI5WVWwA)Uwg>Yn!X`GuE zNeyfoAp@Q%(@jYAVmUqPt2-}9kY+m5L|g0t07Q;DRi;WKZpXAwT4c)`P%uB@hiyR$ z=uuAYgEBvA(ORe}AQDW9 z!fAJ8At0;d8q?ZdVIaWAGfsr9iIOx5P*O=;%t4A>c0d3efOG9%F6iy(XB^Ut?M63B zihZf_YJiH_P12AgU=H(H`?aLW3X%_6=A*1c!5@0e-fHGd83U}=R;<;@Br|oyf+HVV z8OOj9AOR!+(vp8OQQ=Uj;F`5Bm(V{kGr&C4@#1Vm@}21k0>u%uy@R?1vLmZ*UvFa-_#xk_BECnhGeZCa$OA`_XXk}wrEf|277d@MzLk;QA9_2 zY%?Uq&l@vNIHA{$Xi;!fpcG*Ap0rKXvw#jg<25TCAx^9wWOWtMhG@}-c-vzgEP1L) z2{o~?ViJ%^=aEaatrba10ZfRkOBP!KNk${}u5XhrZ0UH=;)yhp42&LVpy5hEkU1u5 zB|9btc%j@}sDz}G0~1(b9h;@m8=JEPaqUYPv$)k?4O%v(1ra!{E{)*({eATAZAoO2 z(nrrAyKa>d4tCe9t(oaU zyJ(dIDHO|6RKZ-L?KARUivIwC*4G6~wrB+&$QnQwWFtl>UpthTSxPlX!ja3UR&1cNb9s>j)yC|zL%F9xa{v?`lcMJ zo{t6866!A7a3&j${8i{mU0QB#WhzL(2Z36Rf52L$nItx)Z9PHFW$&!+t*w&VsajPK zJwN8Tc`;56+B%r>LTOVAxViH!OO|_zk3A_xrRJsPh?tlHiE(~Kj58prB}b)p!DUUW z)(k-~X)=p+H99mluaJ;kGNlPJc&3*3-dG9RV;Bd&TE+hWF%7-2jog^ zaU-Pye*sAjTSC^dRCtbQcE6*IX#oo*I|oj+dlzAFq$*NBU!@+06tW0L4}4;ZfwMcH zcv-zB=gxIyP!ByTO#Yx63R7x?ZQ%8)-U-@pGdZPB+P!i@2MW(L4m!|j+qN*$HNPg{ z`9Vr5=@q@v^@X^E!Pu1s=@mXiDa8#72nCdC^g5$SKM&C&8aY~I@rguTZOK>mx zZIFYIdsABi<+`(gIQFeAlSvFDEjy#PS`$%i4bIR4rZH7KNh=o|&AMk<)D`(bmEuKh zJUagI>cYPD=Ju_)+k}Elm`LI)&^$b)Zb%~}MPG{Ca;7WFig?+yY(SXI)vdfFh|hVd z(fL4}(4|{U1I>E~x<{01*i)}T!Oafi#VM7j{{XcwySz=ZcP&CiXEh%bwv><;6`L-2 z&G@?s`yaPZZ#m%6UxUBF1>s}=9iJo;s)=cMuYPDb8T`CettP=W; zX*)*v{WBa^&vJ9c?W7MaeL0$quZsa!^%WS!c*}RUAf+kYGX%vjUZ&`1NisPmm-_7O z-G%xLD^z;w{9wXdg)9V|<^@at09G(_xKA-P`eA|JYQ&#M)*|mB8C3|)vYh`sw>4#tV_~mMBt6p^V;LhCvtE;XkXMa zu$R;EBi){L@K9xrG1dI-8kLo!) zXiEBYI!yJqJXXroQiYF@YANwc2{|Hq=DbV!>xfc_UP$*e5%CkikV?6MnpgOaUdJz| zW24{Cifzhgc!8d13y&Jwwx2_ZDvsj3QJrb$l2F{IuQ{RJe&CWq%7LqYQOwqezMY9Z zv()tu9SyS5fzLzrrK88HaBZE$^d`L7Ch5s8NC$wS6GpRa=|q&0Fe0;lqdebcrQ=|N zsx=q>F>ohx6hQ+yr3c2v;H~N)%pCG575$TrAg3}xz%djX8bhT-ByOGsB2voO`bfa1x4I>}nNa+^b5v=`O_QY}b_`FoHDcbyyvfHx-@!hB zT2$h$XHQR;Af9nrHK~UrstM{yrI!Ja;0+j_Pg-xM7_mz%q;b11FV>JkhX7R8!rW}A zl3-$bSJr4+)TIRyg!|Mrom0!x9Q*(YSV;sPgmmei~dXM^JKz=z^8XEBId;T zN)4D0V!V)R+M~Cn4zvfZB2F`((zYwOa^AqW`u$x*aYRQcrrvyI16#BwGyQy2?P@&mmKY-MavGv`uC4N2Y$9Z zl@bhsIHp=IsI7g6HYH6FfJm>N(Jb#BF1*;>aUfF-KFv0XQ+CJ+1V$>;o0i^-YAQNg zGDl9iP4+I-QWrZX^sR-mP?R=9AiX&w80}Qr_;@vY=eT^uDOmta*0WFW{nnuJP~EbS zINYHTRgVniDvGjX%LuD%#Q2NDE#IvUgcU5~smE$BOwre)v=Ys`k18M~DJ0MQ*R$y# z!{C)6cZ=RX(grJJ#)o5Q0aHzYkTOPVy@Tl4a{Hz2{^t`v)^OrnW9~eE!oCmGbw>lY zEPfHQ{-NqWO7+bz!nU>v3vnSz##f)xh<8!6(~6NkHKx+*|607T;qyX#qee;C3~zb(yhxpQZ-)slvJ#PKQtZ9{@4XT%-1$~ zCzG?O3~++b#rv_${F6%VH2@QX9R$*=ok*MmG@|10l4O~xP#I$@tJ5R`dCqB5_l9ty zuD@Ch>&P+G&`W(vAV4Hy4K+j;BFFAYk|dMGA7tD#6mX-HnoK10kF5PE^9}4_6F41d z_|ifq^iu9g02wDHgK5!kiF8Zj81BWxC|$!&2%_hWqgg)l?!7z6uPKX z$JVM+r1CNP)K=`yWS^~2Ryi|0g=mZfIhO7-v;v|>&2&gV)gc{Xa%g`sVL2hO{*W_W z4kjcfAaZ%F3mBI$w7QgMZaNZc;_|ti^@?sUURXqEK=-5C%kusl>G+^t?&O*1DyV2nzS1p&NnRC#`q-eODgzJWJ$W z93misBaf|gI$}wa5mbLoXVS0_&2Tr_c$%C`<`5quCm5>*#E;s5T3ksH>J!$!OQr;- zB+$HNsf@$KpN=Y9_i#x1QslZj#+JZ1?U!J13PHB2pP?Ss%&hr*Q#nJ4NgP6JfQxY|kFoDhicXz} zI?zz|J!l`p9+c5UDFI_CP>_A7S@`?bNaJBjNY{B7&*@zZbOI-eFd%Yq6*RXe>GrK& zn-H=Ke1i~?#Mczea4D&11pAD~dZE+=%pR1t28PCVD19U(!6b1?pG2s{p4qdABQeE6i%j5s#W88P99DLyd`Tb#CL@Zi(m=o?=dE9v z0MGna-dR!VuC+)oIUtT^h-eu0>sE$`Kp>jxhS8h>#wMUAXe590; zGgOz!5Py0&=mzX93-t&wiYbC-dd6vb6s0RMC$|)HN{CF(IiQ;kC^<4fjo$zcC- zO^RZhKqY%qCy~%qZ$3cA3cq?G##AI=_o39tnB(T17eF;&OLVD5V~+i6?S*l))&4=< z9L7#4maWua{{WsUN?{};xU}4oR3PRev$xhNS^T6XGY5m(y$M!ej{g8kUv;I04h(^T z{{VW@oRM_TIz@w@n2p&4%^uaM0FopR^orCwrNIYmMtGVLcFAT8$?4LP+76hxt9Ve) z&suY2(DaN6?m4BZbRdAEk8YG*jnYTQY7SOi8rwvKBykbNP24I9V0_S4muWHqqF!)p zKK)HKFlZUuorj(~{`D5`vIg;rRey3&pGlbQ)|7uRP$Y$M??Bi~X2n-QX^EeCpfAuI zLWquWN!rw5B=BgT#_vv5GoB|j4ne4jZy1A-pYcn;30z{EJt>}R7i3gm{{YynC7ZAl)35te;Dt+LhhN$Wk;P}VJEcmKCmpEE zr<4X%ek(7kT&V$39in^Iq^wK!fL%DJ7D7o9Pg+lL)BJ(#MM3rBo(&R( zb|O=<3?R7RfsVA|+QGt{bdPG5on#97jB*e7t-hV4rNoSU)|e!Ow%Ltdn4(E3pR5X% zww|Vlk@FS4b4Wl6kpr|)4(X7n+CG(8GVw*n9kDxB+ma7_v^Pj{u3sYRl?2X;{pa#mPccBWd?BN}s%OTZl;pG1j!VnrxDN{{H|<0O@2ON4OZR zIP*ygo+~rE+;HY#^s9xTP*f662NdjS;RP`Rj&t)@sgQs`Ppgh-a^!40a8VI9$CPp) za}$~^yP%QDN$oRM+%QUEDafR^4LaJSM0Lexjh8l%(aVsb5+L*@fpXA3GCsnqwF+B; za~Z8Ym4}*uBZ1zT#f!T!iqL}9`Fi%I`hJ9#)(+k|j+L@+M;if%jOV3D*`=ip21J4@ zRAoq0g|JH4_*CW6((UtdX9+x4srX~~e$~2Ax0?lL`;B@no|w(5KPVAi zo5A{YVN=Q3=RDu2%6AM}Mq5DeRj!=cA(11g6{y3;=LF6cdTx{vE-&QYp;D1jNvKBC{{Yoo8j?;k8%`o5wl=IE@+GZ4~K>ow7=Y<^3sb&W?vc^z-dnMK(UQQrY&S zQURmR^*;vM-GUavOrB#k=i2Y^iq7zohKAxhSH7))M+5CbyJnSgKgA5O zKWjf6_>=e`veSix5KQGG{8yLif5Q!HObbgvOKvz&ivEM#X$`t^kWa-S)O;apbd`CG zAk5FCS2HiuFy!k;TMyN6jD>jh>HD_tp5ND ztgh0bspuXf(;WW*<5Q>vZk5+)kD}*s}eyG-gx* z>sGX)+1#NS%yq1ljn<@xcHS_gj?jMeh8hgMv13_TTtB)3;=65_M@mZeINqZA_%Fz^M@_q z%$g$AryM@C?)q_6Z=_XL*|4*E=JLQ5D9mRxs^-AD>B5vm)2$)Ic_js6f6Xoy7i-i3 zLEPC_Zj{MmZ6+!crqMvrx26<>gV1Ja#+_(cKhg=@I#Y+OmMst&66AW1B7Nws7fx}- ze_bRmh$9qn$GSi~y0%$ntRzZWT7lInqM48(P<#)EEKLQ2F6L!{~C?J58P&<-L6(voH9uE(5vEv(mBJzP`1sNkaKOIIS7H)fMd% zhr(_9hdTcNio_`>R4Xtf*PrTIrNgQVDk4Fg;QZB(z2_(?!`B^XSK2EZM8Q34iIP*w z?X$Ov5X7k?J9()rl6PbFs_gCyive79%@3_xEvF9LXEZjSxlW=)8Gv}Kd><+{@>Fd! z?KoaI5}cAcV!aDV@Q4j0l;_%#ctwQmLYzcF<}1`%eYTj5)9^E0e0U52 zfsgY-FG>qaj_4<~Q*)`h@&kZEPw7=!>TYd=BZ^f)vHt+XOR{!aFm1wC1`M3k?=&>K z0RR#6(ulhbF4*}B%tkSh{{WgJN=vAxqCDm{3r6?|OuwpD?cH8{Zs&TR*N6r8B} z(#rrW9n;TxSExO3`^i(zp$LZ=%1AoW$+nhCQ#kukE4OH!%S^~30OFOgdRBt#YTX%; zQ5unW!6h(qIH}4gl_tkcp_K^R2^q~vN(fOy2$ZNn&3&hz#H4UB%=7I|e+tVi@7g2| zNT#KG6DcI<&s-(3mv+e<{pyBNSznl#Qaf>9YN#56kkW{iNzV{}@7|wj)<{jKmf#b# zbmoC8dJVoY2(`U!*7hC0v7;GU0+NJByz^MAtM_#4!Vp4$5gmD^ZR+DP0m~R z7^4-X*qg*!Gw2C=*riIDCI{N8uvN|4+iK}D*cl>ut!<`(tLxV3Q&#Fu;f=y8S)+dq zFBH;4>Pu&)(IkE8`21Y?zR0>0lgPt`d*qCzzK+romD{B$G5xe{u8Ot0yKePNffHV< zrhgAD+CqHtTp;pIX{|grXJ|bQp+6B_d_P6aCv(xr&Gn2?)OL8azl3!=x5z_YS_~1t zA2qPje}?8}>2sU`+qHTkoV2wAf@mwB5wOBk8`_u?J?pQF>0Ul9%*}1}a9~vgVR*CWcQ$%0 z--wxHdK3r6XK(ygQY8wUL{ipbWciH{-e04sNnO{6`FozrKD-c;YK~0ZAZjz zJHn(--m_O)ikarQ>~op{%Yv*bW4zT4IX|1!Ip&z6 zQ;}oWW=tL^cOD~*?&7I&K}kDK(oI7ukuo?xG{k_iE-a{kOdjKs$YBuSnpigdMxnMxCnoX~cRg}9=${!`Rdw@yhQ81M9>T?>}jBhDO* zU}TD|u%{IO;xbJ|!^{cBJJpC-Z2?Xkj^E~jZjez{&zvC!2WX0mVgf`CIi6@{oKwqM z$i(70)$4R1V0=))YRjAW32joOfq}q-M>ymmKB+O2(wADVVt0X_hqTq6VI&wLc&c%- zxfd5BcfNC7b<^d9&rSiVhUJe@g!epGTyP=-Kzxx*jz7q`PRdvLf2gP`{@CeNmjZB+ zv_@(hlu7D4`&2hU04SAvj8l@KR!k~Ss&Zzgl&G8t132qlLpu|__)NqON%~Zj=D{mc zTO4iv4m5)Tbs%JphwcU0$S+!PJAASgSC( zS5y7U_N+NP%G43E9xd)noyUsWk}WOVcz>zGuzk6q{{Wv{tk3k@9QqO0C?re~z%(#XPvwI-_^*_r zvJUN}A66+6g&HF}FIfx7KExiB5m%Nag*a;RD3>pdb!0JqQtJFx%4C+d_?MRy)9SdG2Pfvjcxo!zQ{i3c@ z1xn_NwYBfSgWNG)|j;1Hkv{bndYp@rqrLM%B84q$I&PqY}>E zhI)Hb=Je!&kteAY7py=@F`5oG5|s#)y}g8cb3wkKW-@8B_M~>lZj=JpjGt91YKA9b zhZ0U^YQ_;OFe!nu1mJ`F5mvQiloCQl(MoQHrcTwki7*Cp(wnf5k&d{I)EZjy_JM=# z#aX}0GbRVpqE;1-*~k%|wQ|)n7|kVbP)bvr1IglwZ&50DD@k@9mmq{%YW zff9%iW7=xBT1c24qGaRtqPBtlCaGMeNZ^mPCCQDB#TQ6OB%FOKHK;h0lNqfg`Sgta zf5l?;S0i?FIL%eq&XA(!@TEq29+;?FY8zDq;9@CV|&{IvNNdS!W`K~=_ zxhf$?ij}H@$b%#rVdaH34tVG(a6=_3G8dL-a2rl?Pc##j#T$aKJ*!;xuH=&g=yOZn zuu)uqRAM{gw#F)qu58P$-Y^r>5ymRktP1w}0X^yP(+=s_K`~5ibfO|q2hCKK7Nur! zT0Z2T>HQ*?T<9T0`X}7irZuG`03eLd_^K{+(GpYBty~)D876r-(hDk*IpQh4oIfxl(}n_91%Uub;l-T#ZrAkB4V6pI8Owb z;-N7FAFWH7&P_sj#8r@q5h6_$rD{sjbsKxsOf;`p{{T`B6h7JdP-R1ejE=Ph91>J{ z;M8i+H%~|CI~$}v%W6u3+gd`Bpk_hhiBd{y&p(G+-{LHnr@ViwX$86d=FcBviu5*? z6qr^)t~OXWNu9h?=M>L{bQ zIb1c`;i5$xIjEdesw7o{M|5?rhX~}(Y?ULLNMf@or5xw@-~IWX{qk(f0~+D}1> zpgKod>3*w=Me;{WaynECASB2YFzkx%;ohv4FLKc;oKP=m$zLhQYILq@rKU|1Wf^Oo z1(%XSkPp(c+K+*3uD>%W`U>=d)QY8cq23o zaXn_gV?Uxa;Pti$_@x&f6SQ}hM8`pj=Vba$DSf&+SpKn+KXN}Ks>0^oKh|i85yce4 z$t~?5C1(P8n*Aw#;9FOLPn093D>--kFl4BdAtMK&tTOseNd2taAFE=T_h|h7ZSPR( zl+&sSCj+P6wfZKarrSt;dz2%dOjhRe_-a5YLP!xEMJH$YZF0lgflvd8B-TgZXM^b1 z`urIeV+}p<=}89!R+`ne{{TN;NcW_y=vNkK2}mX}kyfPLv`R_|5gbuR9I?N06Dh|h z+Q$!Gp$Q^HaqcN%?^@n>1pLJVb8_lbN|2}rfmz*CQ(GZ$77lU;S{$=!4aJQ&qj=t^ z!wrI!0Amnq72+=tCgn?PlN?27Rj$6GQnaWKlSps%aJMj3lUQ=+a)leP`06sY8+mx< z{{ZQzsz{l~{wgh@cPR=~nD>eWr!qGKGoGTFpet_F3=z)S#~9n$woaCcg3yRWi9Xa? zE&SA-phtP7cQ$SZwJUv*$YPt=OYUSBAs6&nb6Wfj`w(U?* zBt|(sf7*>E!L<~2(&VNB*C`z+<@kcp1Rx(&L`^DPs!~*$5P2eiZ%9h+g37W^AXIXa z(kqN!*x}pfTv?sTLF2VetH#i>QUshF{?%peg3XLGKOoj1Tk6c+4a<~_f98f#aIFOh zsjkmSZN#m$DR@yh%>?z*Q1B^IWE>IOr6;&W&)@)MdsIYZ_3cUY>n%a|%5EH1z+;LT zH}X;y*Va*O%P*b@6hdg{UN}1DUTux@mUyiHw3few2psn}sq| z*E?osiW$`VLF2e|#=ahHt1UdV@sq*oerQ{(S#@ebf$5o0EX7(K_w3hy@J7 zjDwl$QGK^We|=cW!t`3T{Y8+JjD1ZrdtPZ)iV8lMFimIK(=MH}^tQ6SxEpF{@a_HG zus~tJt+-GWXt+O5@#-da;_xR5aw z>bigM)hw{u98%VxB59?M@Z!~_%Z-;kQ|dFC(~s$1RJ%rb{ZGb@@1yhPo}Ep1fe!l8 zJ0^aV!&mU#?ebeo47{>>fdtp-OK%3*TjVl;Mt>|(gTuCoSW=RZN40b@eGXMqk%K4K zA$J&`pS&&nG}KbGt>k}h(Y%skxAwoo#{!b4CNa_?y+x+2Eszob5G#w+E!vWhG4EU9 z^p6?}r0l_y>mEekwNZ&z@Y>e#2^UE72pxLUZ9l*|eVPJ82uL2$Ol~|x+6Sf+p46%T z02A&pI1|>k!v~9psF-Esd3n2$r?j!MuxueA04KG4_MvTVJrT!x!M);DZiz5)GfJ*C z?=?b7jQir8{FudiP_^SuPUOw)&xz33Aqt4aCb{usf{>&0SZlpYYKaRWAY(HWwcSZT zdW>@u`&LPLGkqFk;=vl*?-41&t&2SINgvfp!3tLwQGlLsFCXgFAQH+xW-gFgl+2lWzC zWn6Wn?%kChKGZ9hIU;lWiX{X~89%7WCTHzHIe1kNV4f%!9-Qa%aTQ9O-6xOMX`Dgi zTf?2a}E~;;xY=y;fC$9q~kwR(H%N6-vyUf4xWoN&Wu-+PJ*WCajiL z4oq?@ih;~v{`J$5#2DtR4R4cC+J@M~Nx)N5dV0vKHHw!MW%5DLS zp4IctDDa+m6k|4@hhVz!-0OxDJc?8IIvSr9Whh6K5|D9;nxSjlq5&YNK+jI~dgo#6 zdG@Lhoyw8{=m`|$v}bH3w>Bhgf@B=_tHa}H1rnepXPOWs;WLnVs}cqY{KuNKg}F9d zre6 zmkA0{B6;MRYvMl-zR_7qR1Zu|Wf5%aPaWXE#}Qt8JIg71j@}sKjC4P@aZV5fflV1` z6q)8Ticey81jrudX^QVWu>x^J6w8}LFBwMDG4n{SErN`89ch)t$Xc-{6+#B(dW;jz zY=lfRkQ;ht~Th@;TJaWX$IwO!FDd8KNi@)~jxbOi0_;O;%? zw$!>eB!M-M{5y>ltrCT#%2YWAxUDXqcrKa_gn;6GE~Iz=08sg+!fILqNylW!+aCVa z1^UVV0I^4@uP}2XrYI#NYRD%Pa*1)7R=^T5ndYKgDH~JPtWcRTiX0E>s3;NCPZ3!q z8aBdKQ*wn6m4ZUIM?EU0o$~>0N7M&iv_rh}#V=Y5PoZfzFe9}o=yexdI;&Hn-)1c@ z!)wp>sOR)it%}wH)B?r{?O2^fMa>IPxBlkJE?dTA7+3hCMyuukzc6~U(v;=LK)Bnp z(Nvz50^EJ7@SOhu&2WJjrnZD2Qqd>cxLJ}hJ*dE);z+K9at9QLv8WL?p-1w;??kn2 zv`kEMRuBLkg;u+rfbD_ahoW{A+9Dw0q~dT%;yTjNegcS@G*eeeSOP>Ikyl|;LcK!V zs1hcAIINxRtA%AGb%9=pwvgzPpK4iir=i6FQ#s<8m@-ymEUna%nVxf79WkQ`#9~3A z+S0-aFbDqtQ%$YVhPjEv=8-kXWHdBkp#nx?i5pTxfIr@ZY4rE5F6J>G1EjEYun%742ZVxqgUM94oBwJI4zZr#Bo9^COtoPXAE1z&RUR%Sh@0qP{E z%y*>EV2ATp4iQq@dsuee*@w>P{@i^GjT?amAg|CYtG*V~JLB zXk@7c8Cj~+o>QAbj^Q2aNoNp~1ByqdT9gEnPgw;RDNo+9%2dZSV-S!*=zI67+(-n# zB%X#bQ4WEEMh1PVmtP6YgN~IsS}8@QR^}8s$9fg@0fWgMGf{o`-5HU|J!xgt)CF|t z2&C1Ii=Z7v+#RHWA9}@Vgo3S~ntB+mHPfYL3B=BL_@x&XCD{P;-@Q68ML8I2meffk zZu%OjEs&`r+?v)qqmVL?2gL^NmQoBL3{ka0(1}S{Pcz%UfALZdgeFc8S}N`>(qw@h zV~Rm_s3j%|8RT`XSgo-+X(7W-1^R%=HK5ZQEn#-8GB;+!ejO3Qwi{(CwCHJ|^rO4b$A@?u2-!BO%B)OK*@3%i-XXkwO zK6`)O@7MGBc%ZKU@Mf+}OOlc2IQjrpsXnNc@BP9G$Y(Z8rUPX1(CuJX{@4m#j3>3| zoI&K<$+=kGXP-Qvq`|`8meFs2pBzla@eKx3zrSbK^c>%p_8NKsoNidD|GrQ@=n02T z<*dBXdSfNww3JYqGTX(Bd3i;xI3PUQyR9~--1ul1J8sk99v}Yr5u;jPQN%y6)}`S` zE0x0zFEv!#{fuiQ99??&242CJfwjNKKw6+FwJAc2F$$)vj0*`qSgjkH$vGWOc`(_G z04j&${9cQmmS_>}9qzqx@P=q!R+Du_J)ne7J}=gc0Y14SZTu;GJi_KTK<`fxu23xD z=bV5dTNH`K#_(p^w${l}jSKx8D=0JQHr!lbGyRcwI$sfNHfb1grm|P9_RuSp~)^tM?F4;bEuy#i=CX zo1L$#0h}iJyGD+)Fb}1LWYTdH$6DG^NsdK3-ZPyrw>Iz|dX7oUJu0<91H&VyAO%W}yc;D{p<1T+fruW2D8H=c=FrVggohVtfBKPHR~I?*yni zEr4(i{<5VC&!I_9qv8u|VdmJ$EPP3-`{Wh5`)7p7te&9V8yY4;(4!0+y*9Xma{_Jt zGt9)>V$=9{W7PGH0&2we%K=I6CVM+#Isb(OZb5Ro&;HF?_jI)l=b}^iE^ljngmH7= z?fS!dj)7eGsaT|;`<@hG$oU%Pxn%$5&@0}+^T8&`$z>G=rJOD2NMGs3w=`vLTa)xG zvI?m9FjCU2@Z4Y5mRoDqS&>dM^uV`B_Q3a~zwasW5T7SCnC7rzQ1!GtVaOn{47FZN z%NRmY8gQJ+xs~}Ig+VBeU-qxw^=(-onSUx-Cf-`_j$99+o!6_S@_)2kuD!=r!@Pck z99BD2vhI4XKVU&hx)+e21z%ACzViR730S1$Ib6-?9NCmV=VE*^*&izLIlS(sK|JI4 z&V7(j#Yn=)XFEU({-dbLA)sW7bW{6Fc5guvu}_?H62wZ)yVaF`$kPRXQ?hQ>#$WN; z&dcimT_iqY5C0z^t(-9SvSp>PP&#Jy={A==wADlXe*iAk?~sxLDx&3==2dJ158*%- za=+q20%E!6*1xX z#!#OnP55K;4-k1>tAdHtE@iMM}FC|IEE|5x(3jEQmAFi(gZ3JN7-FdvwpI zqxG%N7e|xrsoL95f>|v?RM8uWuSTN=Y6ioNdq!`x6*)AcEe*|o`TDFCtXkb8>S~qf zPuzTwz98YDglhS6W^#>_mnqhR+h=LK$i8sEq99$~XY6vCzODc+QsUfk(m$1}C+5RL zp*~h$y%xd;OojcvM0^!SM5}ioe|{M7i$0BoQ;tRaXlDHIl~<>dBF74+uE9t7dLOt5 zoZe{jQod*bl0I>{#{OH9{2VtH_CfF8g+(oZLcU=`zkz41{O7TN_w!7Vw=ad8>@Hq! zzn+kF{GSag3RfM<)TRDmGFC~wy75Tk!oR9Dl}}G%G8=C6 z+nZaMZ`hQyU;7FXgtBit9^PtX1rn{REKJ0bwgo4bLr4Dzp8gLYdzz~d1)Q(XZ{?WE3!#9BcWs#r+wX(oY#ML|KXx4_U@jxH~tf9ybD0EGk>Ni z)m#{g+2BYcEqF~G-D}{?^{v##csx3|O3IvUdmVhfsIK8xI_RKs)9ophpP2VHBj*%Z z13XL^NBOTu((WO74=rE#29k_Ywk;}hY_}*( zi>Lvrr4h&yj^r^{)Hty)+Z0PWQ>Qk}MDmDz7)&ttYx{m33FOc=7cTweoCgg5Wy=2b zXBd(oW%qSa-w2K44=Y@1lfGkg9)4>b#ppqBza>LUv)On4POR8UQFc@&^4Fi-+R(}n zSGk#WdpmX5%z6K_=fgu?XIDDDFfpSig7M1C)B zVURNPbj@JAG<;B&!38GzKo`QN~w zc&2A#CZGToO{}AGIX6EWF0IYuvwadA4y%=8{lPbE+147>X37{xZ6$$wM)2=oL=pSN_jz>|1-7S(xBB$ zGn+wY`j_=u%c2G#8=v!?+COM)CFU-4ZJG18X8bmzBW=nEF;QCG69B|PkLD#K<~MP= z9mliSE3=+sZWT!87Ic0=Te#7OFDfkB^f&Y=>wGW?)v=K)y!n3>5joVynM^?n3Xx5bRpLu#5 za*4f%{LEjk##=`1oHPz5oD-+07 zLNfzOVHL3XG``Z%GSD14r4CIQePI2dxa^s+ghQrronU1-UPSNkj!car(QSZXuKtIx z@Nvu23Qn_C3Sxd~I<!- zP+Z+DZ^@~2`R-VWunMZ*VtB;jhhp0-0G%(LcEbap zt7Q|0>6O(}&=+@ofAcm4)e|7MGAP#jdIM)AsvncK^hUF;!e&b|Q%}XZY~~(6Q@*M4 zB~0BMfeMkCU$3a&x)$r9r*m*~Se7GcTkj$Ug$*eZYt}5rium4QhITI62V^oK?V9!z zq2|7mjVTwyKZQ?`KAL67iy8Wn8r=QkpUo-a_*2ag+xhAI85@!g{!dYmfDA*~hL{b$ zC9<00s!~8BhmeU(vIw=C%FHH1R}PmB8d-Vsm;ByA&?@J!38cdM9j&jk*pK;RatU0@CvGvkY7<<6FnRiOtYq;)<(y@bgS?L-lRti34W9nUF z3_B3g@Y77`0rb>zzD=EpEnrnfaMXchl5Q-&C+NmW5+z+DgKkUP^4dUdXd1$ zbdJwG`Ni>uz=-Mct*c?=Mo=Qyl-GBJ$ByGmXc}CcKGVX zul0ArmijjCJf!^uuaL_>rPT8DO70&lIJ}xxTSboK%R+-na=QzP?|gqZ?MBLEl zBH=mz9c4pKk@J_JSZ<=~Csj5DVejyO83?=ju;ALP6o)84I;JiREzFl3Vf7eq10cuj zVgidsQ%KJVXtA}!E!%lJxr{{YQCVLjIxbby>YRsV+(ugEIztmZoq9>+v$+x{cK5J^ zT%Pj2v~NA-u3sXDBnBzPqLut8t9e{hf%h3R21Gou{-IrGp!XMN>l@2D`!lHf3PR2a z4f*8}YYKC+a%+4U0}Ct8Tyay+e`DgsB(?uWUYC$kN5`0bEmUy*uRC6#cP!4ZeKB{Pu+{CK8LCz|=f`9P;T!S@^cu_b z^5`5~3)(TN-?NGX?E{|`b@iAG6P`Yosa&*F0a{3wTNp5bI*u#F4b>p~^Hg_<5Az9f zV%YHl&JF*q=^Qkdl%Bv6ef4X0MX&FF8P!UG&yA)(&dDEo-biI({Sbc05TzKBy_q0- zwELH(E?nq0YtI2DzkhdA&Vseg_~}YD=X-OdwPSOIrqNs5n+|9uYx2as(|!T);tdN0 z7}(@xXdjuh?e;|Asa7iX_*;lv$MD9DZ2or-r@M`Uv}poBbncKEde9iYg|lD`S-Ga#eO z(~OOl`JH3iMG292cKbrC>pkY{z6B1tr(+>ZGx?L-6;7npwKQSeY_|@v{(vU$adF6! zixk}V9A;w(G4-)ivw;X{C1+MCqYh;B2MJvV`1apLVqP!XHk!>~eU z$U1?DTDxYo(kf}Df;R0C6QHE6^!33atOM$gxwG~>TD~Qgeoj$3E{m8r{{`?(XF>~T zC3cbgN$1d^mMye&{Xn4am{h%V$Of7W;K|wf#Fpt%?2HXkL4Ocs2L0|PhP?EUQ)3w< z_yan2aHpt2ORI=J`C&};CIG%4Obg&JdFagdGswF3+Y%$Hs+2@Np#2XZCdkgp!SILl zz_d;+&@!*+hPHZ#0#h0Ce=w;(QVH};j-N7f1dF+MH488HE_ zfJVEE5%tE;;|g!h=q|AJVhTSqtH~u5;NHu!U5mHS2-a1aJfp`_E&o< z4r95JCkbyYAU|Q@BdQ{vrH)a2!I~G+g@TLP7c8M;+itxMwYAqiJ2T*}8~`Re*`uiq5WiX45bG5_SIBj zsH~B`uKo@ETQ40;!2<1D*14L+Mrw@O(4jiOAi9jR@lG4`eK}K$RkT#In)r#eHYhtO z%VYrzjgcn2Ckm(|3BPz61Hx!sT|`?$a5_3x!=n%f>45&y{+>`84KePZ)PmNr9Dit+ zDoy6}G9-7b3&M`UVcBRI%Hs|hwtxzIdk|0w`AdQf2Ssj`imj!q3;TI3Gi|BJsA z+nSrJ7(`n8CQLduGEojq+;&PfUMgbutkqLPUP?!~dbC=YBg;9g+fWok30RPayo-M$-yu%-Ej02A zbL;5JS@{=geaNDzz??_wWDrwT25qaZu(p}s~>BT zemyW+%KxR)-Q1bI5MOVu^i!AM{U>t&!|GBcgMZvkRd4L?W|9^7WN6r9rFOh7SI$=l z2c%{qZu~X`MWcrwTDkskssx2OX1Y--Pk_q@>q=}CYKoy|S2~B%j=6AHIX}l5<>4(3 zZx!XA$rsbvKG5+?PhtG*$y4JD!6|>fLLwboiHbmtw|uu{*7PYmwg5A)jnM3}l z-m1$gSN~IWdYSM-^Q}>=TNKrKkG*Jn4ZujPy&X+46idRa^(~eBefb!1GtYC(L%Lv5 zCfpN}NmQk5q?gi+^z#ww(z>Gj8UTkQg#h8y;%DzHi;H=jJzhA+r*s2YGGHuHFu}s= z@Wi1YkII)?_Y7^#Shp7zd&Y=2AFe3D5+|&$ed6{UGw-i>W}+l!!&RGGtf`$##rE@` zx97}%l=Xh`k8sEKQtHk2)JF|G;^b^z%f!>VJwn0)-*}Dl@J}bsp^_L(C@x+IUyRjx{(#8tNZ*B+ z_L3VBfng*P zho?-=;Y~Q#O!5D%23W*cs@Y3e8@<4KMwf`2cN6T1jWI$C+Q;C;hWG4Pt`Gt2hui@U zPUbDgJ0ePEHbcrgqQV7j!EX=bBymb}b>^El(?d*-D(Wh#0uuC&H>~AmF-BkSvA{yi zn+W|rO$dx2lbEN9%`4lvC2P(^vRxXlPg279)mpg=(9W>XMC22#Z(vZn7Ey0@`3#SdSJgF}mLlxYkBmV5xPiIKA zeQFlv#WR&Y7uhF4nR9k9dHdq_SOu6AJ~%bLDT_>AH~_zrVDA>%3lq495-hL%#QO6M z_OdFAmfpPjw!^KfAIVJtAY(p9?W-sQ!E}_l+RDp{-1)4xTE`z)p7373-nT}wk6d1# zVR2-$RO*f>z9f9p6&R?oQvXl-P8$h=PVCmkOagYNh6PS9evveZi<3k)j(pZLQCoR4 zR|U~|uos1aW37_r1e7&6tx|go%u7bB0<)$0*|g5&3xRnoYWyZttlIiQoNk*&9a@25 zRKikB0!?3^zy|a(0OP1u(Oh1c{0aGM*J41%1jb`z=WEqYxzn#wl*$@&v7Z^lr@g5= zgxlmmXqVMr6nVCm(WPlnOT66JA&oq_Yu>gg6Z3iLdF?LZ-}gb+ zw}Bf7JeKnl!IorT0{0p+<20dW6#bdx&pmVvW22A`0Y-S^p*vBOR%%xjReS73^M~NZ zob1aUiV8A4KQpLzQyf+QUMV2H+GK24aRQq%iIg+hi|JK89YyNkJq8}AMZ5sDo0K)1uvX61-?zFdhppzxgyF@R+;sRskW+75xX;Ae@-d0uRCL z@Dfm5?imZVmJr@Z19SBX1JQ^*f~FfZIJb&@*>=oR_l8As4723owo__oW!6tV0jK#S zwjc5%vM7G6XEQA;_uuSa9MqUO)uYk@u#pd|F1*ekq{Vks=xnBFQf3BokC`ra0Iwe> z7tAGIfVTLDiRE^~TM4M7ZN}bK-0YLXs>x{^FxV#+$Q7VzjVvaYK^i}_jv_U+t+2R@ z56q5-{MB&G2%Ro|o}ougzt-RStY}O8had01@Y^&Y+RP$UPG`AK7ksSqHMSC{L^_aM z?qwjJGPll+T(zz5wY1rCEqVM?W^edz+64FEQ&n|^YhU_%Zcm(X{#W`kRnM*zY^sL5x_>J8Y!pZ})?Q^c;nwoH1iA-J(UhMhJ4Z>K zmL*+n%M7+m(>c(qrxO{ydojPRN!sex5Wd=z&JUtGnQvRm;n)PktT8cU8zZi@R26Fl z0o&>{Qjw=5)gA?wX52=;BqU@lT-B9K9wd?@r}vI^rJ_8J z{i2MRA>K*rH&T{q$(4rpTM5DylA*V>SE3wZLX!dVY%stdvh`Jd(6D`B;g4fmR;48I z%>=E|96qsqC!7p0YKgY?%cRIlR@(3@*UwmE=Sll!U5RxX5t`9(;_<}JY5ft8s@4t5 zJMt~Pl_(}W?TemxIfDqC%}n0e!DkQhDe0{!nKUm-xF?A>SIB?=-DR5X(l3cAJIbF?Ng4|4#Mx=lz&(n{9I#DCcV<_zufF8&`*e3P2(4hveamWO7*z0WF~ zZeZ!=pk&EoTGkJC^G5`F25T5|{-Nbb{~xNVlJec}J&|p+Y&B1H?J{}jLB5JLulq00 zL{)C7*|9H!LR}n;1A?)uvN0it%^xikV|^(a^2oWRu#hZo6{B1ct6_abT^OCeG^u{b zq3ci+29mVcQN7HXi0Cw7j@!1b>=)agdoSP4r?hR|{LVlzoJ6~{l9!x&wpo&s%oF}6 z-B?NH-;g1SL1sRq#Twg?0eyWc;PYjnAh8axG5(!mPz^XgTX zVxu!Qz7vFVH9a7!r_%%dFf(p~tR;3WVp8ydu`*W(vYR6(HMCYhCtZVR0oQ)Ag9m4r zfYPDAhrwu&ur$2QDcv2&roB~!0P!n^Pe3%n6?~a&eMN(khVGXcT?a&vVsIdi0z$$w zc0>lNI?X1hU(=kBaX-EnepLH3s*ZeWt+crfk?t#`lsL{)_g&bKPSF z9us|znWEkoTsdA$?vn>C6Dh-$5f?w?sBPi(mtx!Hf>%W+9>C;Yd^1{}GcOYBwX$k> zdJ9PxzWG%2u||HwIa-cT!9JD*hT*N7<>Y!G-0Os?7s(k@&5s9vqLrgCQXDhV0VqEZRJM`W5RjJcDgKHnCalG4@CJ!mmPevfOB7yf!++n zh{dZza@`|JI;!uf;AY87#!_x(>faz+KZlu{^-?XGWYyGlCaHY`8IvsxdJyMxr5c-;&5azvl^)spmKUEHzx=!CI*6&iFDzv)DDTr%KY2}Px z`zV#RM}qS0AaACWkHM33%BHcrwc7B*(&@CCTyokYpIkLuohG0a%hC^$P>`wuPOaqG zkba)QK@UACgPZr*fJtgH(xrZivRKsBVG01sG33>-^*?|Ki;*WXGg5K_gu(_*J#A>d z+_fsW(T8^p7r6K~gI{xPbX$k(KBZ^wwm<_*vPjsykOqIK#B4`lT;To38n;rf-W&HC zndc;W68L$X4{Vl#u6+C^U2Hv3NUw^x7)o&?#@ zz4K+7fh|F12fpTxpgVb3_hoMR-kKz|Iv;kf0m8n`2M$^O#3dw7Gq{|MfO@24EOx-W zUfj~bU?aVk>#@7Tn6@g5R3l^cZm)yt+f{lGj`9ockq{Tg```koQcxGEX^?p?totG- z^UK(OK)v|Moa(RAt2F+CTM zvMTnw-0)oB2VpG3956brjpXJ5P>}lFGcMIRW{>xoFETV-h2@Zbq zG^kKl`+Ki1XR@$Ts#?j9hH(Vs;R*wa(_Pf$&autvi>Qoc}|v{&)lOZv-V3_ z(#A7mHP^a1^|V%AI?e!J*)AcH&lQn*o-0Y4xyqHQ;;WMw5QcH5y4S0X>Ls;&YpUpP z<>4Z-ug{fRjDfYQM5~=k)tSrnvGl6RYt{vM(*>Ui3Pk$?0{t9fAjL5DV{IvV5lU8Z zPAQy8pu&BH*5AF*Z#{%2XHX6!foHGdJ(R49pN%%toBSG*+40t-mKz)})T4>)ZR^V= z8(mYzj=M^C=^Q|~ws)Ygpuhh^gZMOq$3%N=H@kgaHzg&u8W-nXT(JAkF}>j%C+U$09e%p3OkRQOTwV(ZFiAHsx_ihP{WTy-L#23n+2)x%c6o z{g&X-)HK0p;deJP)z^Y^EmjFkHLw&-Z9}Nc%*gZK)+n=Z6Uu%II0K1@(F&1Yenk1yHxyY3#9Wd5{2^uFoZ)*W6?hE(m1!iWsD4_l zkA$zc$S(Ha=~e!Fhy)?ndEGaYmOnUzcPM*|FY92zNNXRLAGNmlc*!sSF709W$x6HR z`mG59GVD7pQxPwd?l$+Xv-h?TM=ZzS6lofF6^Mpr6oez*E9h1EMiMJ3QGeUkeHuSi zj)5MIl>*m##VCFrFG)a!b1&*n{n*Z#e%EYCEqn9#eR{cdv>qs8$fsrs+@Ke?ZFQ;0 zC6+bHVN|#wz8MzwLPx?r027Ec-ge$SRrn`qkP-1<0xKAs&%*MBhdssSn2yjL!^Ehq zO@F@y$KEq*G_XjAc)Y!B_Obu(o7*F+$HzW@a8VO2&-mPXCRSJ=lXUj}d6N-$?eZRI zO=fjm$h1_ES&_5lQ%kGpR+8J&h9fbQ3`inetxB)%M)fxin7P2sS|Mnj%lPkHakV@{ z;Fdss{EjqRI=#-r7Me879vbuF4{WGoZAs?k zkoh`utC{hH?M8&>$o}Qlfw5IH=w(iHdTBn-T#Nnguc*4|-MxMMiGI*+yndyPmG_~? zL!S!uvhPhiZ!}}ge6XRVOS0pvH6@GlV&F!-6Wo=7;IiS%zv!vuud?62=ylYy7(A@m zrkB-}gFg0rktt5_{2w6Us$X5pn^3>z#$XdWt(7(zmu-@>Tt)Y7w;)P8%dI?bYCYsg&=6n`^s|5NcOshps`{@oOdNMXv3CzBf~HY zsLfiA`cYOiWT(#%D0TB;zoID5dX|X8!PA#_{#2Q=obD}a+>+0Np_18 zRx=*EAsaehe|~2faeo@=Fak;J#o;>*ITb7N^YHu}1<4WC3^$Jo>G4cr&Xz$ZN)Rdx zjp^4K8D{6q*d0sK4rC3biK7Ptj9`yHGH?83uv;5dgm~o=c<>>#3u>1!n7#}AZo`UR}`Cv?#37D9kj<#97Pf@ zWt9pYQ;Kd7`GmG@F58PXGfvfLJXuYZONFG9iPhTzJHg!ZB;=Qjj7)8mmHr-5>k=!y zhnvERokToAXmorzeDg|po)#$c8tI-z9O*~_N+&` z?PGEd%{JP@iqwMM;5TkU>Z5@!?CRZ@R4og$(Mi9-IP_u*sv-_ZL{$JwCW4QOrd$S% zhwrkwUWu?Sx~LB{P#-Q*+z-Q(*@Q^3)BS3-l|=G9snQ5lU=v!z z9$VzRkCQvd+kREr|EK$5f*dMCTKT*c2OogT)UuY`uRi2d{Y#o*s}01ST3<-Z;yKDA zQ>uuIY&m3GzKz&GLUy?{^czyY0EXJNfCWImR2@bN(A`Qp! zgAr7Zi0igl+a0m47>w^VK|<#*cXBpPUUaE90~yopUIbU9VIv-t%+oobogP7?4<4gT zDyfIwV3(9RC;&USXXq@#1XG0T9C|S+hKiYkkH$djIv%y8dXN!_*Jr!DsMnz$TCvR0 zbx8BSV9AvOXXufR_np^efKM?%Ufy&~UbUj_jJD2K2fa&A?bt=rN;zPr#-kUyT1(y+ z&U}YGS01R9^s8wh;$#kW`D~o3Yr5rfG9P&@s>=sYzBigE(s6Wo==twtgnGBh|e@J2vt)$oF{1w>2b8^y4{)11vY$28Jw;QNX5 z@frIhGWp*j8roi)EzA$2*{4V3XE7CX9`$YEGmY)4Y-)?)bCpi1pwtXKCEodY@m>Aq z)CG}C8TlXVzz&%hqTZ1R|Dt08^z?UiZJj$t&`O5v_+(t`-$e1zgj%*4%0?^wnp-Zz zcj0i6tyX3(TWjnYqs*b?xNU{OWGk_)WV}>^gSeL_hsi5LoY(FwHQ+N{zR`(qtveJ= z{lL`1!I-118|q%s7+w_#Dz|_kXiuB@s_RVZz@onzliH>+W>j@BS`8EBKyB1&W{=j$ zS8yLT@KGz(kn*VOX=#njRC?A)_>(_1)+zrzXOjEe06|G~W_us` zAv(U5_SP+rAJzDcNZaIO#^#^BSD?m!`^t60&}NHhB!%V>@87U>Y?P77n>-Jx?$9|H zeSEiV7O3~92H~jufxtE)Ku;&DgY#Q(dlRUsIGpoZx(4U4^Iu1`G>Vb)kWjeF^d~@q z0Fu&rUzP>cW#-*O`(1W{XXy=hrL8U3yo_e5IbqF%@To_(1pQ_G^eKx*Kvlv$U1ng~ z70-Q&Fz4Qgs0kB-Nm@rC-}`?6>wrM?fX#ik((uezyoJyhH9?IzgrHgmpp#4I^PN(L+L9>B5JsM&}9IOQ+^hmGMhYhxD?OazM=QZ-T;s4JFY@YTe*!lREyv%YrinhQ9pw|Vfi$&LD-mS>Ws*^^%BPd5x}(Le_=cDqII<~zK_B5b~o;e*^i zD`;OdXff>+Xt8IkHJ`h_+41R7r^ z!J?$3xYow-gcbkJrT}y4dWT|*ECpeTFT+Qhv=Q=^UQSG1_Zd-*WI&A-RzFkrSe=OQ z@l1229S75EUnio`3$NUU@K58kHMVRm=c3;eN!>na?>3xgwck(l3{9$KB|x)!TyyCq z{}f=*UlAj)=XaX>55inb=kIZV9;PZdmW0tTA!u|>oHN%=5{qdmnG)>%1u%CPp=hRI zjlub#dWGR{u3Vn5Vne!_BAy7avN_C(1E|of-tvN)OYg?7nW^mBznFcM8mqc!(!^h= zfSG#D9?cK|4~5Nj&RUqO^aRKoocZmlOzYAeqB1KMoHzB`E0ulVmC8?3My_+LKQ<~X zzbKFj8aL8^2y^e)#y-{4^pO9pzb}?$J`Aj_AHkD3%_V~zi#*cv3f;RV9YuXKjm<*K z1N}IO=veQ+5RTLGj3J~^>|CFw(NB4s=Y8_%jOKiL<#_s&!i;PtOqTB>b*TH5(`$eO zDfqIf=S~A0wal@?5@Ed8)tob@wo-Zf3Gc=LDC)49-^spB(>TR$W4N*I9e-5#%!cV@ zO#?EokHfdeKv0%vPVxwk-8q%xsODb!)l2lEbkgg2BLRV#I04g|?Z?1)H;VDS;Mb+h z5~G9T5jQ)U(ertv&c>n3mLm0ocT~i3O1Awj2JVS?Yd(7e7PS?{qk(4U!*!#zH2;); zxUfR(^r>83J}fH^dYHVFESAy~r=^v0t-l2;uj1g?8|T;CNH{3EwyoELV0{(Fq$fe& z`n8HFZZ*yr%GiECL$+yKYz7*gzq@4bkow5$zv+SD{^F9G3=6SrWk5XcunRu+2FWqVdbcu*2bSTWh!WklE z8E}XW!>2Uwz7|2WF^El4=T)bj7IGblaeKXd@=AOwp)g2hsR$2TfeQDZdD%K_q8Rfr zkH*KI3>`hbvB|wkD+laSCt7W;$hM=W?$2YET@;+d*Qi}#_!=K79f5-g=LIgA%qOuD z|Js&EacY?vL>-*{t&g^Ltq(JnzqjTdGv~wqwK++q7O?JRG5_opok*Z7pD3KW5v`Aj zNjC&7*BiygFR3GB4#orpnS`rYJWk;&(9kCs!Y1>*G{cWB|7_WmeuROGS)0jEpO%HQ zo0>YnahCcZ32u4ok9vyxrj|2*ZghX2+`nvVVl7=4WdriU}%%Cqf> zkJW5LL?!#31l2<-xEmHh@kvN!c~x7lV#hS633kp^@LWu{K`NqToK&NiO0>BrL)yvJ z%`tTzV3h&e3+kKL(k(t9`ja@#_ohw!~5 zp4^Km>UN_#m=h`kxLI&_jRC3IH(r`4b=$4PEU!|z-;Ajj%hlY~;~4^@6@y~-ZRHNZ zn14%??s7P7=p<$f%5&1~T(1u4B~)jyYFhI#XR7Jq@KrF~+u?0pJe4#&K+BC%ILgAs ztAXc%JUWDcBAAg5iJ4ID*~0VS>hxUQ!XleWcwRCC+@n6{@@PUnwyrBE&VY~Q5-2 zq@G5qjvHrtH~FOKnA~mi+WnzpbaTo~SSAqaIzSn)x|5e=VnT6j95 zIC}o0ue^!Ygc)>MlC;AIGhviJZH%z@sdW{x$@h~OjsrYd9A!Pg*W-Rj>+3byzUjuR zkv{q1DtVl{j%|$a<-bdv3b9&LNnlaY*7Hco~cK9qB zEDMW4YZT!slgYs#0lPW~ zr>pgm>VL2|Qr)@LD$7PZu&$vLwQGtpd#jY^&s2UAzE`o`;Lz`9UbcR#$)REspPmVi zTY3-p1j+d%kF&9HapwtNX#x~=9fGCRcs*p+-!`Sa562hV^Rj78e;lh+?*~KDc{qkJ zTwkQ8*0?wI))*z2>UJJ>g%z-aZR)jVUnlu$10=9?+W->!R!9w=P&3~bG^9# zkUDs_h8L_z-nFod&(RWNto*e6Gw&2q@l7ped}U{T^A=K8!e!rDKr484r}%EZCU$^qV9vGT{Vm`@dqn}rzk;B0DQw*L5zLJChuet zwdN6i${6)kkXx&4z4B+P(8BqVoAokF@v4>$KiN_{W5Yya#C7Bj?+-n!n>r1(AZPe7>_=`dI>DoRg#u=S|L*fPvpMv+${q)%^#( z)d!UA;#qAvvr(5Cs;31fkplOaBPqs(+~2j#XpAg|PAxXuU4n(ZOI1O^ho8&S z=H9mj_1*i=3>ZOK{`Rs}Q*9N~`SJsx;_#9BRw1d4ght z?jzT)y&VgmY^xBN+{BQD4tf2~-Sm zt&?lpn}DpY=UlI1=QlpJfQ^b-F^nFCOoxvep4d!gc7o+4W=Xe#Yh*ca85`mX!&+7= zv2$Q7X0n1v@;aaE7vvPU`0k2KOHy^gKPqvuz_R5# z;gL-9t+4?IK=~0><5BA0DZ%uTyo2J0mNQCUVLsbP7AF}oes%TAw_aa+7W<_Ha!XXT zS;n$ra|=psTBkqK6fs;6r_k;u6NR%0O`~i%+>4!|tS{+QP)xm<#zBqcecelm`pSW> zrByta)rN~F60cyZ9%$Lx9uCKT!@5IUuijtmaUsKF(0Sv6JYON^8_wCM71iRE z>d2mV+O3@9{XKH4@GH?-mYy#iHvC!4X5zZWPehT0%;F%iZePb=3xNMqbQW$+zikvB zFk(o<`~eaZMr;U5Dcw0ojnN?j5+kG$5G55Rju;?JBnOP{QbAB!C*6oR2>~Y{p%VJ; z{RbA;uJ7*Wxz9PDgNBF(`aPL^0s<0^^*?VJw=zn4&nB+t@uxCn+$v6p_~LTHI=Acy z(*ppFkf_Mk87|rZez?+DOm7qS;nQP+D=&ZF5cu0)(c9j4k%!>y_7?Wn65RPi(JMY2 zt;-C&xa=XNUYwe8M6M^49x@>G#1hZSpjmyf!)N|QCGO}UB?Cw=)jAtxXjQLp=fCS) zKR=2|Z8W`nA7s~B7^UfVJ2WioGGenR`olH?v6h%P+qkZz`+XGFabuWY_}h|OD_sI} zV5=i4qg2dVm1@}d9@EROO1^DoNcc3uex`Ugn6#oC__V)HzhG|r>V++qsaioj9WEIf z(j=*n|3a|c$Kt+gsnnJ3aj4QXn_c6{13CV#p7rx|sm(h_3i~d`r}5jIRK1qoTCIji z3D=U(go*86>bktU_I{h#Y0ra^;|9Y7rF8AWh~5{r>)Z-(JYMp_jD!G?Ulfr7O|Y2t zyPftMz-A2I8~#{LUuwn25g2Pu~MD!dUIXkE@Iuu}PO(4|y!W z^G6J16Shs|%LPW-(aBt(qyzBV<--C)XO-`MSnvfV*?1K(wHd#=K|^!$MtlsDv|*IG zMrY^N3aCh$VZ|5#9{A=P;0$OX0*uM-060-ENAV*h_M5ltu;{b&I#nmtVM2-jse1?` zINsp?jat<&WSbj$W-W1&>-FDg&=}z%s_W-~pvQ~)X?1efTazVB6<9R=-WGTinEa}H z7T@*{@U~N_rmYC(`TP$4kIjrIbEbnv+Wk3+MdjaJv8utbic|4<^)u46REhGa_Z@XD zv24F+akY0a)@S3ikNx`X0$6nJzEF(&=0{iA0Z!AUykzOT!Hl++vQ>y4RlxW|Vt?7A|db@Ab8K_&~ z(|xB;Ivg4!Sm1D_l~BIPU?O7`2k+gs`D0T334}t}WMC%Do;;roaCRMEWfdk1K1MaN zvL!m4()e9VT8ITLr$tcvpNUbe#61A0qx=TceXveja%lW8ilO9^afypmAcRXJjY#-2P`l_*|v#PTkZMgoZPX-c0 z;9F^Z*l9ubK69#(vFJc$nLu>@C>KrPN}}U_x*Qh7gYSx^bxQbWzFp@CnAGHwmVrTN zQ50|7bc6AYOM16PcFbm%!wDgZZ91bF@KP=Ll@t_+xeirqtZzi87r%Q(D(Gx|1o%a0ZoXv@ugX3VA?NTqfo1+%l|a3WVE%wRmGSEghd}tH%mC#g-cvQtIr* z&#TKT7vX&9TACd#2@hATq1*68n;r>J%H-mgfd=`BHlp_+Ge4vsMu=6R=?XEmFJTVy z3e0V&VpVZ~F$Vu0BxXvfUIAwgITokj^YX{Qh#WC*n$g8{OxZy(^=r8`%NMBhkCCs+ zh`}F_p~-rU{I6AoZ46ThHa9(Zn5+PUA>z0;#F%*MAZCWt&pU=FPDADu%rM?TAWVly zbOQ!bo47XB{P@%&o1hfnVPQi2BW!LlzFlQ)8YF+Nqbo8aQQ<)%xzSDiJuV-1mkwID z9&&@1^Ippw<2AUN%rE>#mZLeuqf&{18eaCA=3$UKHtnANG3rHVaAY9 zXI6B-XLt>|P|br&hJw#bJHnQZzMcVb@8kTE?|qm)PRQaZY%~lVkx|fek=APrDZ`JD zau|h7gh_axa$cddA;h<4MfjS?jPxryOBIOs(lIHQHJ!PK!#A%DxzBAIr4ytxgvq8Y z9^UPhjrF@%Ja^|LaV%*1=KCWPRMJ>e%@+pc=VCYw9f>`*#Y)&2=qE=_~L zd~$@8VxKVFO0KUxF)#5J-|&#dIPI<|S{Ecn%TEng8OTT<(fzS-p1c0QveW=icy++_ zpzw%)m*#~a(@Qjt$23m+ zR79rC7th40KUNma`6C5KbIP z$KF_!BYw(x&%*5Idq_8LdvGt#yHMt>Ro%76XC2vZ{%ZEdYt`p8>@}n>2E*E>%ggt2 zq*u=|UzEpr6TcrCdKR+jR&O8^q$&q=Ior1*6)3j&%=VY1riqGEN=sk^n`HXJarfeS zn*^46{II4fN!Cg}wYORhS418o+XfF&!4mj`0~;P40DesQf|iw3!OT4dC~m&tNgU_# zj-_K26K$-Ol zMFo_pG6=&>{6(TlEk~dN%IX(PD0C_&^qSu)9F`4IxMwTgLFmMMpIq-xO34@Ao}_Ep z5TJ;82%m42w>4$9Mv%?N%$=|n#KHqd88dn5(Jqn0`4W_LbF)^KfuYZpup{TrC3aIu z+8&Si|8JCY$|6map;3={nOf9DomWF6I4ZOcc!+Z#qVCujjwg~pZ*#M+5Eiv z6xT#^0K3#odbz_(NFGuI)*nAnNQ#+LmQO3eooqJF`YL8Nd?s0X=)TqB(|+6o&>!M< zj&v5AU?2Xj(Yz+d6+TVzav#|OfX9}@6Ju87hA2ct+EWA9Psi(sBU}2ur10}{>knS7$BTLwGfFc;RO>Q^guJfcqd3*g zQ+^gMHwkR}uGeR>;7@GpKFPyGGLJPZ(*tfp-K?c43|GQ-77QqkKc@bqX zRUX({#Q6+0FWtAAi7c45Bzc6onmlPdiRcH{KbN#Fuyk|keFq~@6(*R{x9UlSmlP(DeB^Gk8Tn`-Wm#orfAPEYQM zYsCNcc`vK!3C(Z|-*t@1wSK8)sGD12&mh2GGXkHuxBC9d6&B+MZ6#A-=cN&;qTSXW z7_*TAIa?(JU#R#%1B~J!j)MmuxL5>EJ#tdEq%CKj8fJCmR&z z-Y>+Rh+LSmPC>?x+oavdT#TNQY^PPR=z=Umo+iKtK)UkcTQ>t+54)u9G6+|Ft%n8| zgzoAmVHq9!{;3OHJa%P*rQ z)*LJUPIyT`BINb$@5L#t(V{Jquf9rICSOSeg8C@0sW;Z;NCn;kHP62iP42cV%HFr- zZHP4}?CL-ll#c zVJ}y_yF#;ZMmAHCj=ve^@(1qo4(}{=n{dq2*k~EqLsS%K zcc?p9Or-^?#ie~xv_g6e*byUoa_67e>1epk^+||p zgFUsEo_z9Aq9t08FH*M zNu|BWD|j5=^xwb-@Z`4m90mnGd8WK7{_m2-?L-g$QY6#U-DC}&a3^>2i0D(dht~f9 zFQcU&5~jq0}{*QO=PHI6SYG8qHy3hqVS&=QzEdw!VB z{1H6qH@6i0A6r5}f*OI1GgnK&$??vg3y)l_{w!+9nEkgX*-9;MtpcPI4xiqze+|76 zHrX`W;(ED5%{!G^ucD8I*+waUiwI~54qQBj<_W|1j2}yXM!pe4U=-@-ybB+4h4aZR zdBP+-rVb45@ml%r7`;u&<6+m@w|i{g-5OLS0GP|U7xiQ*)^I5%JP*+_K5q80=cQLFXCR_xGB~4FYzCF91_`^@7{4Z$B_p^x#wjs===aHNO`qlLzUkb}{_4JbW zPx95NUG%9(@UUpKGD1DbXMoG4F#*+R9M~$h0RO73WDEfw9Gj+tgOb>Z_yL-1Xa11PR|=+uXE|EIA2`h{4OzuZMO)`QakRfSxFH-! zq^5i&k*JGmaZ!YeJTeYxbW?4p*3mcDj`{R5=yk;CglV>qF6~@)xkr+0I)KBXKunI* zq`?3`MVwMCopOvoLVI{xG&HRHcFcIHU zmfZcp&!TT3+nOkj>8zq{sk?0{uaavbObgCofjqz2O<8YEUhF+m$q^Y*9>N~Bl|gObSR zqtk4bKe6#M=07EL1RN`HO<9hn z@eWd|c^RlSuzq^afuQ6onb)*iSLyTHb@cnd*zmfWSdDA zHr`q3B*oJjek=_Q0g3$e0g9?B)B#7`-$haN+z^(O?+*v`zNa7)tf6)$prx)`btaMq z+@c$$M2{@aEgQp}HINm6$8+-4Geov27c;N1OLzMvQ^iH!ySXlosLULZZn)JIcwdp; za<7%`xb!=a)$c89{mWcj5U7r_Q}v=7bN zxHYVZvdx#G3KCINZXBh8+1%KnK3+cP)&CogWgx%sE*!8Z}G`by}n!X zt$J~$FPQib;0rxHuet4Uc83{rx6VX5TV$uCki27e=-cnY4mW0 z+U<*5ptCREiWdG%Q55}a)?BbEJ(uD%x=e?-%mW;7ik`T=m(&Fz@_6Ns_EMt1u4v2T zISL}z5(nU~_FwAh6?Hmdr>+&sko9ATq_dfKKTqP8@qCD|3yBXU$z#HL7GJ2pr1#1R z7BLhTeaj+c>QlVBHP&3g%TA&-#X@sPS*fwS=BJ3f@sjUH%~lCu;s~~zr|-ED(o2;fpUc+< zr8}{%xysqutkKu(XXsthI({S-T!{HD=oK+$68x!H()zd#j7#AvvS(g1Q}H58Kdl-< zx~J)1LV=*5{>W0>z5s}qEWI0$DsQYD@G6aNdQV2*~g$ z9729bvwQwN%R$1QI>^eJEK!Qb9}sRuh?%m=FBZMd0E(PfDKA2Ju}=(@q~O5)^yq@J zM$&788IjvR2tF-`$?w+q`RI4W+$JpAO*Db1jwMSVPF#uFqGqza%a`A8$gx4R>KiUz zfg7x{g>fV92FeQ*wh8*Umuw!4bAYUlm9;kt{SO&d8b5xLCV6Awi&Pz|ZRq$6biq86 z82vR$lh&kHK%+t=-1+&Xe}dX-#}6Hl_h86}YN0eoDk3m41+mghRFGw=Hs!_E@wo0O zwg9oF(&&HUt)NOZ&x+{P{B{!!BwA=`Kuj`s&HaDix&_R~=agR!#`d(WiZ?FzYs35Qw@7NeS?m49N%~7u$73S0W3XqWjEW)+bw70>`0dXog zKh1LI>%AU4O}_OOrLyAq*`x0nKPTBjU9$&s7X?h_*_|sL zq4FzZ+Km&ax4kHLmNdKKZ!bg8*(U~W$l+>jz^p&kl zy;!N~cg+GgrOkJ_nd3vO3@f>;x=X~Nf3<%~ic!bNeC*Doa#AM`Yw3%tXj5GZhiZfR z#C&Xf-|&!RtGWn#z{;tkYV;nzD7_jgu|di5w#On^ zN3)Jt+m|QIxnp0x^3)VHe>B&hU~bWkx{FFoEHwAMdH@MQtK(oUKN=eOE%EacK^bzU zf>X5;t?SO-I}Yb}T^VT(3FO1&S);Em&LneYfsM@1+cn`(deg%Bx;(r~zJfL1y~4BC z<#8ldNe}G6565X8g{nGBUxK(>AyzDn22`u6Qd& z77|zx$tPD-Ceiu>VsZ|{zLo6-Z3@}?<88`}RMh7~cT1$Pra`M#&v8BQH zxJuV`lWFxH&hJ_6LDPP6VkEzhw&){;V*N-jRj)P!Q-)k;L@F?` zC%?Fr59y{otrAAq*g(9>d-N8!XFK^q8EC_td&tAG)RcOPJ|`QB)68HhB>iID52w>{ z`RSP7USbd0l!nYf)mKsydw0lY8XTOrFl)Uzg3lPRNm}3BiM&5$zU9>?c8cU&DO*da z?>~yCx}@L5{&shFCy74~2|lW-OcOj&OazYu6bv1VM=h9vcD*cO7+)I2VfLl9W{5mm zue;@^@k&>RB$Q>>TAYR>W;n0rA`z!AghcZ78g-zE8&2QE`~Rb>uZCnAA7D8mdX)+PwUnn z5^<_vUKD92Bc7B=wH8+v6w9ZVw?uhWlwjAnu9#JSyS_+Zb=Ar2D^@y}{AwQR7FJh} z&#RLsaf_u!-U;kH6Nx<2Ym{?%aJh43`ARIi@?P@q*HeO);y=@@1EPCf=QS3Pqh_~X zGo92Gbv6Z^5F9Lq*DS+xv`7EG`|lOQt>ZVxUdQ_l^2xxHtH?Qb>)yAB(z z?W;7%?tjW_nGR^+%A5Z-Y@u&cWVzNO{OoIBp!P5LjoBsJQSe7ale3$(>~fzF@DyQK zZgP;Ezmw&r5Anf`N$^AWRX<=QVe0zt4v`O?l!D13yB3gdjjLY=0v6OhkLHY4x9sR_ z@_U&7Mu0H9CFkO>0?WJX4i)lhzcq68$f(z;pB4}aZM=c!r4Da<-4rowEd_tM#Z^Hy zd`(Q_@(aOtD^4~$awg#`q-)&@O$C(ebU*-xfiU?{OXpM7uEHCcuVcC7$I&)JDm5lq zUe)#kDyI6CJ|BHAHjpG2TBM}9D(vQ9ciLbE{h`Y1g;H}Delh0GBt$V|&F{YlUR|3= zWEfNXO*dWdI`f0(Wm!-iu-|;l_at_W$D??4>MDnFU{Z-WcRt>6i2YSsHhrdexqBa_ zwrMkS$!qy9x^YBba8l!)NvF-z&FoHOkV=F3)Q@*$DPJjp293GzSp&V{jXdsC`#|`R9cXsp&Q4&-}O}^hKP~x zOk!2Ev=iB}G*hd->sN_$Ea>mwwhLOQP!-^>YJ@QW^qVFN9ZaI!Z$y${(^&@l<7f~V z)|ww*ge~t!o&*c+D~ICBg-CR|a!0L&Cc@_=VaQBRDjz*^v(<8ke^uvr$L=wz__hxI zm_$kj)3p}9tx9&gAu~@002}!#5=a*oSdn?dm1D}?*Gd(lCm)BkPj6Y7Xzs|_;Qz4O zyJ~N;-2VC1*XqN48yeL{bhhVqk*t_xbXbj$kxJSj?IwoVaZ|f%D4`gizhkM9FSM^I zQ)9_v3P`k(UBi`akR-eZJrC2!XloEtu!bFa8~L_LyIlVRQn*s+Z2Tq2ffuqi123{1 zG}g8K6UT2=FyrS)?|xgUUC@>Vh5Q&x3R$?2MXB{GzRw8+uZM>$k zeD%~!Yh7QH5!`Ds-_#|iOM~}n1l%g&%o~z*V!#Y!c+C)pH2#@bGaEUCS(hOl(}-L6 zwR?-pqEo|=U&6p6m6XBKoBpdNfu)yO6V9lTxQ_;BKk7xMkZJ9mz3Gd!EQKawQT5q$ z;)N|}2iN6QE;c7|M5B>oT47QF3WS>1t zCh6&ZllJ1bS=#dXA`Yib2lK{79v09dgd@`+gFQ!0Ps-n<1Z&|i)`1|T=i>`|zk`8ZlXT5OMQ-v|^Smm!u8m3nTbvg$iC*W_w6r857@CJ^m@}G!Mp8GK;ACRixw@I8KBVuHzl1NBTSUoSrTw5}3EwP( zgWif1I`iTN`CbioqD>b9)odXt!`FKE(Zr@&!(-bCr>6{}f7!j{zRymmXBiw~kDtkA z{M1Q6q!}v_{sG>7*S;2Kdc5)M;}e>5j`>Md8#JS~AO$OwY9(GCZTJt6XbP95<1c)a zsr^MJNq|Lbv->}3Y;fi?&#Gsvi@^b;a8oh01O2+XU7oVe)7f}e;aC2;{+kB?Rj^*G zdO^CZb^iblX4H3vb;%dPQ*1mCJRGdk7jHKBB$D9-1N@3@d&BMu*A37MHXISFN*Tu| zYMxg+NytwvwjqCOt-Haz6y*CTd5ga_zJAcc&nj9Iq(Jka;6-J#t|iU9-Fyzai-7myrF5$02ydp2DgB!Qph&J;x-tp?dXnl@}+C>2l9w% zM_;}Y$3jsg$X=l`F=|fZ!N!t;b3TbpUAGL!BzA_xlBY^>XX1v&#!yl zb<_)^*Yu20!PqEY(!#fl-^s4BGTN<=nqa!6QCk~ zuFCIx$oQ42$C$;oj}_ZRZ(=Q8)>otPCMz%Fp&1YM(Vv#tcpk#?m9kE)K&CshNCf&Eo_#@e4?y%+(`?vKj7|2>(ea zLeP=j-sjIgo!`HTfABuFJ0^6)i8UjCbEBYwN(X_)F*7W_-hO8{&s>up?WO#5;ghrT zYvOVEWmCb;z>MM$v6%7qQBt_J7k|C%xIFD1iXcyLv?@zdBy@00O)O^8X$a^+J7{5k zthtSx56Hgk{6te)rLz{y;kxCsP4kytv|N_RfL>3q+E-_qU^lVDUH6RfF_v zYi8kzog2oJgyvp*U}{}cDi&3y818AJ!1Ak>dXx$H!DJR~-L9)odmlgYHBo9W)lRy! z?0QslEx0oj{429f7bY^;_Fsg$3eG(NGk^#(QQ+izY2qz8Jnxhf@T=bzJ2~kNOikM; zyImR2;456U!v>`5#px7h{{t|VyHV%M#hBD*`e1&rAw=7pM(f*BnDY99sX`R>=9(z? z+q+r$iQI!ePVWa~H3ne{qB5%TQdw16b^g|#)^GVj6H5EOTf*{`g zJfrIeKlfvx)S*dmHm)GJ#SW{!_snO)!9M_xpF~U_`qWIDtst*9lakic`m-n~p}rgD z$2AY&pzTh7oK<hrKR&Ghdh-)$}sx$EEZr z0(@TO&r+eEdIdmCeyrQz+b+Opg^rK+#Dkorv&bt<1JfHJr-@n0xr;v| z`Ycnc!?n33ShR}e$M>oB)6WF7^A7V$^aII(cYLZOWnsWQRT@178+OEKK7Xo?xN`+i0;>At*#30&t;uR4 z+1r}p=|rPR>hQSj`G)VNEiL1_RfjOh{dP4~vHS{urs?ZL@_4@OnSTIj6N-V|3x|6z zIJ=U0jNIEV-oS=;eKD>RpR6C$NUamRwE-2+(0n!f&|#L3N6+BH>A6o?mrh7*sgH4M zj#k$7ObuQG%3nHbTesb|nv8uAJU_2~$uE*ctBZlshP&qi0mJAFB84VnJ}JKnSOp|< zb7PUEi=qOVcl$YN<5)|jS|`%B>#xLj=C2`V?<*xVqU8Iz&HV!+^Q6SBiJ!K-z2{^F%y+t8 zVWjQ-r>$yyHx9T)+43^u(JJ$y_$`$mzOpMME)cwMf%3?&UsfWxbL}5sdDdLP3unp} z7&dn8wkodm#SYJ8dY~J@|7k$^jZ1w}oz->`j}uZDQuXMG;B{>o7;IWidZEfaMQinC%+()EQsgBM} zmn{a~{v%T6ARD12Pw0$7YBn7nN!65T8jD{2#a{)YTDCMX=l`jS7ZO67djp@sKJHj%nYS?Gatfwo% zmXD473*0MtKf;CSswy?v^@NsqB@K-rr#4F-g%~LJ0UA-t#iC7hKCccI2mhiawg((o z%aq?TJGQH(w;B7N-y_+8sh@-^h?f!pf(0Bes{4y0ur(mD#`(NdaK(f&5Aj0eG`jV# zA@85Z7=vuD1|4x&pOB#Q*AdJ48~^z%m7w6Eh_Id2lZY$tQqNTb3<6bu;7;}ZfC4mJ`1T}0C`}wV~dp$D>gur4beI|b#%UvBcj>8h{ctoNHZv7 z1Kxsc+YsHHuVV_hZ|HgdZpHIY%H*?@DY<6qAfQE%#!?*X%!dsbq4WBM-s69%J*ponfn#^duHTh#|!{nny zTg;_eXc{iRHZtWpC#yFRUiiYmpIR#*@YAj@1bl+K-DWX;981Fd6 zynXCwg^a`=P%Wul>PGTXJN9Qkf-3)8=;-^B**d4sbabNtDFMt<-?wW%-FASRnonTg zn6&8;E)|>beX&hpfQUv13aJWOXD54+pWa~PRgH=Ms&mlK8Etzj{H8h@(Ue*tI2lnO zaJ~`OWNJNVWW4MAeX`&+?qb+A(h#%Sg@E@dPqD-DSlBa{MZ{#J^+kK5KJlO*7>utt zKl;=T#eavn%i%+z&q!>kOr5UD;Sh3@aA}_y=Xr2k7Jk9A&yIN`N7}@EMM}~6A3#bH z83iZ*)sP<(9;YmP8c9|8fqC^jF3xQ*bl0lmlBFK9uR1$jJ_c>1GT=wXPYw-}p9mHj zVV%Rx`OQs*DUa;K`c%LkC1$lB-Bq?w*3_hoj4F-Z+Ize3fEM&Wz@teMA;uTYrW&g= z7}TT=>~|@@t_-jIUw9?crW+5qpLMhx=DC_*P9lZexg#?;uJg?-D5o5nH>84fThfZ- z^W

51~WSL1;2rMeEK*EwkJixt#qcOLY>vMYeMj`-8- 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 c(6YgI Date: Fri, 13 Jan 2023 07:06:51 +0530 Subject: [PATCH 19/38] Document ability to download latest binary artifacts from CI job (#2134) ## Description ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [x] :world_map: Documentation - [ ] :robot: Test - [x] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- .github/workflows/ci.yml | 30 +++++++++++++++++++++++++++--- website/docs/developers/build.md | 7 +++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 008bb6e35..035c3ee4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -419,11 +419,35 @@ jobs: RUDDERSTACK_CORSO_DATA_PLANE_URL: ${{ secrets.RUDDERSTACK_CORSO_DATA_PLANE_URL }} CORSO_VERSION: ${{ needs.SetEnv.outputs.version }} - - name: Upload assets + - name: Upload darwin arm64 uses: actions/upload-artifact@v3 with: - name: corso - path: src/dist/* + name: corso_Darwin_arm64 + path: src/dist/corso_darwin_arm64/corso + + - name: Upload linux arm64 + uses: actions/upload-artifact@v3 + with: + name: corso_Linux_arm64 + path: src/dist/corso_linux_arm64/corso + + - name: Upload darwin amd64 + uses: actions/upload-artifact@v3 + with: + name: corso_Darwin_amd64 + path: src/dist/corso_darwin_amd64_v1/corso + + - name: Upload linux amd64 + uses: actions/upload-artifact@v3 + with: + name: corso_Linux_amd64 + path: src/dist/corso_linux_amd64_v1/corso + + - name: Upload windows amd64 + uses: actions/upload-artifact@v3 + with: + name: corso_Windows_amd64 + path: src/dist/corso_windows_amd64_v1/corso.exe Publish-Image: needs: [Test-Suite-Trusted, Linting, Website-Linting, SetEnv] diff --git a/website/docs/developers/build.md b/website/docs/developers/build.md index ce2cbef4b..baa3261ed 100644 --- a/website/docs/developers/build.md +++ b/website/docs/developers/build.md @@ -18,6 +18,13 @@ 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. +::: + ### Building via Docker For convenience, the Corso build tooling is containerized. To take advantage, you need From f43c458844b6e92d604ff3d49d10119ecab8fffe Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Thu, 12 Jan 2023 18:19:21 -0800 Subject: [PATCH 20/38] Dedupe collection item IDs prior to fetching item data (#2116) ## Description Under some conditions, items can be returned multiple times when enumerating the items in a folder in Exchange. Begin deduping those items by doing the following: * use a map in ExchangeDataCollection to dedupe on item IDs * have enumeration function return a single slice of items containing both additions and deletion so a "last-change-wins" policy can be implemented ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #1954 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/exchange/api/api.go | 9 + .../connector/exchange/api/contacts.go | 18 +- src/internal/connector/exchange/api/events.go | 12 +- src/internal/connector/exchange/api/mail.go | 18 +- src/internal/connector/exchange/api/shared.go | 27 ++- .../exchange/exchange_data_collection.go | 12 +- .../connector/exchange/service_iterators.go | 22 +- .../exchange/service_iterators_test.go | 217 ++++++++++++++++-- .../operations/backup_integration_test.go | 24 +- 9 files changed, 292 insertions(+), 67 deletions(-) diff --git a/src/internal/connector/exchange/api/api.go b/src/internal/connector/exchange/api/api.go index 3fd15409f..999eb6c98 100644 --- a/src/internal/connector/exchange/api/api.go +++ b/src/internal/connector/exchange/api/api.go @@ -24,6 +24,15 @@ type DeltaUpdate struct { Reset bool } +// DeltaResult contains the ID and whether the item referenced by the ID was +// deleted. This allows functions that fetch items for a folder to return a +// single consolidated stream which is easier to dedupe as the order between +// add/update and delete operations is known. +type DeltaResult struct { + ID string + Deleted bool +} + // GraphQuery represents functions which perform exchange-specific queries // into M365 backstore. Responses -> returned items will only contain the information // that is included in the options diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index ab41ff4b3..9a2b3c3dd 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -179,10 +179,10 @@ func (p *contactPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (c Contacts) GetAddedAndRemovedItemIDs( ctx context.Context, user, directoryID, oldDelta string, -) ([]string, []string, DeltaUpdate, error) { +) ([]DeltaResult, DeltaUpdate, error) { service, err := c.service() if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } var ( @@ -192,22 +192,22 @@ func (c Contacts) GetAddedAndRemovedItemIDs( options, err := optionsForContactFoldersItemDelta([]string{"parentFolderId"}) if err != nil { - return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") + return nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") } if len(oldDelta) > 0 { builder := users.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, service.Adapter()) pgr := &contactPager{service, builder, options} - added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) // note: happy path, not the error condition if err == nil { - return added, removed, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() + return items, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() } // 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 { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } resetDelta = true @@ -217,10 +217,10 @@ func (c Contacts) GetAddedAndRemovedItemIDs( builder := service.Client().UsersById(user).ContactFoldersById(directoryID).Contacts().Delta() pgr := &contactPager{service, builder, options} - added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } - return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() + return items, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() } diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index bd37a361a..a9065a686 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -165,29 +165,29 @@ func (p *eventPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (c Events) GetAddedAndRemovedItemIDs( ctx context.Context, user, calendarID, oldDelta string, -) ([]string, []string, DeltaUpdate, error) { +) ([]DeltaResult, DeltaUpdate, error) { service, err := c.service() if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } var errs *multierror.Error options, err := optionsForEventsByCalendar([]string{"id"}) if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } builder := service.Client().UsersById(user).CalendarsById(calendarID).Events() pgr := &eventPager{service, builder, options} - added, _, _, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + items, _, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } // Events don't have a delta endpoint so just return an empty string. - return added, nil, DeltaUpdate{}, errs.ErrorOrNil() + return items, DeltaUpdate{}, errs.ErrorOrNil() } // --------------------------------------------------------------------------- diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index bf6739384..9c6b34155 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -177,10 +177,10 @@ func (p *mailPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (c Mail) GetAddedAndRemovedItemIDs( ctx context.Context, user, directoryID, oldDelta string, -) ([]string, []string, DeltaUpdate, error) { +) ([]DeltaResult, DeltaUpdate, error) { service, err := c.service() if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } var ( @@ -191,22 +191,22 @@ func (c Mail) GetAddedAndRemovedItemIDs( options, err := optionsForFolderMessagesDelta([]string{"isRead"}) if err != nil { - return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") + return nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") } if len(oldDelta) > 0 { builder := users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, service.Adapter()) pgr := &mailPager{service, builder, options} - added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) // note: happy path, not the error condition if err == nil { - return added, removed, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() + return items, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() } // 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 { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } resetDelta = true @@ -216,10 +216,10 @@ func (c Mail) GetAddedAndRemovedItemIDs( builder := service.Client().UsersById(user).MailFoldersById(directoryID).Messages().Delta() pgr := &mailPager{service, builder, options} - added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) if err != nil { - return nil, nil, DeltaUpdate{}, err + return nil, DeltaUpdate{}, err } - return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() + return items, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() } diff --git a/src/internal/connector/exchange/api/shared.go b/src/internal/connector/exchange/api/shared.go index c77e21fa8..dbb1d13fc 100644 --- a/src/internal/connector/exchange/api/shared.go +++ b/src/internal/connector/exchange/api/shared.go @@ -61,10 +61,9 @@ func toValues[T any](a any) ([]getIDAndAddtler, error) { func getItemsAddedAndRemovedFromContainer( ctx context.Context, pager itemPager, -) ([]string, []string, string, error) { +) ([]DeltaResult, string, error) { var ( - addedIDs = []string{} - removedIDs = []string{} + foundItems = []DeltaResult{} deltaURL string ) @@ -73,33 +72,37 @@ func getItemsAddedAndRemovedFromContainer( resp, err := pager.getPage(ctx) if err != nil { if err := graph.IsErrDeletedInFlight(err); err != nil { - return nil, nil, deltaURL, err + return nil, deltaURL, err } if err := graph.IsErrInvalidDelta(err); err != nil { - return nil, nil, deltaURL, err + return nil, deltaURL, err } - return nil, nil, deltaURL, errors.Wrap(err, support.ConnectorStackErrorTrace(err)) + return nil, deltaURL, errors.Wrap(err, support.ConnectorStackErrorTrace(err)) } // each category type responds with a different interface, but all // of them comply with GetValue, which is where we'll get our item data. items, err := pager.valuesIn(resp) if err != nil { - return nil, nil, "", err + return nil, "", err } // iterate through the items in the page for _, item := range items { + newItem := DeltaResult{ + ID: *item.GetId(), + } + // if the additional data conains a `@removed` key, the value will either // be 'changed' or 'deleted'. We don't really care about the cause: both // cases are handled the same way in storage. - if item.GetAdditionalData()[graph.AddtlDataRemoved] == nil { - addedIDs = append(addedIDs, *item.GetId()) - } else { - removedIDs = append(removedIDs, *item.GetId()) + if item.GetAdditionalData()[graph.AddtlDataRemoved] != nil { + newItem.Deleted = true } + + foundItems = append(foundItems, newItem) } // the deltaLink is kind of like a cursor for overall data state. @@ -122,5 +125,5 @@ func getItemsAddedAndRemovedFromContainer( pager.setNext(*nextLink) } - return addedIDs, removedIDs, deltaURL, nil + return foundItems, deltaURL, nil } diff --git a/src/internal/connector/exchange/exchange_data_collection.go b/src/internal/connector/exchange/exchange_data_collection.go index 8668e86b0..c98854f4a 100644 --- a/src/internal/connector/exchange/exchange_data_collection.go +++ b/src/internal/connector/exchange/exchange_data_collection.go @@ -53,9 +53,9 @@ type Collection struct { data chan data.Stream // added is a list of existing item IDs that were added to a container - added []string + added map[string]struct{} // removed is a list of item IDs that were deleted from, or moved out, of a container - removed []string + removed map[string]struct{} // service - client/adapter pair used to access M365 back store service graph.Servicer @@ -102,8 +102,8 @@ func NewCollection( data: make(chan data.Stream, collectionChannelBufferSize), doNotMergeItems: doNotMergeItems, fullPath: curr, - added: make([]string, 0), - removed: make([]string, 0), + added: make(map[string]struct{}, 0), + removed: make(map[string]struct{}, 0), prevPath: prev, service: service, state: stateOf(prev, curr), @@ -222,7 +222,7 @@ func (col *Collection) streamItems(ctx context.Context) { } // delete all removed items - for _, id := range col.removed { + for id := range col.removed { semaphoreCh <- struct{}{} wg.Add(1) @@ -247,7 +247,7 @@ func (col *Collection) streamItems(ctx context.Context) { } // add any new items - for _, id := range col.added { + for id := range col.added { if col.ctrl.FailFast && errs != nil { break } diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index 9a3ef3be7..b893092b9 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -19,7 +19,7 @@ type addedAndRemovedItemIDsGetter interface { GetAddedAndRemovedItemIDs( ctx context.Context, user, containerID, oldDeltaToken string, - ) ([]string, []string, api.DeltaUpdate, error) + ) ([]api.DeltaResult, api.DeltaUpdate, error) } // filterContainersAndFillCollections is a utility function @@ -93,7 +93,7 @@ func filterContainersAndFillCollections( } } - added, removed, newDelta, err := getter.GetAddedAndRemovedItemIDs(ctx, qp.ResourceOwner, cID, prevDelta) + items, 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 { @@ -125,8 +125,22 @@ func filterContainersAndFillCollections( newDelta.Reset) collections[cID] = &edc - edc.added = append(edc.added, added...) - edc.removed = append(edc.removed, removed...) + + // This results in "last one wins" if there's duplicate entries for an ID + // and some are deleted while some are added. + for _, i := range items { + m := edc.added + del := edc.removed + + if i.Deleted { + m = edc.removed + del = edc.added + } + + m[i.ID] = struct{}{} + + delete(del, i.ID) + } // add the current path for the container ID to be used in the next backup // as the "previous path", for reference in case of a rename or relocation. diff --git a/src/internal/connector/exchange/service_iterators_test.go b/src/internal/connector/exchange/service_iterators_test.go index d7cbee9e0..2c1329c2f 100644 --- a/src/internal/connector/exchange/service_iterators_test.go +++ b/src/internal/connector/exchange/service_iterators_test.go @@ -30,8 +30,7 @@ var _ addedAndRemovedItemIDsGetter = &mockGetter{} type ( mockGetter map[string]mockGetterResults mockGetterResults struct { - added []string - removed []string + items []api.DeltaResult newDelta api.DeltaUpdate err error } @@ -41,17 +40,16 @@ func (mg mockGetter) GetAddedAndRemovedItemIDs( ctx context.Context, userID, cID, prevDelta string, ) ( - []string, - []string, + []api.DeltaResult, api.DeltaUpdate, error, ) { results, ok := mg[cID] if !ok { - return nil, nil, api.DeltaUpdate{}, errors.New("mock not found for " + cID) + return nil, api.DeltaUpdate{}, errors.New("mock not found for " + cID) } - return results.added, results.removed, results.newDelta, results.err + return results.items, results.newDelta, results.err } var _ graph.ContainerResolver = &mockResolver{} @@ -112,20 +110,25 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections() { statusUpdater = func(*support.ConnectorOperationStatus) {} allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] dps = DeltaPaths{} // incrementals are tested separately - commonResult = mockGetterResults{ - added: []string{"a1", "a2", "a3"}, - removed: []string{"r1", "r2", "r3"}, + getterItems = []api.DeltaResult{ + {ID: "a1"}, + {ID: "a2"}, + {ID: "a3"}, + {ID: "r1", Deleted: true}, + {ID: "r2", Deleted: true}, + {ID: "r3", Deleted: true}, + } + commonResult = mockGetterResults{ + items: getterItems, newDelta: api.DeltaUpdate{URL: "delta_url"}, } errorResult = mockGetterResults{ - added: []string{"a1", "a2", "a3"}, - removed: []string{"r1", "r2", "r3"}, + items: getterItems, newDelta: api.DeltaUpdate{URL: "delta_url"}, err: assert.AnError, } deletedInFlightResult = mockGetterResults{ - added: []string{"a1", "a2", "a3"}, - removed: []string{"r1", "r2", "r3"}, + items: getterItems, newDelta: api.DeltaUpdate{URL: "delta_url"}, err: graph.ErrDeletedInFlight{Err: *common.EncapsulateError(assert.AnError)}, } @@ -333,13 +336,193 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections() { exColl, ok := coll.(*Collection) require.True(t, ok, "collection is an *exchange.Collection") - assert.ElementsMatch(t, expect.added, exColl.added, "added items") - assert.ElementsMatch(t, expect.removed, exColl.removed, "removed items") + expectAdded := map[string]struct{}{} + expectRemoved := map[string]struct{}{} + + for _, i := range expect.items { + if i.Deleted { + expectRemoved[i.ID] = struct{}{} + } else { + expectAdded[i.ID] = struct{}{} + } + } + + assert.Equal(t, expectAdded, exColl.added, "added items") + assert.Equal(t, expectRemoved, exColl.removed, "removed items") } }) } } +func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_repeatedItems() { + var ( + userID = "user_id" + qp = graph.QueryParams{ + Category: path.EmailCategory, // doesn't matter which one we use. + ResourceOwner: userID, + Credentials: suite.creds, + } + statusUpdater = func(*support.ConnectorOperationStatus) {} + allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] + dps = DeltaPaths{} // incrementals are tested separately + delta = api.DeltaUpdate{URL: "delta_url"} + container1 = mockContainer{ + id: strPtr("1"), + displayName: strPtr("display_name_1"), + p: path.Builder{}.Append("display_name_1"), + } + ) + + table := []struct { + name string + getter mockGetter + resolver graph.ContainerResolver + scope selectors.ExchangeScope + failFast bool + expectErr assert.ErrorAssertionFunc + expectNewColls int + expectMetadataColls int + expectAdded map[string]struct{} + expectRemoved map[string]struct{} + }{ + { + name: "repeated add", + getter: map[string]mockGetterResults{ + "1": { + items: []api.DeltaResult{ + {ID: "a1"}, + {ID: "a1"}, + }, + newDelta: delta, + }, + }, + resolver: newMockResolver(container1), + scope: allScope, + expectErr: assert.NoError, + expectNewColls: 1, + expectMetadataColls: 1, + expectAdded: map[string]struct{}{"a1": {}}, + // Avoid failures for nil map. + expectRemoved: map[string]struct{}{}, + }, + { + name: "repeated remove", + getter: map[string]mockGetterResults{ + "1": { + items: []api.DeltaResult{ + {ID: "a1", Deleted: true}, + {ID: "a1", Deleted: true}, + }, + newDelta: delta, + }, + }, + resolver: newMockResolver(container1), + scope: allScope, + expectErr: assert.NoError, + expectNewColls: 1, + expectMetadataColls: 1, + expectAdded: map[string]struct{}{}, + expectRemoved: map[string]struct{}{"a1": {}}, + }, + { + name: "interleaved, final remove", + getter: map[string]mockGetterResults{ + "1": { + items: []api.DeltaResult{ + {ID: "a1"}, + {ID: "a1", Deleted: true}, + {ID: "a1"}, + {ID: "a1", Deleted: true}, + }, + newDelta: delta, + }, + }, + resolver: newMockResolver(container1), + scope: allScope, + expectErr: assert.NoError, + expectNewColls: 1, + expectMetadataColls: 1, + expectAdded: map[string]struct{}{}, + expectRemoved: map[string]struct{}{"a1": {}}, + }, + { + name: "interleaved, final add", + getter: map[string]mockGetterResults{ + "1": { + items: []api.DeltaResult{ + {ID: "a1"}, + {ID: "a1", Deleted: true}, + {ID: "a1"}, + {ID: "a1", Deleted: true}, + {ID: "a1"}, + }, + newDelta: delta, + }, + }, + resolver: newMockResolver(container1), + scope: allScope, + expectErr: assert.NoError, + expectNewColls: 1, + expectMetadataColls: 1, + expectAdded: map[string]struct{}{"a1": {}}, + expectRemoved: map[string]struct{}{}, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + collections := map[string]data.Collection{} + + err := filterContainersAndFillCollections( + ctx, + qp, + test.getter, + collections, + statusUpdater, + test.resolver, + test.scope, + dps, + control.Options{FailFast: test.failFast}, + ) + test.expectErr(t, err) + + // collection assertions + + deleteds, news, metadatas, doNotMerges := 0, 0, 0, 0 + for _, c := range collections { + if c.FullPath().Service() == path.ExchangeMetadataService { + metadatas++ + continue + } + + if c.State() == data.DeletedState { + deleteds++ + } + + if c.State() == data.NewState { + news++ + } + + if c.DoNotMergeItems() { + doNotMerges++ + } + + exColl, ok := c.(*Collection) + require.True(t, ok, "collection is an *exchange.Collection") + + assert.Equal(t, test.expectAdded, exColl.added) + assert.Equal(t, test.expectRemoved, exColl.removed) + } + + assert.Zero(t, deleteds, "deleted collections") + assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections") + assert.Equal(t, test.expectNewColls, news, "new collections") + }) + } +} + func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_incrementals() { var ( userID = "user_id" @@ -353,11 +536,11 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_incre statusUpdater = func(*support.ConnectorOperationStatus) {} allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] commonResults = mockGetterResults{ - added: []string{"added"}, + items: []api.DeltaResult{{ID: "added"}}, newDelta: api.DeltaUpdate{URL: "new_delta_url"}, } expiredResults = mockGetterResults{ - added: []string{"added"}, + items: []api.DeltaResult{{ID: "added"}}, newDelta: api.DeltaUpdate{ URL: "new_delta_url", Reset: true, diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 5876cd331..6a0a62fd2 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -941,19 +941,35 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { switch category { case path.EmailCategory: - ids, _, _, err := ac.Mail().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") + ids, _, err := ac.Mail().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") require.NoError(t, err, "getting message ids") require.NotEmpty(t, ids, "message ids in folder") - err = cli.MessagesById(ids[0]).Delete(ctx, nil) + var idx int + + for _, item := range ids { + if item.Deleted { + idx++ + } + } + + err = cli.MessagesById(ids[idx].ID).Delete(ctx, nil) require.NoError(t, err, "deleting email item: %s", support.ConnectorStackErrorTrace(err)) case path.ContactsCategory: - ids, _, _, err := ac.Contacts().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") + ids, _, err := ac.Contacts().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") require.NoError(t, err, "getting contact ids") require.NotEmpty(t, ids, "contact ids in folder") - err = cli.ContactsById(ids[0]).Delete(ctx, nil) + var idx int + + for _, item := range ids { + if item.Deleted { + idx++ + } + } + + err = cli.ContactsById(ids[idx].ID).Delete(ctx, nil) require.NoError(t, err, "deleting contact item: %s", support.ConnectorStackErrorTrace(err)) } } From f777aa2c4c49bf5e54e946f4c80b39bb754a97ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 07:02:46 +0000 Subject: [PATCH 21/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.178=20to=201.44.179=20in=20/src=20(#?= =?UTF-8?q?2139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.178 to 1.44.179.

Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.179 (2023-01-12)

Service Client Updates

  • service/cleanrooms: Adds new service
  • service/lambda: Updates service API and documentation
    • Add support for MaximumConcurrency parameter for SQS event source. Customers can now limit the maximum concurrent invocations for their SQS Event Source Mapping.
  • service/logs: Updates service API and documentation
    • Bug fix: logGroupName is now not a required field in GetLogEvents, FilterLogEvents, GetLogGroupFields, and DescribeLogStreams APIs as logGroupIdentifier can be provided instead
  • service/mediaconvert: Updates service API and documentation
    • The AWS Elemental MediaConvert SDK has added support for compact DASH manifest generation, audio normalization using TruePeak measurements, and the ability to clip the sample range in the color corrector.
  • service/secretsmanager: Updates service documentation and examples
    • Update documentation for new ListSecrets and DescribeSecret parameters
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.178&new-version=1.44.179)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 6de239001..68c469180 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,7 +6,7 @@ replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.2023011220 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/aws/aws-sdk-go v1.44.178 + github.com/aws/aws-sdk-go v1.44.179 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 diff --git a/src/go.sum b/src/go.sum index 97aa7007a..76e086bee 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.178 h1:4igreoWPEA7xVLnOeSXLhDXTsTSPKQONZcQ3llWAJw0= -github.com/aws/aws-sdk-go v1.44.178/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.179 h1:2mLZYSRc6awtjfD3XV+8NbuQWUVOo03/5VJ0tPenMJ0= +github.com/aws/aws-sdk-go v1.44.179/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= From 01d8d085e81d79ba0fe16ce8773e91529afdb346 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Fri, 13 Jan 2023 13:14:55 +0530 Subject: [PATCH 22/38] Write logs to disk by default (#2082) ## Description This changes the behavior of logs and writes it to disk instead of `stdout` by default. It also adds a new flag `--log-file` to specify the filename for the log file. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * fixes https://github.com/alcionai/corso/issues/2076 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 1 + src/cli/cli.go | 6 +- src/pkg/logger/logger.go | 82 +++++++++++++++++------ src/pkg/logger/logpath_darwin.go | 10 +++ src/pkg/logger/logpath_windows.go | 9 +++ src/pkg/logger/logpath_xdg.go | 17 +++++ website/docs/setup/configuration.md | 14 ++++ website/docs/support/bugs-and-features.md | 3 +- website/styles/Vocab/Base/accept.txt | 4 +- 9 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 src/pkg/logger/logpath_darwin.go create mode 100644 src/pkg/logger/logpath_windows.go create mode 100644 src/pkg/logger/logpath_xdg.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 650b773c5..3a38d8554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The selectors Reduce() process will only include details that match the DiscreteOwner, if one is specified. - New selector constructors will automatically set the DiscreteOwner if given a single-item slice. +- Write logs to disk by default ([#2082](https://github.com/alcionai/corso/pull/2082)) ### Fixed diff --git a/src/cli/cli.go b/src/cli/cli.go index c9b2a333e..be5059809 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -64,7 +64,7 @@ func BuildCommandTree(cmd *cobra.Command) { cmd.Flags().BoolP("version", "v", false, "current version info") cmd.PersistentPostRunE = config.InitFunc() config.AddConfigFlags(cmd) - logger.AddLogLevelFlag(cmd) + logger.AddLoggingFlags(cmd) observe.AddProgressBarFlags(cmd) print.AddOutputFlag(cmd) options.AddGlobalOperationFlags(cmd) @@ -91,7 +91,9 @@ func Handle() { BuildCommandTree(corsoCmd) - ctx, log := logger.Seed(ctx, logger.PreloadLogLevel()) + loglevel, logfile := logger.PreloadLoggingFlags() + ctx, log := logger.Seed(ctx, loglevel, logfile) + defer func() { _ = log.Sync() // flush all logs in the buffer }() diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index bd30fcbd9..bead584b5 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -3,6 +3,8 @@ package logger import ( "context" "os" + "path/filepath" + "time" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -10,6 +12,9 @@ import ( "go.uber.org/zap/zapcore" ) +// Default location for writing logs, initialized in platform specific files +var userLogsDir string + var ( logCore *zapcore.Core loggerton *zap.SugaredLogger @@ -17,6 +22,9 @@ var ( // logging level flag llFlag = "info" + // logging file flags + lfFlag = "" + DebugAPI bool readableOutput bool ) @@ -34,17 +42,26 @@ const ( const ( debugAPIFN = "debug-api-calls" logLevelFN = "log-level" + logFileFN = "log-file" readableLogsFN = "readable-logs" ) -// adds the persistent flag --log-level to the provided command. -// defaults to "info". +// Returns the default location for writing logs +func defaultLogLocation() string { + return filepath.Join(userLogsDir, "corso", "logs", time.Now().UTC().Format("2006-01-02T15-04-05Z")+".log") +} + +// adds the persistent flag --log-level and --log-file to the provided command. +// defaults to "info" and the default log location. // This is a hack for help displays. Due to seeding the context, we also // need to parse the log level before we execute the command. -func AddLogLevelFlag(cmd *cobra.Command) { +func AddLoggingFlags(cmd *cobra.Command) { fs := cmd.PersistentFlags() fs.StringVar(&llFlag, logLevelFN, "info", "set the log level to debug|info|warn|error") + // The default provided here is only for help info + fs.StringVar(&lfFlag, logFileFN, "corso-.log", "location for writing logs, use '-' for stdout") + fs.Bool(debugAPIFN, false, "add non-2xx request/response errors to logging") fs.Bool( @@ -54,13 +71,17 @@ func AddLogLevelFlag(cmd *cobra.Command) { fs.MarkHidden(readableLogsFN) } -// Due to races between the lazy evaluation of flags in cobra and the need to init logging -// behavior in a ctx, log-level gets pre-processed manually here using pflags. The canonical -// AddLogLevelFlag() ensures the flag is displayed as part of the help/usage output. -func PreloadLogLevel() string { +// Due to races between the lazy evaluation of flags in cobra and the +// need to init logging behavior in a ctx, log-level and log-file gets +// pre-processed manually here using pflags. The canonical +// AddLogLevelFlag() and AddLogFileFlag() ensures the flags are +// displayed as part of the help/usage output. +func PreloadLoggingFlags() (string, string) { + dlf := defaultLogLocation() fs := pflag.NewFlagSet("seed-logger", pflag.ContinueOnError) fs.ParseErrorsWhitelist.UnknownFlags = true fs.String(logLevelFN, "info", "set the log level to debug|info|warn|error") + fs.String(logFileFN, dlf, "location for writing logs") fs.BoolVar(&DebugAPI, debugAPIFN, false, "add non-2xx request/response errors to logging") fs.BoolVar(&readableOutput, readableLogsFN, false, "minimizes log output: removes the file and date, colors the level") // prevents overriding the corso/cobra help processor @@ -68,20 +89,40 @@ func PreloadLogLevel() string { // parse the os args list to find the log level flag if err := fs.Parse(os.Args[1:]); err != nil { - return "info" + return "info", dlf } // retrieve the user's preferred log level // automatically defaults to "info" levelString, err := fs.GetString(logLevelFN) if err != nil { - return "info" + return "info", dlf } - return levelString + // retrieve the user's preferred log file location + // automatically defaults to default log location + logfile, err := fs.GetString(logFileFN) + if err != nil { + return "info", dlf + } + + if logfile == "-" { + logfile = "stdout" + } + + if logfile != "stdout" && logfile != "stderr" { + logdir := filepath.Dir(logfile) + + err := os.MkdirAll(logdir, 0o755) + if err != nil { + return "info", "stderr" + } + } + + return levelString, logfile } -func genLogger(level logLevel) (*zapcore.Core, *zap.SugaredLogger) { +func genLogger(level logLevel, logfile string) (*zapcore.Core, *zap.SugaredLogger) { // when testing, ensure debug logging matches the test.v setting for _, arg := range os.Args { if arg == `--test.v=true` { @@ -136,20 +177,23 @@ func genLogger(level logLevel) (*zapcore.Core, *zap.SugaredLogger) { cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder } + cfg.OutputPaths = []string{logfile} lgr, err = cfg.Build(opts...) } else { - lgr, err = zap.NewProduction() + cfg := zap.NewProductionConfig() + cfg.OutputPaths = []string{logfile} + lgr, err = cfg.Build() } // fall back to the core config if the default creation fails if err != nil { - lgr = zap.New(*logCore) + lgr = zap.New(core) } return &core, lgr.Sugar() } -func singleton(level logLevel) *zap.SugaredLogger { +func singleton(level logLevel, logfile string) *zap.SugaredLogger { if loggerton != nil { return loggerton } @@ -161,7 +205,7 @@ func singleton(level logLevel) *zap.SugaredLogger { return loggerton } - logCore, loggerton = genLogger(level) + logCore, loggerton = genLogger(level, logfile) return loggerton } @@ -178,12 +222,12 @@ const ctxKey loggingKey = "corsoLogger" // It also parses the command line for flag values prior to executing // cobra. This early parsing is necessary since logging depends on // a seeded context prior to cobra evaluating flags. -func Seed(ctx context.Context, lvl string) (context.Context, *zap.SugaredLogger) { +func Seed(ctx context.Context, lvl, logfile string) (context.Context, *zap.SugaredLogger) { if len(lvl) == 0 { lvl = "info" } - zsl := singleton(levelOf(lvl)) + zsl := singleton(levelOf(lvl), logfile) return Set(ctx, zsl), zsl } @@ -192,7 +236,7 @@ func Seed(ctx context.Context, lvl string) (context.Context, *zap.SugaredLogger) func SeedLevel(ctx context.Context, level logLevel) (context.Context, *zap.SugaredLogger) { l := ctx.Value(ctxKey) if l == nil { - zsl := singleton(level) + zsl := singleton(level, defaultLogLocation()) return Set(ctx, zsl), zsl } @@ -212,7 +256,7 @@ func Set(ctx context.Context, logger *zap.SugaredLogger) context.Context { func Ctx(ctx context.Context) *zap.SugaredLogger { l := ctx.Value(ctxKey) if l == nil { - return singleton(levelOf(llFlag)) + return singleton(levelOf(llFlag), defaultLogLocation()) } return l.(*zap.SugaredLogger) diff --git a/src/pkg/logger/logpath_darwin.go b/src/pkg/logger/logpath_darwin.go new file mode 100644 index 000000000..1c2ea862c --- /dev/null +++ b/src/pkg/logger/logpath_darwin.go @@ -0,0 +1,10 @@ +package logger + +import ( + "os" + "path/filepath" +) + +func init() { + userLogsDir = filepath.Join(os.Getenv("HOME"), "Library", "Logs") +} diff --git a/src/pkg/logger/logpath_windows.go b/src/pkg/logger/logpath_windows.go new file mode 100644 index 000000000..dfa046ba7 --- /dev/null +++ b/src/pkg/logger/logpath_windows.go @@ -0,0 +1,9 @@ +package logger + +import ( + "os" +) + +func init() { + userLogsDir = os.Getenv("LOCALAPPDATA") +} diff --git a/src/pkg/logger/logpath_xdg.go b/src/pkg/logger/logpath_xdg.go new file mode 100644 index 000000000..fe1000338 --- /dev/null +++ b/src/pkg/logger/logpath_xdg.go @@ -0,0 +1,17 @@ +//go:build !windows && !darwin +// +build !windows,!darwin + +package logger + +import ( + "os" + "path/filepath" +) + +func init() { + if os.Getenv("XDG_CACHE_HOME") != "" { + userLogsDir = os.Getenv("XDG_CACHE_HOME") + } else { + userLogsDir = filepath.Join(os.Getenv("HOME"), ".cache") + } +} diff --git a/website/docs/setup/configuration.md b/website/docs/setup/configuration.md index 4a07d8f21..cee7c7988 100644 --- a/website/docs/setup/configuration.md +++ b/website/docs/setup/configuration.md @@ -126,3 +126,17 @@ directory within the container. + +## Log Files + +The location of log files varies by operating system: + +* On Linux - `~/.cache/corso/logs/.log` +* On macOS - `~/Library/Logs/corso/logs/.log` +* On Windows - `%LocalAppData%\corso/logs/.log` + +Log file location can be overridden by setting the `--log-file` flag. + +:::info +You can use `stdout` or `stderr` as the `--log-file` location to redirect the logs to "stdout" and "stderr" respectively. +::: diff --git a/website/docs/support/bugs-and-features.md b/website/docs/support/bugs-and-features.md index 1e3bfff12..ac637655a 100644 --- a/website/docs/support/bugs-and-features.md +++ b/website/docs/support/bugs-and-features.md @@ -4,4 +4,5 @@ You can learn more about the Corso roadmap and how to interpret it [here](https: If you run into a bug or have feature requests, please file a [GitHub issue](https://github.com/alcionai/corso/issues/) and attach the `bug` or `enhancement` label to the issue. When filing bugs, please run Corso with `--log-level debug` -and add the logs to the bug report. +and add the logs to the bug report. You can find more information about where logs are stored in the +[log files](../../setup/configuration/#log-files) section in setup docs. diff --git a/website/styles/Vocab/Base/accept.txt b/website/styles/Vocab/Base/accept.txt index 7adf5969f..eae43d149 100644 --- a/website/styles/Vocab/Base/accept.txt +++ b/website/styles/Vocab/Base/accept.txt @@ -34,4 +34,6 @@ Gitlab cyberattack Atlassian SLAs -runbooks \ No newline at end of file +runbooks +stdout +stderr \ No newline at end of file From 928913f6b0f486dac818c560203789d939276c4e Mon Sep 17 00:00:00 2001 From: Niraj Tolia Date: Fri, 13 Jan 2023 00:19:32 -0800 Subject: [PATCH 23/38] Update log file docs (#2141) ## Description - Updates the log file docs to use OS tabs - Fixes Windows forward vs. backslash in paths - Updates the GitHub issue template ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :bug: Bugfix - [x] :world_map: Documentation --- .github/ISSUE_TEMPLATE/BUG-REPORT.yaml | 2 +- website/docs/setup/configuration.md | 31 +++++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml index f9861b415..438c9f35b 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml @@ -35,6 +35,6 @@ body: id: logs attributes: label: Relevant log output - description: Please run Corso with `--log-level debug`. + description: Please run Corso with `--log-level debug` and attach the log file. placeholder: This will be automatically formatted, so no need for backticks. render: shell diff --git a/website/docs/setup/configuration.md b/website/docs/setup/configuration.md index cee7c7988..85c99c6bb 100644 --- a/website/docs/setup/configuration.md +++ b/website/docs/setup/configuration.md @@ -129,14 +129,29 @@ directory within the container. ## Log Files -The location of log files varies by operating system: +The default location of Corso's log file is shown below but the location can be overridden by using the `--log-file` flag. +You can also use `stdout` or `stderr` as the `--log-file` location to redirect the logs to "stdout" and "stderr" respectively. -* On Linux - `~/.cache/corso/logs/.log` -* On macOS - `~/Library/Logs/corso/logs/.log` -* On Windows - `%LocalAppData%\corso/logs/.log` + + -Log file location can be overridden by setting the `--log-file` flag. + ```powershell + %LocalAppData%\corso\logs\.log + ``` -:::info -You can use `stdout` or `stderr` as the `--log-file` location to redirect the logs to "stdout" and "stderr" respectively. -::: + + + + ```bash + $HOME/.cache/corso/logs/.log + ``` + + + + + ```bash + $HOME/Library/Logs/corso/logs/.log + ``` + + + From 0ea5f58ae4a2ec2de1a20f8a7b5f1c67eaf3aa59 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Sat, 14 Jan 2023 00:22:28 +0530 Subject: [PATCH 24/38] Update changelog header for v0.1.0 release (#2131) ## Description Update headers in changelog now that we have made v0.1.0 ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [x] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [ ] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a38d8554..352b8703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] (alpha) + +## [v0.1.0] (alpha) - 2023-01-13 + ### Added - Folder entries in backup details now indicate whether an item in the hierarchy was updated @@ -116,7 +119,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.0.4...HEAD +[Unreleased]: https://github.com/alcionai/corso/compare/v0.1.0...HEAD +[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 [v0.0.2]: https://github.com/alcionai/corso/compare/v0.0.1...v0.0.2 From 5980c4dca00f62bd77fd6d1d0cca5b7f6935f4ac Mon Sep 17 00:00:00 2001 From: Keepers Date: Fri, 13 Jan 2023 12:34:01 -0700 Subject: [PATCH 25/38] log observed messages (#2073) ## Description add logging to the observe package, assume that every instance where a message is observed, it also gets logged. Merger may want to wait until logging to a file is the standard behavior, else the terminal might get messy/buggy. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * closes #2061 ## Test Plan - [x] :muscle: Manual --- .../connector/exchange/data_collections.go | 2 +- .../exchange/exchange_data_collection.go | 8 +- .../connector/exchange/service_restore.go | 2 +- src/internal/connector/onedrive/collection.go | 8 +- .../connector/onedrive/collections.go | 2 +- src/internal/connector/onedrive/restore.go | 13 ++- .../connector/sharepoint/collection.go | 6 +- .../connector/sharepoint/data_collections.go | 5 +- src/internal/kopia/upload.go | 1 - src/internal/observe/observe.go | 89 +++++++++++++++---- src/internal/observe/observe_test.go | 17 ++-- src/internal/operations/backup.go | 4 +- src/internal/operations/operation.go | 2 +- src/internal/operations/restore.go | 6 +- src/pkg/repository/repository.go | 2 +- 15 files changed, 120 insertions(+), 47 deletions(-) diff --git a/src/internal/connector/exchange/data_collections.go b/src/internal/connector/exchange/data_collections.go index 33467411a..719764d35 100644 --- a/src/internal/connector/exchange/data_collections.go +++ b/src/internal/connector/exchange/data_collections.go @@ -251,7 +251,7 @@ func createCollections( Credentials: creds, } - foldersComplete, closer := observe.MessageWithCompletion(fmt.Sprintf("∙ %s - %s:", qp.Category, user)) + foldersComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf("%s - %s", qp.Category, 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 c98854f4a..7fc3faadd 100644 --- a/src/internal/connector/exchange/exchange_data_collection.go +++ b/src/internal/connector/exchange/exchange_data_collection.go @@ -195,7 +195,11 @@ func (col *Collection) streamItems(ctx context.Context) { if len(col.added)+len(col.removed) > 0 { var closer func() - colProgress, closer = observe.CollectionProgress(user, col.fullPath.Category().String(), col.fullPath.Folder()) + colProgress, closer = observe.CollectionProgress( + ctx, + user, + col.fullPath.Category().String(), + col.fullPath.Folder()) go closer() @@ -320,7 +324,7 @@ func (col *Collection) finishPopulation(ctx context.Context, success int, totalB }, errs, col.fullPath.Folder()) - logger.Ctx(ctx).Debug(status.String()) + logger.Ctx(ctx).Debugw("done streaming items", "status", status.String()) col.statusUpdater(status) } diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index cdf4541a9..06c56ac9d 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -365,7 +365,7 @@ func restoreCollection( user = directory.ResourceOwner() ) - colProgress, closer := observe.CollectionProgress(user, category.String(), directory.Folder()) + colProgress, closer := observe.CollectionProgress(ctx, user, category.String(), directory.Folder()) defer closer() defer close(colProgress) diff --git a/src/internal/connector/onedrive/collection.go b/src/internal/connector/onedrive/collection.go index 22ac8d746..4ea9ea9eb 100644 --- a/src/internal/connector/onedrive/collection.go +++ b/src/internal/connector/onedrive/collection.go @@ -182,10 +182,10 @@ func (oc *Collection) populateItems(ctx context.Context) { } folderProgress, colCloser := observe.ProgressWithCount( + ctx, observe.ItemQueueMsg, "/"+parentPathString, - int64(len(oc.driveItems)), - ) + int64(len(oc.driveItems))) defer colCloser() defer close(folderProgress) @@ -253,7 +253,7 @@ func (oc *Collection) populateItems(ctx context.Context) { } itemReader := lazy.NewLazyReadCloser(func() (io.ReadCloser, error) { - progReader, closer := observe.ItemProgress(itemData, observe.ItemBackupMsg, itemName, itemSize) + progReader, closer := observe.ItemProgress(ctx, itemData, observe.ItemBackupMsg, itemName, itemSize) go closer() return progReader, nil }) @@ -290,6 +290,6 @@ func (oc *Collection) reportAsCompleted(ctx context.Context, itemsRead int, byte errs, oc.folderPath.Folder(), // Additional details ) - logger.Ctx(ctx).Debug(status.String()) + logger.Ctx(ctx).Debugw("done streaming items", "status", status.String()) oc.statusUpdater(status) } diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index f7e3d9290..d3528cb02 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -89,7 +89,7 @@ func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { } } - observe.Message(fmt.Sprintf("Discovered %d items to backup", c.NumItems)) + observe.Message(ctx, fmt.Sprintf("Discovered %d items to backup", c.NumItems)) collections := make([]data.Collection, 0, len(c.CollectionMap)) for _, coll := range c.CollectionMap { diff --git a/src/internal/connector/onedrive/restore.go b/src/internal/connector/onedrive/restore.go index 75dd6fa07..31149c7aa 100644 --- a/src/internal/connector/onedrive/restore.go +++ b/src/internal/connector/onedrive/restore.go @@ -99,7 +99,10 @@ func RestoreCollection( restoreFolderElements = append(restoreFolderElements, drivePath.Folders...) trace.Log(ctx, "gc:oneDrive:restoreCollection", directory.String()) - logger.Ctx(ctx).Debugf("Restore target for %s is %v", dc.FullPath(), restoreFolderElements) + logger.Ctx(ctx).Infow( + "restoring to destination", + "origin", dc.FullPath().Folder(), + "destination", restoreFolderElements) // Create restore folders and get the folder ID of the folder the data stream will be restored in restoreFolderID, err := CreateRestoreFolders(ctx, service, drivePath.DriveID, restoreFolderElements) @@ -195,7 +198,11 @@ func CreateRestoreFolders(ctx context.Context, service graph.Servicer, driveID s ) } - logger.Ctx(ctx).Debugf("Resolved %s in %s to %s", folder, parentFolderID, *folderItem.GetId()) + logger.Ctx(ctx).Debugw("resolved restore destination", + "dest_name", folder, + "parent", parentFolderID, + "dest_id", *folderItem.GetId()) + parentFolderID = *folderItem.GetId() } @@ -236,7 +243,7 @@ func restoreItem( } iReader := itemData.ToReader() - progReader, closer := observe.ItemProgress(iReader, observe.ItemRestoreMsg, itemName, ss.Size()) + progReader, closer := observe.ItemProgress(ctx, iReader, observe.ItemRestoreMsg, itemName, ss.Size()) go closer() diff --git a/src/internal/connector/sharepoint/collection.go b/src/internal/connector/sharepoint/collection.go index 14d0beb34..ff6af4132 100644 --- a/src/internal/connector/sharepoint/collection.go +++ b/src/internal/connector/sharepoint/collection.go @@ -156,7 +156,11 @@ func (sc *Collection) populate(ctx context.Context) { ) // TODO: Insert correct ID for CollectionProgress - colProgress, closer := observe.CollectionProgress("name", sc.fullPath.Category().String(), sc.fullPath.Folder()) + colProgress, closer := observe.CollectionProgress( + ctx, + "name", + sc.fullPath.Category().String(), + 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 76b0287b7..d7c6547a0 100644 --- a/src/internal/connector/sharepoint/data_collections.go +++ b/src/internal/connector/sharepoint/data_collections.go @@ -2,7 +2,6 @@ package sharepoint import ( "context" - "fmt" "github.com/pkg/errors" @@ -43,8 +42,8 @@ func DataCollections( ) for _, scope := range b.Scopes() { - foldersComplete, closer := observe.MessageWithCompletion(fmt.Sprintf( - "∙ %s - %s:", + foldersComplete, closer := observe.MessageWithCompletion(ctx, observe.Bulletf( + "%s - %s", scope.Category().PathType(), site)) defer closer() defer close(foldersComplete) diff --git a/src/internal/kopia/upload.go b/src/internal/kopia/upload.go index a64715fc3..6a99b9898 100644 --- a/src/internal/kopia/upload.go +++ b/src/internal/kopia/upload.go @@ -286,7 +286,6 @@ func collectionEntries( continue } - log.Debugw("reading item", "path", itemPath.String()) trace.Log(ctx, "kopia:streamEntries:item", itemPath.String()) if e.Deleted() { diff --git a/src/internal/observe/observe.go b/src/internal/observe/observe.go index 9dd9a68fa..6d648be5f 100644 --- a/src/internal/observe/observe.go +++ b/src/internal/observe/observe.go @@ -7,10 +7,13 @@ import ( "os" "sync" + "github.com/dustin/go-humanize" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8/decor" + + "github.com/alcionai/corso/src/pkg/logger" ) const ( @@ -127,15 +130,17 @@ func Complete() { } const ( - ItemBackupMsg = "Backing up item:" - ItemRestoreMsg = "Restoring item:" - ItemQueueMsg = "Queuing items:" + ItemBackupMsg = "Backing up item" + ItemRestoreMsg = "Restoring item" + ItemQueueMsg = "Queuing items" ) // Progress Updates // Message is used to display a progress message -func Message(message string) { +func Message(ctx context.Context, message string) { + logger.Ctx(ctx).Info(message) + if cfg.hidden() { return } @@ -153,12 +158,15 @@ func Message(message string) { // Complete the bar immediately bar.SetTotal(-1, true) - waitAndCloseBar(bar)() + waitAndCloseBar(bar, func() {})() } // MessageWithCompletion is used to display progress with a spinner // that switches to "done" when the completion channel is signalled -func MessageWithCompletion(message string) (chan<- struct{}, func()) { +func MessageWithCompletion(ctx context.Context, message string) (chan<- struct{}, func()) { + log := logger.Ctx(ctx) + log.Info(message) + completionCh := make(chan struct{}, 1) if cfg.hidden() { @@ -173,7 +181,7 @@ func MessageWithCompletion(message string) (chan<- struct{}, func()) { -1, mpb.SpinnerStyle(frames...).PositionLeft(), mpb.PrependDecorators( - decor.Name(message), + decor.Name(message+":"), decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 8}), ), mpb.BarFillerOnComplete("done"), @@ -192,7 +200,11 @@ func MessageWithCompletion(message string) (chan<- struct{}, func()) { } }(completionCh) - return completionCh, waitAndCloseBar(bar) + wacb := waitAndCloseBar(bar, func() { + log.Info("done - " + message) + }) + + return completionCh, wacb } // --------------------------------------------------------------------------- @@ -202,7 +214,15 @@ func MessageWithCompletion(message string) (chan<- struct{}, func()) { // ItemProgress tracks the display of an item in a folder by counting the bytes // read through the provided readcloser, up until the byte count matches // the totalBytes. -func ItemProgress(rc io.ReadCloser, header, iname string, totalBytes int64) (io.ReadCloser, func()) { +func ItemProgress( + ctx context.Context, + rc io.ReadCloser, + header, iname string, + totalBytes int64, +) (io.ReadCloser, func()) { + log := logger.Ctx(ctx).With("item", iname, "size", humanize.Bytes(uint64(totalBytes))) + log.Debug(header) + if cfg.hidden() || rc == nil || totalBytes == 0 { return rc, func() {} } @@ -224,14 +244,23 @@ func ItemProgress(rc io.ReadCloser, header, iname string, totalBytes int64) (io. bar := progress.New(totalBytes, mpb.NopStyle(), barOpts...) - return bar.ProxyReader(rc), waitAndCloseBar(bar) + wacb := waitAndCloseBar(bar, func() { + // might be overly chatty, we can remove if needed. + log.Debug("done - " + header) + }) + + return bar.ProxyReader(rc), wacb } // ProgressWithCount tracks the display of a bar that tracks the completion // 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(header, message string, count int64) (chan<- struct{}, func()) { +func ProgressWithCount(ctx context.Context, header, message string, count int64) (chan<- struct{}, func()) { + log := logger.Ctx(ctx) + lmsg := fmt.Sprintf("%s %s - %d", header, message, count) + log.Info(lmsg) + progressCh := make(chan struct{}) if cfg.hidden() { @@ -282,7 +311,11 @@ func ProgressWithCount(header, message string, count int64) (chan<- struct{}, fu } }(ch) - return ch, waitAndCloseBar(bar) + wacb := waitAndCloseBar(bar, func() { + log.Info("done - " + lmsg) + }) + + return ch, wacb } // --------------------------------------------------------------------------- @@ -320,7 +353,14 @@ func makeSpinFrames(barWidth int) { // CollectionProgress tracks the display a spinner that idles while the collection // incrementing the count of items handled. Each write to the provided channel // counts as a single increment. The caller is expected to close the channel. -func CollectionProgress(user, category, dirName string) (chan<- struct{}, func()) { +func CollectionProgress( + ctx context.Context, + user, category, dirName string, +) (chan<- struct{}, func()) { + log := logger.Ctx(ctx).With("user", user, "category", category, "dir", dirName) + message := "Collecting " + dirName + log.Info(message) + if cfg.hidden() || len(user) == 0 || len(dirName) == 0 { ch := make(chan struct{}) @@ -357,6 +397,8 @@ func CollectionProgress(user, category, dirName string) (chan<- struct{}, func() barOpts..., ) + var counted int + ch := make(chan struct{}) go func(ci <-chan struct{}) { for { @@ -371,17 +413,34 @@ func CollectionProgress(user, category, dirName string) (chan<- struct{}, func() return } + counted++ + bar.Increment() } } }(ch) - return ch, waitAndCloseBar(bar) + wacb := waitAndCloseBar(bar, func() { + log.Infow("done - "+message, "count", counted) + }) + + return ch, wacb } -func waitAndCloseBar(bar *mpb.Bar) func() { +func waitAndCloseBar(bar *mpb.Bar, log func()) func() { return func() { bar.Wait() wg.Done() + log() } } + +// --------------------------------------------------------------------------- +// 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...) +} diff --git a/src/internal/observe/observe_test.go b/src/internal/observe/observe_test.go index 96809a235..681cbeaf5 100644 --- a/src/internal/observe/observe_test.go +++ b/src/internal/observe/observe_test.go @@ -44,6 +44,7 @@ func (suite *ObserveProgressUnitSuite) TestItemProgress() { from := make([]byte, 100) prog, closer := observe.ItemProgress( + ctx, io.NopCloser(bytes.NewReader(from)), "folder", "test", @@ -96,7 +97,7 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnCtxCancel observe.SeedWriter(context.Background(), nil, nil) }() - progCh, closer := observe.CollectionProgress("test", "testcat", "testertons") + progCh, closer := observe.CollectionProgress(ctx, "test", "testcat", "testertons") require.NotNil(t, progCh) require.NotNil(t, closer) @@ -131,7 +132,7 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnChannelCl observe.SeedWriter(context.Background(), nil, nil) }() - progCh, closer := observe.CollectionProgress("test", "testcat", "testertons") + progCh, closer := observe.CollectionProgress(ctx, "test", "testcat", "testertons") require.NotNil(t, progCh) require.NotNil(t, closer) @@ -163,7 +164,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgress() { message := "Test Message" - observe.Message(message) + observe.Message(ctx, message) observe.Complete() require.NotEmpty(suite.T(), recorder.String()) require.Contains(suite.T(), recorder.String(), message) @@ -184,7 +185,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithCompletion() { message := "Test Message" - ch, closer := observe.MessageWithCompletion(message) + ch, closer := observe.MessageWithCompletion(ctx, message) // Trigger completion ch <- struct{}{} @@ -214,7 +215,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithChannelClosed() { message := "Test Message" - ch, closer := observe.MessageWithCompletion(message) + ch, closer := observe.MessageWithCompletion(ctx, message) // Close channel without completing close(ch) @@ -246,7 +247,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithContextCancelled() message := "Test Message" - _, closer := observe.MessageWithCompletion(message) + _, closer := observe.MessageWithCompletion(ctx, message) // cancel context cancel() @@ -277,7 +278,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithCount() { message := "Test Message" count := 3 - ch, closer := observe.ProgressWithCount(header, message, int64(count)) + ch, closer := observe.ProgressWithCount(ctx, header, message, int64(count)) for i := 0; i < count; i++ { ch <- struct{}{} @@ -310,7 +311,7 @@ func (suite *ObserveProgressUnitSuite) TestObserveProgressWithCountChannelClosed message := "Test Message" count := 3 - ch, closer := observe.ProgressWithCount(header, message, int64(count)) + ch, closer := observe.ProgressWithCount(ctx, header, message, int64(count)) close(ch) diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index f2e68aec5..3a35eb349 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -238,7 +238,7 @@ func produceBackupDataCollections( metadata []data.Collection, ctrlOpts control.Options, ) ([]data.Collection, error) { - complete, closer := observe.MessageWithCompletion("Discovering items to backup:") + complete, closer := observe.MessageWithCompletion(ctx, "Discovering items to backup") defer func() { complete <- struct{}{} close(complete) @@ -492,7 +492,7 @@ func consumeBackupDataCollections( backupID model.StableID, isIncremental bool, ) (*kopia.BackupStats, *details.Builder, map[string]path.Path, error) { - complete, closer := observe.MessageWithCompletion("Backing up data:") + complete, closer := observe.MessageWithCompletion(ctx, "Backing up data") defer func() { complete <- struct{}{} close(complete) diff --git a/src/internal/operations/operation.go b/src/internal/operations/operation.go index c068d888e..30770bdf5 100644 --- a/src/internal/operations/operation.go +++ b/src/internal/operations/operation.go @@ -94,7 +94,7 @@ func connectToM365( sel selectors.Selector, acct account.Account, ) (*connector.GraphConnector, error) { - complete, closer := observe.MessageWithCompletion("Connecting to M365:") + complete, closer := observe.MessageWithCompletion(ctx, "Connecting to M365") defer func() { complete <- struct{}{} close(complete) diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index b4713f57c..f7505fa7d 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -159,9 +159,9 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De return nil, err } - observe.Message(fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID)) + observe.Message(ctx, fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID)) - kopiaComplete, closer := observe.MessageWithCompletion("Enumerating items in repository:") + kopiaComplete, closer := observe.MessageWithCompletion(ctx, "Enumerating items in repository") defer closer() defer close(kopiaComplete) @@ -183,7 +183,7 @@ func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.De return nil, opStats.readErr } - restoreComplete, closer := observe.MessageWithCompletion("Restoring data:") + restoreComplete, closer := observe.MessageWithCompletion(ctx, "Restoring data") defer closer() defer close(restoreComplete) diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index e7d2a3c56..f8559759f 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("Connecting to repository:") + complete, closer := observe.MessageWithCompletion(ctx, "Connecting to repository") defer closer() defer close(complete) From 8d798d6024d1153fe688772f06146375008e0754 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Fri, 13 Jan 2023 12:11:25 -0800 Subject: [PATCH 26/38] Switch to "delete wins" merge strategy for Exchange IDs (#2142) ## Description #2116 implemented a "last change wins" strategy for deduping IDs. However, it appears Graph API doesn't really enforce an ordering for returned items so that strategy may not work. Instead, implement a "delete wins" strategy. Items that are added, deleted, and then restored in M365 will be restored with a different ID This PR does the following: * reverts changes to return a unified list of IDs and added/removed status * switches to a "delete wins" merge strategy for duplicate IDs * updates tests ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * closes #1954 ## Test Plan - [ ] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/connector/exchange/api/api.go | 9 - .../connector/exchange/api/contacts.go | 18 +- src/internal/connector/exchange/api/events.go | 12 +- src/internal/connector/exchange/api/mail.go | 18 +- src/internal/connector/exchange/api/shared.go | 27 +-- .../connector/exchange/service_iterators.go | 26 +- .../exchange/service_iterators_test.go | 225 ++++++++---------- .../operations/backup_integration_test.go | 24 +- 8 files changed, 148 insertions(+), 211 deletions(-) diff --git a/src/internal/connector/exchange/api/api.go b/src/internal/connector/exchange/api/api.go index 999eb6c98..3fd15409f 100644 --- a/src/internal/connector/exchange/api/api.go +++ b/src/internal/connector/exchange/api/api.go @@ -24,15 +24,6 @@ type DeltaUpdate struct { Reset bool } -// DeltaResult contains the ID and whether the item referenced by the ID was -// deleted. This allows functions that fetch items for a folder to return a -// single consolidated stream which is easier to dedupe as the order between -// add/update and delete operations is known. -type DeltaResult struct { - ID string - Deleted bool -} - // GraphQuery represents functions which perform exchange-specific queries // into M365 backstore. Responses -> returned items will only contain the information // that is included in the options diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index 9a2b3c3dd..ab41ff4b3 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -179,10 +179,10 @@ func (p *contactPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (c Contacts) GetAddedAndRemovedItemIDs( ctx context.Context, user, directoryID, oldDelta string, -) ([]DeltaResult, DeltaUpdate, error) { +) ([]string, []string, DeltaUpdate, error) { service, err := c.service() if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } var ( @@ -192,22 +192,22 @@ func (c Contacts) GetAddedAndRemovedItemIDs( options, err := optionsForContactFoldersItemDelta([]string{"parentFolderId"}) if err != nil { - return nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") + return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") } if len(oldDelta) > 0 { builder := users.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, service.Adapter()) pgr := &contactPager{service, builder, options} - items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) // note: happy path, not the error condition if err == nil { - return items, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() + return added, removed, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() } // 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 { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } resetDelta = true @@ -217,10 +217,10 @@ func (c Contacts) GetAddedAndRemovedItemIDs( builder := service.Client().UsersById(user).ContactFoldersById(directoryID).Contacts().Delta() pgr := &contactPager{service, builder, options} - items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } - return items, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() + return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() } diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index a9065a686..bd37a361a 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -165,29 +165,29 @@ func (p *eventPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (c Events) GetAddedAndRemovedItemIDs( ctx context.Context, user, calendarID, oldDelta string, -) ([]DeltaResult, DeltaUpdate, error) { +) ([]string, []string, DeltaUpdate, error) { service, err := c.service() if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } var errs *multierror.Error options, err := optionsForEventsByCalendar([]string{"id"}) if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } builder := service.Client().UsersById(user).CalendarsById(calendarID).Events() pgr := &eventPager{service, builder, options} - items, _, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + added, _, _, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } // Events don't have a delta endpoint so just return an empty string. - return items, DeltaUpdate{}, errs.ErrorOrNil() + return added, nil, DeltaUpdate{}, errs.ErrorOrNil() } // --------------------------------------------------------------------------- diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index 9c6b34155..bf6739384 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -177,10 +177,10 @@ func (p *mailPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) { func (c Mail) GetAddedAndRemovedItemIDs( ctx context.Context, user, directoryID, oldDelta string, -) ([]DeltaResult, DeltaUpdate, error) { +) ([]string, []string, DeltaUpdate, error) { service, err := c.service() if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } var ( @@ -191,22 +191,22 @@ func (c Mail) GetAddedAndRemovedItemIDs( options, err := optionsForFolderMessagesDelta([]string{"isRead"}) if err != nil { - return nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") + return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options") } if len(oldDelta) > 0 { builder := users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, service.Adapter()) pgr := &mailPager{service, builder, options} - items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) // note: happy path, not the error condition if err == nil { - return items, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() + return added, removed, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil() } // 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 { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } resetDelta = true @@ -216,10 +216,10 @@ func (c Mail) GetAddedAndRemovedItemIDs( builder := service.Client().UsersById(user).MailFoldersById(directoryID).Messages().Delta() pgr := &mailPager{service, builder, options} - items, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) + added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr) if err != nil { - return nil, DeltaUpdate{}, err + return nil, nil, DeltaUpdate{}, err } - return items, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() + return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() } diff --git a/src/internal/connector/exchange/api/shared.go b/src/internal/connector/exchange/api/shared.go index dbb1d13fc..c77e21fa8 100644 --- a/src/internal/connector/exchange/api/shared.go +++ b/src/internal/connector/exchange/api/shared.go @@ -61,9 +61,10 @@ func toValues[T any](a any) ([]getIDAndAddtler, error) { func getItemsAddedAndRemovedFromContainer( ctx context.Context, pager itemPager, -) ([]DeltaResult, string, error) { +) ([]string, []string, string, error) { var ( - foundItems = []DeltaResult{} + addedIDs = []string{} + removedIDs = []string{} deltaURL string ) @@ -72,37 +73,33 @@ func getItemsAddedAndRemovedFromContainer( resp, err := pager.getPage(ctx) if err != nil { if err := graph.IsErrDeletedInFlight(err); err != nil { - return nil, deltaURL, err + return nil, nil, deltaURL, err } if err := graph.IsErrInvalidDelta(err); err != nil { - return nil, deltaURL, err + return nil, nil, deltaURL, err } - return nil, deltaURL, errors.Wrap(err, support.ConnectorStackErrorTrace(err)) + return nil, nil, deltaURL, errors.Wrap(err, support.ConnectorStackErrorTrace(err)) } // each category type responds with a different interface, but all // of them comply with GetValue, which is where we'll get our item data. items, err := pager.valuesIn(resp) if err != nil { - return nil, "", err + return nil, nil, "", err } // iterate through the items in the page for _, item := range items { - newItem := DeltaResult{ - ID: *item.GetId(), - } - // if the additional data conains a `@removed` key, the value will either // be 'changed' or 'deleted'. We don't really care about the cause: both // cases are handled the same way in storage. - if item.GetAdditionalData()[graph.AddtlDataRemoved] != nil { - newItem.Deleted = true + if item.GetAdditionalData()[graph.AddtlDataRemoved] == nil { + addedIDs = append(addedIDs, *item.GetId()) + } else { + removedIDs = append(removedIDs, *item.GetId()) } - - foundItems = append(foundItems, newItem) } // the deltaLink is kind of like a cursor for overall data state. @@ -125,5 +122,5 @@ func getItemsAddedAndRemovedFromContainer( pager.setNext(*nextLink) } - return foundItems, deltaURL, nil + return addedIDs, removedIDs, deltaURL, nil } diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index b893092b9..70f2190c5 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -19,7 +19,7 @@ type addedAndRemovedItemIDsGetter interface { GetAddedAndRemovedItemIDs( ctx context.Context, user, containerID, oldDeltaToken string, - ) ([]api.DeltaResult, api.DeltaUpdate, error) + ) ([]string, []string, api.DeltaUpdate, error) } // filterContainersAndFillCollections is a utility function @@ -93,7 +93,7 @@ func filterContainersAndFillCollections( } } - items, newDelta, err := getter.GetAddedAndRemovedItemIDs(ctx, qp.ResourceOwner, cID, prevDelta) + 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 { @@ -126,20 +126,16 @@ func filterContainersAndFillCollections( collections[cID] = &edc - // This results in "last one wins" if there's duplicate entries for an ID - // and some are deleted while some are added. - for _, i := range items { - m := edc.added - del := edc.removed + for _, add := range added { + edc.added[add] = struct{}{} + } - if i.Deleted { - m = edc.removed - del = edc.added - } - - m[i.ID] = struct{}{} - - delete(del, i.ID) + // Remove any deleted IDs from the set of added IDs because items that are + // deleted and then restored will have a different ID than they did + // originally. + for _, remove := range removed { + delete(edc.added, remove) + edc.removed[remove] = struct{}{} } // add the current path for the container ID to be used in the next backup diff --git a/src/internal/connector/exchange/service_iterators_test.go b/src/internal/connector/exchange/service_iterators_test.go index 2c1329c2f..e1872b55c 100644 --- a/src/internal/connector/exchange/service_iterators_test.go +++ b/src/internal/connector/exchange/service_iterators_test.go @@ -30,7 +30,8 @@ var _ addedAndRemovedItemIDsGetter = &mockGetter{} type ( mockGetter map[string]mockGetterResults mockGetterResults struct { - items []api.DeltaResult + added []string + removed []string newDelta api.DeltaUpdate err error } @@ -40,16 +41,17 @@ func (mg mockGetter) GetAddedAndRemovedItemIDs( ctx context.Context, userID, cID, prevDelta string, ) ( - []api.DeltaResult, + []string, + []string, api.DeltaUpdate, error, ) { results, ok := mg[cID] if !ok { - return nil, api.DeltaUpdate{}, errors.New("mock not found for " + cID) + return nil, nil, api.DeltaUpdate{}, errors.New("mock not found for " + cID) } - return results.items, results.newDelta, results.err + return results.added, results.removed, results.newDelta, results.err } var _ graph.ContainerResolver = &mockResolver{} @@ -110,25 +112,20 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections() { statusUpdater = func(*support.ConnectorOperationStatus) {} allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] dps = DeltaPaths{} // incrementals are tested separately - getterItems = []api.DeltaResult{ - {ID: "a1"}, - {ID: "a2"}, - {ID: "a3"}, - {ID: "r1", Deleted: true}, - {ID: "r2", Deleted: true}, - {ID: "r3", Deleted: true}, - } - commonResult = mockGetterResults{ - items: getterItems, + commonResult = mockGetterResults{ + added: []string{"a1", "a2", "a3"}, + removed: []string{"r1", "r2", "r3"}, newDelta: api.DeltaUpdate{URL: "delta_url"}, } errorResult = mockGetterResults{ - items: getterItems, + added: []string{"a1", "a2", "a3"}, + removed: []string{"r1", "r2", "r3"}, newDelta: api.DeltaUpdate{URL: "delta_url"}, err: assert.AnError, } deletedInFlightResult = mockGetterResults{ - items: getterItems, + added: []string{"a1", "a2", "a3"}, + removed: []string{"r1", "r2", "r3"}, newDelta: api.DeltaUpdate{URL: "delta_url"}, err: graph.ErrDeletedInFlight{Err: *common.EncapsulateError(assert.AnError)}, } @@ -336,136 +333,81 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections() { exColl, ok := coll.(*Collection) require.True(t, ok, "collection is an *exchange.Collection") - expectAdded := map[string]struct{}{} - expectRemoved := map[string]struct{}{} + ids := [][]string{ + make([]string, 0, len(exColl.added)), + make([]string, 0, len(exColl.removed)), + } - for _, i := range expect.items { - if i.Deleted { - expectRemoved[i.ID] = struct{}{} - } else { - expectAdded[i.ID] = struct{}{} + for i, cIDs := range []map[string]struct{}{exColl.added, exColl.removed} { + for id := range cIDs { + ids[i] = append(ids[i], id) } } - assert.Equal(t, expectAdded, exColl.added, "added items") - assert.Equal(t, expectRemoved, exColl.removed, "removed items") + assert.ElementsMatch(t, expect.added, ids[0], "added items") + assert.ElementsMatch(t, expect.removed, ids[1], "removed items") } }) } } func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_repeatedItems() { - var ( - userID = "user_id" - qp = graph.QueryParams{ - Category: path.EmailCategory, // doesn't matter which one we use. - ResourceOwner: userID, - Credentials: suite.creds, - } - statusUpdater = func(*support.ConnectorOperationStatus) {} - allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] - dps = DeltaPaths{} // incrementals are tested separately - delta = api.DeltaUpdate{URL: "delta_url"} - container1 = mockContainer{ - id: strPtr("1"), - displayName: strPtr("display_name_1"), - p: path.Builder{}.Append("display_name_1"), - } - ) + newDelta := api.DeltaUpdate{URL: "delta_url"} table := []struct { - name string - getter mockGetter - resolver graph.ContainerResolver - scope selectors.ExchangeScope - failFast bool - expectErr assert.ErrorAssertionFunc - expectNewColls int - expectMetadataColls int - expectAdded map[string]struct{} - expectRemoved map[string]struct{} + name string + getter mockGetter + expectAdded map[string]struct{} + expectRemoved map[string]struct{} }{ { - name: "repeated add", + name: "repeated adds", getter: map[string]mockGetterResults{ "1": { - items: []api.DeltaResult{ - {ID: "a1"}, - {ID: "a1"}, - }, - newDelta: delta, + added: []string{"a1", "a2", "a3", "a1"}, + newDelta: newDelta, }, }, - resolver: newMockResolver(container1), - scope: allScope, - expectErr: assert.NoError, - expectNewColls: 1, - expectMetadataColls: 1, - expectAdded: map[string]struct{}{"a1": {}}, - // Avoid failures for nil map. + expectAdded: map[string]struct{}{ + "a1": {}, + "a2": {}, + "a3": {}, + }, expectRemoved: map[string]struct{}{}, }, { - name: "repeated remove", + name: "repeated removes", getter: map[string]mockGetterResults{ "1": { - items: []api.DeltaResult{ - {ID: "a1", Deleted: true}, - {ID: "a1", Deleted: true}, - }, - newDelta: delta, + removed: []string{"r1", "r2", "r3", "r1"}, + newDelta: newDelta, }, }, - resolver: newMockResolver(container1), - scope: allScope, - expectErr: assert.NoError, - expectNewColls: 1, - expectMetadataColls: 1, - expectAdded: map[string]struct{}{}, - expectRemoved: map[string]struct{}{"a1": {}}, + expectAdded: map[string]struct{}{}, + expectRemoved: map[string]struct{}{ + "r1": {}, + "r2": {}, + "r3": {}, + }, }, { - name: "interleaved, final remove", + name: "remove for same item wins", getter: map[string]mockGetterResults{ "1": { - items: []api.DeltaResult{ - {ID: "a1"}, - {ID: "a1", Deleted: true}, - {ID: "a1"}, - {ID: "a1", Deleted: true}, - }, - newDelta: delta, + added: []string{"i1", "a2", "a3"}, + removed: []string{"i1", "r2", "r3"}, + newDelta: newDelta, }, }, - resolver: newMockResolver(container1), - scope: allScope, - expectErr: assert.NoError, - expectNewColls: 1, - expectMetadataColls: 1, - expectAdded: map[string]struct{}{}, - expectRemoved: map[string]struct{}{"a1": {}}, - }, - { - name: "interleaved, final add", - getter: map[string]mockGetterResults{ - "1": { - items: []api.DeltaResult{ - {ID: "a1"}, - {ID: "a1", Deleted: true}, - {ID: "a1"}, - {ID: "a1", Deleted: true}, - {ID: "a1"}, - }, - newDelta: delta, - }, + expectAdded: map[string]struct{}{ + "a2": {}, + "a3": {}, + }, + expectRemoved: map[string]struct{}{ + "i1": {}, + "r2": {}, + "r3": {}, }, - resolver: newMockResolver(container1), - scope: allScope, - expectErr: assert.NoError, - expectNewColls: 1, - expectMetadataColls: 1, - expectAdded: map[string]struct{}{"a1": {}}, - expectRemoved: map[string]struct{}{}, }, } for _, test := range table { @@ -473,6 +415,24 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_repea ctx, flush := tester.NewContext() defer flush() + var ( + userID = "user_id" + qp = graph.QueryParams{ + Category: path.EmailCategory, // doesn't matter which one we use. + ResourceOwner: userID, + Credentials: suite.creds, + } + statusUpdater = func(*support.ConnectorOperationStatus) {} + allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] + dps = DeltaPaths{} // incrementals are tested separately + container1 = mockContainer{ + id: strPtr("1"), + displayName: strPtr("display_name_1"), + p: path.Builder{}.Append("display_name_1"), + } + resolver = newMockResolver(container1) + ) + collections := map[string]data.Collection{} err := filterContainersAndFillCollections( @@ -481,12 +441,12 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_repea test.getter, collections, statusUpdater, - test.resolver, - test.scope, + resolver, + allScope, dps, - control.Options{FailFast: test.failFast}, + control.Options{FailFast: true}, ) - test.expectErr(t, err) + require.NoError(t, err) // collection assertions @@ -508,17 +468,26 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_repea if c.DoNotMergeItems() { doNotMerges++ } - - exColl, ok := c.(*Collection) - require.True(t, ok, "collection is an *exchange.Collection") - - assert.Equal(t, test.expectAdded, exColl.added) - assert.Equal(t, test.expectRemoved, exColl.removed) } assert.Zero(t, deleteds, "deleted collections") - assert.Equal(t, test.expectMetadataColls, metadatas, "metadata collections") - assert.Equal(t, test.expectNewColls, news, "new collections") + assert.Equal(t, 1, news, "new collections") + assert.Equal(t, 1, metadatas, "metadata collections") + assert.Zero(t, doNotMerges, "doNotMerge collections") + + // items in collections assertions + for k := range test.getter { + coll := collections[k] + if !assert.NotNilf(t, coll, "missing collection for path %s", k) { + continue + } + + exColl, ok := coll.(*Collection) + require.True(t, ok, "collection is an *exchange.Collection") + + assert.Equal(t, test.expectAdded, exColl.added, "added items") + assert.Equal(t, test.expectRemoved, exColl.removed, "removed items") + } }) } } @@ -536,11 +505,11 @@ func (suite *ServiceIteratorsSuite) TestFilterContainersAndFillCollections_incre statusUpdater = func(*support.ConnectorOperationStatus) {} allScope = selectors.NewExchangeBackup(nil).MailFolders(selectors.Any())[0] commonResults = mockGetterResults{ - items: []api.DeltaResult{{ID: "added"}}, + added: []string{"added"}, newDelta: api.DeltaUpdate{URL: "new_delta_url"}, } expiredResults = mockGetterResults{ - items: []api.DeltaResult{{ID: "added"}}, + added: []string{"added"}, newDelta: api.DeltaUpdate{ URL: "new_delta_url", Reset: true, diff --git a/src/internal/operations/backup_integration_test.go b/src/internal/operations/backup_integration_test.go index 6a0a62fd2..5876cd331 100644 --- a/src/internal/operations/backup_integration_test.go +++ b/src/internal/operations/backup_integration_test.go @@ -941,35 +941,19 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() { switch category { case path.EmailCategory: - ids, _, err := ac.Mail().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") + ids, _, _, err := ac.Mail().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") require.NoError(t, err, "getting message ids") require.NotEmpty(t, ids, "message ids in folder") - var idx int - - for _, item := range ids { - if item.Deleted { - idx++ - } - } - - err = cli.MessagesById(ids[idx].ID).Delete(ctx, nil) + err = cli.MessagesById(ids[0]).Delete(ctx, nil) require.NoError(t, err, "deleting email item: %s", support.ConnectorStackErrorTrace(err)) case path.ContactsCategory: - ids, _, err := ac.Contacts().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") + ids, _, _, err := ac.Contacts().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "") require.NoError(t, err, "getting contact ids") require.NotEmpty(t, ids, "contact ids in folder") - var idx int - - for _, item := range ids { - if item.Deleted { - idx++ - } - } - - err = cli.ContactsById(ids[idx].ID).Delete(ctx, nil) + err = cli.ContactsById(ids[0]).Delete(ctx, nil) require.NoError(t, err, "deleting contact item: %s", support.ConnectorStackErrorTrace(err)) } } From e64d1328f5798c84a45642b36f01b11f73bc97c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 05:57:16 +0000 Subject: [PATCH 27/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.179=20to=201.44.180=20in=20/src=20(#?= =?UTF-8?q?2147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.179 to 1.44.180.
Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.180 (2023-01-13)

Service Client Updates

  • service/connect: Updates service API and documentation
  • service/ec2: Updates service documentation
    • Documentation updates for EC2.
  • service/outposts: Updates service API
  • service/resource-groups: Updates service API and documentation
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.179&new-version=1.44.180)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 68c469180..81863601a 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,7 +6,7 @@ replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.2023011220 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/aws/aws-sdk-go v1.44.179 + github.com/aws/aws-sdk-go v1.44.180 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 diff --git a/src/go.sum b/src/go.sum index 76e086bee..fb9d87835 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.179 h1:2mLZYSRc6awtjfD3XV+8NbuQWUVOo03/5VJ0tPenMJ0= -github.com/aws/aws-sdk-go v1.44.179/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= +github.com/aws/aws-sdk-go v1.44.180/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= From 2334548462edffd55a0e6b2766b8f8a4dac82154 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 08:44:05 +0000 Subject: [PATCH 28/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/du?= =?UTF-8?q?stin/go-humanize=20from=201.0.0=20to=201.0.1=20in=20/src=20(#21?= =?UTF-8?q?46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) from 1.0.0 to 1.0.1.
Commits
  • 9ec74ab Don't strip zeroes from numbers that don't have decimal points
  • 269a863 Fix TestVeryVeryBigBytes
  • 463a095 Add new SI and IEC prefixes: ronto, quecto, ronna, quetta
  • bd075d4 Fix staticcheck errors
  • d5090ed Add 3 more cases
  • 8c89973 Add more unit-test of Ordinal for confirmation
  • 249ff6c Add go.mod
  • afde56e Renew godoc url to pkg.go.dev from godoc.org
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/dustin/go-humanize&package-manager=go_modules&previous-version=1.0.0&new-version=1.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 81863601a..194d1bc06 100644 --- a/src/go.mod +++ b/src/go.mod @@ -65,7 +65,7 @@ require ( github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect github.com/cjlapao/common-go v0.0.37 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/src/go.sum b/src/go.sum index fb9d87835..277e9650f 100644 --- a/src/go.sum +++ b/src/go.sum @@ -95,8 +95,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= From 77b2cd604d69c0bbbf3f6a89e9cbd60b18b29010 Mon Sep 17 00:00:00 2001 From: Abin Simon Date: Wed, 18 Jan 2023 00:02:38 +0530 Subject: [PATCH 29/38] Fix ci test cleanup script (#2140) ## Description Purge script needs go 1.19 and ubuntu latest contains 1.18 as of now. Here is a sample run failing because of this: https://github.com/alcionai/corso/actions/runs/3909006130/jobs/6679661266 ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [x] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * # ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- .github/workflows/ci_test_cleanup.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_test_cleanup.yml b/.github/workflows/ci_test_cleanup.yml index 1cefbb282..41dc064c9 100644 --- a/.github/workflows/ci_test_cleanup.yml +++ b/.github/workflows/ci_test_cleanup.yml @@ -16,8 +16,11 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '1.19' - # sets the maximimum time to now-30m. + # sets the maximum time to now-30m. # CI test have a 10 minute timeout. # At 20 minutes ago, we should be safe from conflicts. # The additional 10 minutes is just to be good citizens. From 1ef300d6c2276d4bd807ff5f85ef1530bc1f6dc7 Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 17 Jan 2023 13:27:37 -0700 Subject: [PATCH 30/38] add item fetching and serialization to api (#2150) ## Description Adds the per item collection streaming calls to the api interface. Primarily migrates a "getItem" and a "serializeItem" acton into the api pkg, out from exchange_data_collection. Building an ExchangeInfo is now also housed in api. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #1996 ## Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/cmd/getM365/getItem.go | 103 +++---- src/internal/connector/exchange/api/api.go | 13 +- .../connector/exchange/api/contacts.go | 77 ++++- .../{contact_test.go => api/contacts_test.go} | 14 +- src/internal/connector/exchange/api/events.go | 146 ++++++++- .../{event_test.go => api/events_test.go} | 13 +- src/internal/connector/exchange/api/mail.go | 118 +++++++- .../{message_test.go => api/mail_test.go} | 17 +- src/internal/connector/exchange/contact.go | 36 --- src/internal/connector/exchange/event.go | 82 ------ .../exchange/exchange_data_collection.go | 278 ++---------------- .../exchange/exchange_data_collection_test.go | 21 +- src/internal/connector/exchange/message.go | 49 --- .../connector/exchange/service_iterators.go | 39 +-- .../connector/exchange/service_restore.go | 15 +- 15 files changed, 492 insertions(+), 529 deletions(-) rename src/internal/connector/exchange/{contact_test.go => api/contacts_test.go} (84%) rename src/internal/connector/exchange/{event_test.go => api/events_test.go} (96%) rename src/internal/connector/exchange/{message_test.go => api/mail_test.go} (90%) delete mode 100644 src/internal/connector/exchange/contact.go delete mode 100644 src/internal/connector/exchange/event.go delete mode 100644 src/internal/connector/exchange/message.go diff --git a/src/cmd/getM365/getItem.go b/src/cmd/getM365/getItem.go index 10648006a..9c2f8f135 100644 --- a/src/cmd/getM365/getItem.go +++ b/src/cmd/getM365/getItem.go @@ -5,11 +5,11 @@ package main import ( - "bytes" "context" "fmt" "os" + "github.com/microsoft/kiota-abstractions-go/serialization" kw "github.com/microsoft/kiota-serialization-json-go" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -18,12 +18,10 @@ import ( "github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector" - "github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange/api" "github.com/alcionai/corso/src/internal/connector/graph" - "github.com/alcionai/corso/src/internal/connector/support" - "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/credentials" "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" @@ -77,12 +75,12 @@ func handleGetCommand(cmd *cobra.Command, args []string) error { return nil } - gc, creds, err := getGC(ctx) + _, creds, err := getGC(ctx) if err != nil { return err } - err = runDisplayM365JSON(ctx, gc.Service, creds) + err = runDisplayM365JSON(ctx, creds, user, m365ID) if err != nil { return Only(ctx, errors.Wrapf(err, "unable to create mock from M365: %s", m365ID)) } @@ -92,13 +90,14 @@ func handleGetCommand(cmd *cobra.Command, args []string) error { func runDisplayM365JSON( ctx context.Context, - gs graph.Servicer, creds account.M365Config, + user, itemID string, ) error { var ( - get api.GraphRetrievalFunc - serializeFunc exchange.GraphSerializeFunc - cat = graph.StringToPathCategory(category) + bs []byte + err error + cat = graph.StringToPathCategory(category) + sw = kw.NewJsonSerializationWriter() ) ac, err := api.NewClient(creds) @@ -107,58 +106,60 @@ func runDisplayM365JSON( } switch cat { - case path.EmailCategory, path.EventsCategory, path.ContactsCategory: - get, serializeFunc = exchange.GetQueryAndSerializeFunc(ac, cat) + case path.EmailCategory: + bs, err = getItem(ctx, ac.Mail(), user, itemID) + case path.EventsCategory: + bs, err = getItem(ctx, ac.Events(), user, itemID) + case path.ContactsCategory: + bs, err = getItem(ctx, ac.Contacts(), user, itemID) default: return fmt.Errorf("unable to process category: %s", cat) } - channel := make(chan data.Stream, 1) - - sw := kw.NewJsonSerializationWriter() - - response, err := get(ctx, user, m365ID) - if err != nil { - return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) - } - - // First return is the number of bytes that were serialized. Ignored - _, err = serializeFunc(ctx, gs.Client(), sw, channel, response, user) - close(channel) - if err != nil { return err } - for item := range channel { - buf := &bytes.Buffer{} + str := string(bs) - _, err := buf.ReadFrom(item.ToReader()) - if err != nil { - return errors.Wrapf(err, "unable to parse given data: %s", m365ID) - } - - byteArray := buf.Bytes() - newValue := string(byteArray) - - err = sw.WriteStringValue("", &newValue) - if err != nil { - return errors.Wrapf(err, "unable to %s to string value", m365ID) - } - - array, err := sw.GetSerializedContent() - if err != nil { - return errors.Wrapf(err, "unable to serialize new value from M365:%s", m365ID) - } - - fmt.Println(string(array)) - - //lint:ignore SA4004 only expecting one item - return nil + err = sw.WriteStringValue("", &str) + if err != nil { + return errors.Wrapf(err, "unable to %s to string value", itemID) } - // This should never happen - return errors.New("m365 object not serialized") + array, err := sw.GetSerializedContent() + if err != nil { + return errors.Wrapf(err, "unable to serialize new value from M365:%s", itemID) + } + + fmt.Println(string(array)) + + return nil +} + +type itemer interface { + GetItem( + ctx context.Context, + user, itemID string, + ) (serialization.Parsable, *details.ExchangeInfo, error) + Serialize( + ctx context.Context, + item serialization.Parsable, + user, itemID string, + ) ([]byte, error) +} + +func getItem( + ctx context.Context, + itm itemer, + user, itemID string, +) ([]byte, error) { + sp, _, err := itm.GetItem(ctx, user, itemID) + if err != nil { + return nil, errors.Wrap(err, "getting item") + } + + return itm.Serialize(ctx, sp, user, itemID) } //------------------------------------------------------------------------------- diff --git a/src/internal/connector/exchange/api/api.go b/src/internal/connector/exchange/api/api.go index 3fd15409f..6edd68f57 100644 --- a/src/internal/connector/exchange/api/api.go +++ b/src/internal/connector/exchange/api/api.go @@ -2,6 +2,7 @@ package api import ( "context" + "time" "github.com/microsoft/kiota-abstractions-go/serialization" "github.com/pkg/errors" @@ -11,9 +12,11 @@ import ( ) // --------------------------------------------------------------------------- -// common types +// common types and consts // --------------------------------------------------------------------------- +const numberOfRetries = 3 + // DeltaUpdate holds the results of a current delta token. It normally // gets produced when aggregating the addition and removal of items in // a delta-queriable folder. @@ -106,3 +109,11 @@ func checkIDAndName(c graph.Container) error { return nil } + +func orNow(t *time.Time) time.Time { + if t == nil { + return time.Now().UTC() + } + + return *t +} diff --git a/src/internal/connector/exchange/api/contacts.go b/src/internal/connector/exchange/api/contacts.go index ab41ff4b3..e12f4b795 100644 --- a/src/internal/connector/exchange/api/contacts.go +++ b/src/internal/connector/exchange/api/contacts.go @@ -2,15 +2,19 @@ package api import ( "context" + "fmt" + "time" "github.com/hashicorp/go-multierror" "github.com/microsoft/kiota-abstractions-go/serialization" + kioser "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models" "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/support" + "github.com/alcionai/corso/src/pkg/backup/details" ) // --------------------------------------------------------------------------- @@ -52,12 +56,17 @@ func (c Contacts) DeleteContactFolder( return c.stable.Client().UsersById(user).ContactFoldersById(folderID).Delete(ctx, nil) } -// RetrieveContactDataForUser is a GraphRetrievalFun that returns all associated fields. -func (c Contacts) RetrieveContactDataForUser( +// GetItem retrieves a Contactable item. +func (c Contacts) GetItem( ctx context.Context, - user, m365ID string, -) (serialization.Parsable, error) { - return c.stable.Client().UsersById(user).ContactsById(m365ID).Get(ctx, nil) + user, itemID string, +) (serialization.Parsable, *details.ExchangeInfo, error) { + cont, err := c.stable.Client().UsersById(user).ContactsById(itemID).Get(ctx, nil) + if err != nil { + return nil, nil, err + } + + return cont, ContactInfo(cont), nil } // GetAllContactFolderNamesForUser is a GraphQuery function for getting @@ -224,3 +233,61 @@ func (c Contacts) GetAddedAndRemovedItemIDs( return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() } + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +// Serialize rserializes the item into a byte slice. +func (c Contacts) Serialize( + ctx context.Context, + item serialization.Parsable, + user, itemID string, +) ([]byte, error) { + contact, ok := item.(models.Contactable) + if !ok { + return nil, fmt.Errorf("expected Contactable, got %T", item) + } + + var ( + err error + writer = kioser.NewJsonSerializationWriter() + ) + + defer writer.Close() + + if err = writer.WriteObjectValue("", contact); err != nil { + return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) + } + + bs, err := writer.GetSerializedContent() + if err != nil { + return nil, errors.Wrap(err, "serializing contact") + } + + return bs, nil +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +func ContactInfo(contact models.Contactable) *details.ExchangeInfo { + name := "" + created := time.Time{} + + if contact.GetDisplayName() != nil { + name = *contact.GetDisplayName() + } + + if contact.GetCreatedDateTime() != nil { + created = *contact.GetCreatedDateTime() + } + + return &details.ExchangeInfo{ + ItemType: details.ExchangeContact, + ContactName: name, + Created: created, + Modified: orNow(contact.GetLastModifiedDateTime()), + } +} diff --git a/src/internal/connector/exchange/contact_test.go b/src/internal/connector/exchange/api/contacts_test.go similarity index 84% rename from src/internal/connector/exchange/contact_test.go rename to src/internal/connector/exchange/api/contacts_test.go index e01782520..411250146 100644 --- a/src/internal/connector/exchange/contact_test.go +++ b/src/internal/connector/exchange/api/contacts_test.go @@ -1,4 +1,4 @@ -package exchange +package api import ( "testing" @@ -11,15 +11,15 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" ) -type ContactSuite struct { +type ContactsAPIUnitSuite struct { suite.Suite } -func TestContactSuite(t *testing.T) { - suite.Run(t, &ContactSuite{}) +func TestContactsAPIUnitSuite(t *testing.T) { + suite.Run(t, new(ContactsAPIUnitSuite)) } -func (suite *ContactSuite) TestContactInfo() { +func (suite *ContactsAPIUnitSuite) TestContactInfo() { initial := time.Now() tests := []struct { @@ -37,7 +37,6 @@ func (suite *ContactSuite) TestContactInfo() { ItemType: details.ExchangeContact, Created: initial, Modified: initial, - Size: 10, } return contact, i }, @@ -54,7 +53,6 @@ func (suite *ContactSuite) TestContactInfo() { ContactName: aPerson, Created: initial, Modified: initial, - Size: 10, } return contact, i }, @@ -63,7 +61,7 @@ func (suite *ContactSuite) TestContactInfo() { for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { contact, expected := test.contactAndRP() - assert.Equal(t, expected, ContactInfo(contact, 10)) + assert.Equal(t, expected, ContactInfo(contact)) }) } } diff --git a/src/internal/connector/exchange/api/events.go b/src/internal/connector/exchange/api/events.go index bd37a361a..ba75cc648 100644 --- a/src/internal/connector/exchange/api/events.go +++ b/src/internal/connector/exchange/api/events.go @@ -2,15 +2,21 @@ package api import ( "context" + "fmt" + "time" "github.com/hashicorp/go-multierror" "github.com/microsoft/kiota-abstractions-go/serialization" + kioser "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/pkg/errors" + "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/path" ) @@ -52,12 +58,17 @@ func (c Events) DeleteCalendar( return c.stable.Client().UsersById(user).CalendarsById(calendarID).Delete(ctx, nil) } -// RetrieveEventDataForUser is a GraphRetrievalFunc that returns event data. -func (c Events) RetrieveEventDataForUser( +// GetItem retrieves an Eventable item. +func (c Events) GetItem( ctx context.Context, - user, m365ID string, -) (serialization.Parsable, error) { - return c.stable.Client().UsersById(user).EventsById(m365ID).Get(ctx, nil) + user, itemID string, +) (serialization.Parsable, *details.ExchangeInfo, error) { + evt, err := c.stable.Client().UsersById(user).EventsById(itemID).Get(ctx, nil) + if err != nil { + return nil, nil, err + } + + return evt, EventInfo(evt), nil } func (c Client) GetAllCalendarNamesForUser( @@ -190,6 +201,66 @@ func (c Events) GetAddedAndRemovedItemIDs( return added, nil, DeltaUpdate{}, errs.ErrorOrNil() } +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +// Serialize retrieves attachment data identified by the event item, and then +// serializes it into a byte slice. +func (c Events) Serialize( + ctx context.Context, + item serialization.Parsable, + user, itemID string, +) ([]byte, error) { + event, ok := item.(models.Eventable) + if !ok { + return nil, fmt.Errorf("expected Eventable, got %T", item) + } + + var ( + err error + writer = kioser.NewJsonSerializationWriter() + ) + + defer writer.Close() + + if *event.GetHasAttachments() { + // 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)) + } + + bs, err := writer.GetSerializedContent() + if err != nil { + return nil, errors.Wrap(err, "serializing calendar event") + } + + return bs, nil +} + // --------------------------------------------------------------------------- // helper funcs // --------------------------------------------------------------------------- @@ -216,3 +287,68 @@ func (c CalendarDisplayable) GetDisplayName() *string { func (c CalendarDisplayable) GetParentFolderId() *string { return nil } + +func EventInfo(evt models.Eventable) *details.ExchangeInfo { + var ( + organizer, subject string + recurs bool + start = time.Time{} + end = time.Time{} + created = time.Time{} + ) + + if evt.GetOrganizer() != nil && + evt.GetOrganizer().GetEmailAddress() != nil && + evt.GetOrganizer().GetEmailAddress().GetAddress() != nil { + organizer = *evt.GetOrganizer(). + GetEmailAddress(). + GetAddress() + } + + if evt.GetSubject() != nil { + subject = *evt.GetSubject() + } + + if evt.GetRecurrence() != nil { + recurs = true + } + + if evt.GetStart() != nil && + evt.GetStart().GetDateTime() != nil { + // timeString has 'Z' literal added to ensure the stored + // DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) + startTime := *evt.GetStart().GetDateTime() + "Z" + + output, err := common.ParseTime(startTime) + if err == nil { + start = output + } + } + + if evt.GetEnd() != nil && + evt.GetEnd().GetDateTime() != nil { + // timeString has 'Z' literal added to ensure the stored + // DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) + endTime := *evt.GetEnd().GetDateTime() + "Z" + + output, err := common.ParseTime(endTime) + if err == nil { + end = output + } + } + + if evt.GetCreatedDateTime() != nil { + created = *evt.GetCreatedDateTime() + } + + return &details.ExchangeInfo{ + ItemType: details.ExchangeEvent, + Organizer: organizer, + Subject: subject, + EventStart: start, + EventEnd: end, + EventRecurs: recurs, + Created: created, + Modified: orNow(evt.GetLastModifiedDateTime()), + } +} diff --git a/src/internal/connector/exchange/event_test.go b/src/internal/connector/exchange/api/events_test.go similarity index 96% rename from src/internal/connector/exchange/event_test.go rename to src/internal/connector/exchange/api/events_test.go index 386f451ab..a41a48e5a 100644 --- a/src/internal/connector/exchange/event_test.go +++ b/src/internal/connector/exchange/api/events_test.go @@ -1,4 +1,4 @@ -package exchange +package api import ( "testing" @@ -15,17 +15,17 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" ) -type EventSuite struct { +type EventsAPIUnitSuite struct { suite.Suite } -func TestEventSuite(t *testing.T) { - suite.Run(t, &EventSuite{}) +func TestEventsAPIUnitSuite(t *testing.T) { + suite.Run(t, new(EventsAPIUnitSuite)) } // TestEventInfo verifies that searchable event metadata // can be properly retrieved from a models.Eventable object -func (suite *EventSuite) TestEventInfo() { +func (suite *EventsAPIUnitSuite) TestEventInfo() { // Exchange stores start/end times in UTC and the below compares hours // directly so we need to "normalize" the timezone here. initial := time.Now().UTC() @@ -136,7 +136,6 @@ func (suite *EventSuite) TestEventInfo() { Organizer: organizer, EventStart: eventTime, EventEnd: eventEndTime, - Size: 10, } }, }, @@ -144,7 +143,7 @@ func (suite *EventSuite) TestEventInfo() { for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { event, expected := test.evtAndRP() - result := EventInfo(event, 10) + result := EventInfo(event) assert.Equal(t, expected.Subject, result.Subject, "subject") assert.Equal(t, expected.Sender, result.Sender, "sender") diff --git a/src/internal/connector/exchange/api/mail.go b/src/internal/connector/exchange/api/mail.go index bf6739384..b3f67ceb8 100644 --- a/src/internal/connector/exchange/api/mail.go +++ b/src/internal/connector/exchange/api/mail.go @@ -2,15 +2,20 @@ package api import ( "context" + "fmt" + "time" "github.com/hashicorp/go-multierror" "github.com/microsoft/kiota-abstractions-go/serialization" + kioser "github.com/microsoft/kiota-serialization-json-go" "github.com/microsoftgraph/msgraph-sdk-go/models" "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/support" + "github.com/alcionai/corso/src/pkg/backup/details" + "github.com/alcionai/corso/src/pkg/logger" ) // --------------------------------------------------------------------------- @@ -92,12 +97,17 @@ func (c Mail) GetContainerByID( return service.Client().UsersById(userID).MailFoldersById(dirID).Get(ctx, ofmf) } -// RetrieveMessageDataForUser is a GraphRetrievalFunc that returns message data. -func (c Mail) RetrieveMessageDataForUser( +// GetItem retrieves a Messageable item. +func (c Mail) GetItem( ctx context.Context, - user, m365ID string, -) (serialization.Parsable, error) { - return c.stable.Client().UsersById(user).MessagesById(m365ID).Get(ctx, nil) + user, itemID string, +) (serialization.Parsable, *details.ExchangeInfo, error) { + mail, err := c.stable.Client().UsersById(user).MessagesById(itemID).Get(ctx, nil) + if err != nil { + return nil, nil, err + } + + return mail, MailInfo(mail), nil } // EnumerateContainers iterates through all of the users current @@ -223,3 +233,101 @@ func (c Mail) GetAddedAndRemovedItemIDs( return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil() } + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +// Serialize retrieves attachment data identified by the mail item, and then +// serializes it into a byte slice. +func (c Mail) Serialize( + ctx context.Context, + item serialization.Parsable, + user, itemID string, +) ([]byte, error) { + msg, ok := item.(models.Messageable) + if !ok { + return nil, fmt.Errorf("expected Messageable, got %T", item) + } + + var ( + err error + writer = kioser.NewJsonSerializationWriter() + ) + + defer writer.Close() + + if *msg.GetHasAttachments() { + // 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, errors.Wrap(retriesErr, "attachment failed"), nil) + } + } + + if err = writer.WriteObjectValue("", msg); err != nil { + return nil, support.SetNonRecoverableError(errors.Wrap(err, itemID)) + } + + bs, err := writer.GetSerializedContent() + if err != nil { + return nil, errors.Wrap(err, "serializing email") + } + + return bs, nil +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +func MailInfo(msg models.Messageable) *details.ExchangeInfo { + sender := "" + subject := "" + received := time.Time{} + created := time.Time{} + + if msg.GetSender() != nil && + msg.GetSender().GetEmailAddress() != nil && + msg.GetSender().GetEmailAddress().GetAddress() != nil { + sender = *msg.GetSender().GetEmailAddress().GetAddress() + } + + if msg.GetSubject() != nil { + subject = *msg.GetSubject() + } + + if msg.GetReceivedDateTime() != nil { + received = *msg.GetReceivedDateTime() + } + + if msg.GetCreatedDateTime() != nil { + created = *msg.GetCreatedDateTime() + } + + return &details.ExchangeInfo{ + ItemType: details.ExchangeMail, + Sender: sender, + Subject: subject, + Received: received, + Created: created, + Modified: orNow(msg.GetLastModifiedDateTime()), + } +} diff --git a/src/internal/connector/exchange/message_test.go b/src/internal/connector/exchange/api/mail_test.go similarity index 90% rename from src/internal/connector/exchange/message_test.go rename to src/internal/connector/exchange/api/mail_test.go index 8b5751610..5611586e2 100644 --- a/src/internal/connector/exchange/message_test.go +++ b/src/internal/connector/exchange/api/mail_test.go @@ -1,4 +1,4 @@ -package exchange +package api import ( "testing" @@ -10,15 +10,15 @@ import ( "github.com/alcionai/corso/src/pkg/backup/details" ) -type MessageSuite struct { +type MailAPIUnitSuite struct { suite.Suite } -func TestMessageSuite(t *testing.T) { - suite.Run(t, &MessageSuite{}) +func TestMailAPIUnitSuite(t *testing.T) { + suite.Run(t, new(MailAPIUnitSuite)) } -func (suite *MessageSuite) TestMessageInfo() { +func (suite *MailAPIUnitSuite) TestMailInfo() { initial := time.Now() tests := []struct { @@ -36,7 +36,6 @@ func (suite *MessageSuite) TestMessageInfo() { ItemType: details.ExchangeMail, Created: initial, Modified: initial, - Size: 10, } return msg, i }, @@ -58,7 +57,6 @@ func (suite *MessageSuite) TestMessageInfo() { Sender: sender, Created: initial, Modified: initial, - Size: 10, } return msg, i }, @@ -76,7 +74,6 @@ func (suite *MessageSuite) TestMessageInfo() { Subject: subject, Created: initial, Modified: initial, - Size: 10, } return msg, i }, @@ -94,7 +91,6 @@ func (suite *MessageSuite) TestMessageInfo() { Received: now, Created: initial, Modified: initial, - Size: 10, } return msg, i }, @@ -122,7 +118,6 @@ func (suite *MessageSuite) TestMessageInfo() { Received: now, Created: initial, Modified: initial, - Size: 10, } return msg, i }, @@ -131,7 +126,7 @@ func (suite *MessageSuite) TestMessageInfo() { for _, tt := range tests { suite.T().Run(tt.name, func(t *testing.T) { msg, expected := tt.msgAndRP() - suite.Equal(expected, MessageInfo(msg, 10)) + suite.Equal(expected, MailInfo(msg)) }) } } diff --git a/src/internal/connector/exchange/contact.go b/src/internal/connector/exchange/contact.go deleted file mode 100644 index 82bac9601..000000000 --- a/src/internal/connector/exchange/contact.go +++ /dev/null @@ -1,36 +0,0 @@ -package exchange - -import ( - "time" - - "github.com/microsoftgraph/msgraph-sdk-go/models" - - "github.com/alcionai/corso/src/pkg/backup/details" -) - -// ContactInfo translate models.Contactable metadata into searchable content -func ContactInfo(contact models.Contactable, size int64) *details.ExchangeInfo { - name := "" - created := time.Time{} - modified := time.Time{} - - if contact.GetDisplayName() != nil { - name = *contact.GetDisplayName() - } - - if contact.GetCreatedDateTime() != nil { - created = *contact.GetCreatedDateTime() - } - - if contact.GetLastModifiedDateTime() != nil { - modified = *contact.GetLastModifiedDateTime() - } - - return &details.ExchangeInfo{ - ItemType: details.ExchangeContact, - ContactName: name, - Created: created, - Modified: modified, - Size: size, - } -} diff --git a/src/internal/connector/exchange/event.go b/src/internal/connector/exchange/event.go deleted file mode 100644 index 775570d52..000000000 --- a/src/internal/connector/exchange/event.go +++ /dev/null @@ -1,82 +0,0 @@ -package exchange - -import ( - "time" - - "github.com/microsoftgraph/msgraph-sdk-go/models" - - "github.com/alcionai/corso/src/internal/common" - "github.com/alcionai/corso/src/pkg/backup/details" -) - -// EventInfo searchable metadata for stored event objects. -func EventInfo(evt models.Eventable, size int64) *details.ExchangeInfo { - var ( - organizer, subject string - recurs bool - start = time.Time{} - end = time.Time{} - created = time.Time{} - modified = time.Time{} - ) - - if evt.GetOrganizer() != nil && - evt.GetOrganizer().GetEmailAddress() != nil && - evt.GetOrganizer().GetEmailAddress().GetAddress() != nil { - organizer = *evt.GetOrganizer(). - GetEmailAddress(). - GetAddress() - } - - if evt.GetSubject() != nil { - subject = *evt.GetSubject() - } - - if evt.GetRecurrence() != nil { - recurs = true - } - - if evt.GetStart() != nil && - evt.GetStart().GetDateTime() != nil { - // timeString has 'Z' literal added to ensure the stored - // DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) - startTime := *evt.GetStart().GetDateTime() + "Z" - - output, err := common.ParseTime(startTime) - if err == nil { - start = output - } - } - - if evt.GetEnd() != nil && - evt.GetEnd().GetDateTime() != nil { - // timeString has 'Z' literal added to ensure the stored - // DateTime is not: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) - endTime := *evt.GetEnd().GetDateTime() + "Z" - - output, err := common.ParseTime(endTime) - if err == nil { - end = output - } - } - - if evt.GetCreatedDateTime() != nil { - created = *evt.GetCreatedDateTime() - } - - if evt.GetLastModifiedDateTime() != nil { - modified = *evt.GetLastModifiedDateTime() - } - - return &details.ExchangeInfo{ - ItemType: details.ExchangeEvent, - Organizer: organizer, - Subject: subject, - EventStart: start, - EventEnd: end, - EventRecurs: recurs, - Created: created, - Modified: modified, - Size: size, - } -} diff --git a/src/internal/connector/exchange/exchange_data_collection.go b/src/internal/connector/exchange/exchange_data_collection.go index 7fc3faadd..950cf7aaf 100644 --- a/src/internal/connector/exchange/exchange_data_collection.go +++ b/src/internal/connector/exchange/exchange_data_collection.go @@ -6,20 +6,13 @@ package exchange import ( "bytes" "context" - "fmt" "io" "sync" "sync/atomic" "time" - absser "github.com/microsoft/kiota-abstractions-go/serialization" - kioser "github.com/microsoft/kiota-serialization-json-go" - msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" - "github.com/microsoftgraph/msgraph-sdk-go/models" - "github.com/pkg/errors" + "github.com/microsoft/kiota-abstractions-go/serialization" - "github.com/alcionai/corso/src/internal/connector/exchange/api" - "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/observe" @@ -45,6 +38,18 @@ const ( urlPrefetchChannelBufferSize = 4 ) +type itemer interface { + GetItem( + ctx context.Context, + user, itemID string, + ) (serialization.Parsable, *details.ExchangeInfo, error) + Serialize( + ctx context.Context, + item serialization.Parsable, + user, itemID string, + ) ([]byte, error) +} + // Collection implements the interface from data.Collection // Structure holds data for an Exchange application for a single user type Collection struct { @@ -57,9 +62,7 @@ type Collection struct { // removed is a list of item IDs that were deleted from, or moved out, of a container removed map[string]struct{} - // service - client/adapter pair used to access M365 back store - service graph.Servicer - ac api.Client + items itemer category path.CategoryType statusUpdater support.StatusUpdater @@ -89,14 +92,12 @@ func NewCollection( user string, curr, prev path.Path, category path.CategoryType, - ac api.Client, - service graph.Servicer, + items itemer, statusUpdater support.StatusUpdater, ctrlOpts control.Options, doNotMergeItems bool, ) Collection { collection := Collection{ - ac: ac, category: category, ctrl: ctrlOpts, data: make(chan data.Stream, collectionChannelBufferSize), @@ -105,10 +106,10 @@ func NewCollection( added: make(map[string]struct{}, 0), removed: make(map[string]struct{}, 0), prevPath: prev, - service: service, state: stateOf(prev, curr), statusUpdater: statusUpdater, user: user, + items: items, } return collection @@ -137,22 +138,6 @@ func (col *Collection) Items() <-chan data.Stream { return col.data } -// GetQueryAndSerializeFunc helper function that returns the two functions functions -// required to convert M365 identifier into a byte array filled with the serialized data -func GetQueryAndSerializeFunc(ac api.Client, category path.CategoryType) (api.GraphRetrievalFunc, GraphSerializeFunc) { - switch category { - case path.ContactsCategory: - return ac.Contacts().RetrieveContactDataForUser, serializeAndStreamContact - case path.EventsCategory: - return ac.Events().RetrieveEventDataForUser, serializeAndStreamEvent - case path.EmailCategory: - return ac.Mail().RetrieveMessageDataForUser, serializeAndStreamMessage - // Unsupported options returns nil, nil - default: - return nil, nil - } -} - // FullPath returns the Collection's fullPath []string func (col *Collection) FullPath() path.Path { return col.fullPath @@ -208,15 +193,6 @@ func (col *Collection) streamItems(ctx context.Context) { }() } - // get QueryBasedonIdentifier - // verify that it is the correct type in called function - // serializationFunction - query, serializeFunc := GetQueryAndSerializeFunc(col.ac, col.category) - if query == nil { - errs = fmt.Errorf("unrecognized collection type: %s", col.category) - return - } - // Limit the max number of active requests to GC semaphoreCh := make(chan struct{}, urlPrefetchChannelBufferSize) defer close(semaphoreCh) @@ -265,16 +241,17 @@ func (col *Collection) streamItems(ctx context.Context) { defer func() { <-semaphoreCh }() var ( - response absser.Parsable - err error + item serialization.Parsable + info *details.ExchangeInfo + err error ) for i := 1; i <= numberOfRetries; i++ { - response, err = query(ctx, user, id) + item, info, err = col.items.GetItem(ctx, user, id) if err == nil { break } - // TODO: Tweak sleep times + if i < numberOfRetries { time.Sleep(time.Duration(3*(i+1)) * time.Second) } @@ -285,20 +262,23 @@ func (col *Collection) streamItems(ctx context.Context) { return } - byteCount, err := serializeFunc( - ctx, - col.service.Client(), - kioser.NewJsonSerializationWriter(), - col.data, - response, - user) + data, err := col.items.Serialize(ctx, item, user, id) if err != nil { errUpdater(user, err) return } + info.Size = int64(len(data)) + + col.data <- &Stream{ + id: id, + message: data, + info: info, + modTime: info.Modified, + } + atomic.AddInt64(&success, 1) - atomic.AddInt64(&totalBytes, int64(byteCount)) + atomic.AddInt64(&totalBytes, info.Size) if colProgress != nil { colProgress <- struct{}{} @@ -328,200 +308,6 @@ func (col *Collection) finishPopulation(ctx context.Context, success int, totalB col.statusUpdater(status) } -type modTimer interface { - GetLastModifiedDateTime() *time.Time -} - -func getModTime(mt modTimer) time.Time { - res := time.Now().UTC() - - if t := mt.GetLastModifiedDateTime(); t != nil { - res = *t - } - - return res -} - -// GraphSerializeFunc are class of functions that are used by Collections to transform GraphRetrievalFunc -// responses into data.Stream items contained within the Collection -type GraphSerializeFunc func( - ctx context.Context, - client *msgraphsdk.GraphServiceClient, - objectWriter *kioser.JsonSerializationWriter, - dataChannel chan<- data.Stream, - parsable absser.Parsable, - user string, -) (int, error) - -// serializeAndStreamEvent is a GraphSerializeFunc used to serialize models.Eventable objects into -// data.Stream objects. Returns an error the process finishes unsuccessfully. -func serializeAndStreamEvent( - ctx context.Context, - client *msgraphsdk.GraphServiceClient, - objectWriter *kioser.JsonSerializationWriter, - dataChannel chan<- data.Stream, - parsable absser.Parsable, - user string, -) (int, error) { - var err error - - defer objectWriter.Close() - - event, ok := parsable.(models.Eventable) - if !ok { - return 0, fmt.Errorf("expected Eventable, got %T", parsable) - } - - if *event.GetHasAttachments() { - var retriesErr error - - for count := 0; count < numberOfRetries; count++ { - attached, err := client. - UsersById(user). - EventsById(*event.GetId()). - Attachments(). - Get(ctx, nil) - retriesErr = err - - if err == nil && attached != nil { - event.SetAttachments(attached.GetValue()) - break - } - } - - if retriesErr != nil { - logger.Ctx(ctx).Debug("exceeded maximum retries") - - return 0, support.WrapAndAppend( - *event.GetId(), - errors.Wrap(retriesErr, "attachment failed"), - nil) - } - } - - err = objectWriter.WriteObjectValue("", event) - if err != nil { - return 0, support.SetNonRecoverableError(errors.Wrap(err, *event.GetId())) - } - - byteArray, err := objectWriter.GetSerializedContent() - if err != nil { - return 0, support.WrapAndAppend(*event.GetId(), errors.Wrap(err, "serializing content"), nil) - } - - if len(byteArray) > 0 { - dataChannel <- &Stream{ - id: *event.GetId(), - message: byteArray, - info: EventInfo(event, int64(len(byteArray))), - modTime: getModTime(event), - } - } - - return len(byteArray), nil -} - -// serializeAndStreamContact is a GraphSerializeFunc for models.Contactable -func serializeAndStreamContact( - ctx context.Context, - client *msgraphsdk.GraphServiceClient, - objectWriter *kioser.JsonSerializationWriter, - dataChannel chan<- data.Stream, - parsable absser.Parsable, - user string, -) (int, error) { - defer objectWriter.Close() - - contact, ok := parsable.(models.Contactable) - if !ok { - return 0, fmt.Errorf("expected Contactable, got %T", parsable) - } - - err := objectWriter.WriteObjectValue("", contact) - if err != nil { - return 0, support.SetNonRecoverableError(errors.Wrap(err, *contact.GetId())) - } - - bs, err := objectWriter.GetSerializedContent() - if err != nil { - return 0, support.WrapAndAppend(*contact.GetId(), err, nil) - } - - if len(bs) > 0 { - dataChannel <- &Stream{ - id: *contact.GetId(), - message: bs, - info: ContactInfo(contact, int64(len(bs))), - modTime: getModTime(contact), - } - } - - return len(bs), nil -} - -// serializeAndStreamMessage is the GraphSerializeFunc for models.Messageable -func serializeAndStreamMessage( - ctx context.Context, - client *msgraphsdk.GraphServiceClient, - objectWriter *kioser.JsonSerializationWriter, - dataChannel chan<- data.Stream, - parsable absser.Parsable, - user string, -) (int, error) { - var err error - - defer objectWriter.Close() - - msg, ok := parsable.(models.Messageable) - if !ok { - return 0, fmt.Errorf("expected Messageable, got %T", parsable) - } - - if *msg.GetHasAttachments() { - // getting all the attachments might take a couple attempts due to filesize - var retriesErr error - - for count := 0; count < numberOfRetries; count++ { - attached, err := client. - UsersById(user). - MessagesById(*msg.GetId()). - 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 0, support.WrapAndAppend(*msg.GetId(), errors.Wrap(retriesErr, "attachment failed"), nil) - } - } - - err = objectWriter.WriteObjectValue("", msg) - if err != nil { - return 0, support.SetNonRecoverableError(errors.Wrapf(err, "%s", *msg.GetId())) - } - - bs, err := objectWriter.GetSerializedContent() - if err != nil { - err = support.WrapAndAppend(*msg.GetId(), errors.Wrap(err, "serializing mail content"), nil) - return 0, support.SetNonRecoverableError(err) - } - - dataChannel <- &Stream{ - id: *msg.GetId(), - message: bs, - info: MessageInfo(msg, int64(len(bs))), - modTime: getModTime(msg), - } - - return len(bs), nil -} - // Stream represents a single item retrieved from exchange type Stream struct { id string diff --git a/src/internal/connector/exchange/exchange_data_collection_test.go b/src/internal/connector/exchange/exchange_data_collection_test.go index 79efc59f4..a63a7caf8 100644 --- a/src/internal/connector/exchange/exchange_data_collection_test.go +++ b/src/internal/connector/exchange/exchange_data_collection_test.go @@ -2,18 +2,33 @@ package exchange import ( "bytes" + "context" "testing" + "github.com/microsoft/kiota-abstractions-go/serialization" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/alcionai/corso/src/internal/connector/exchange/api" "github.com/alcionai/corso/src/internal/data" + "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{} + +func (mi mockItemer) GetItem( + context.Context, + string, string, +) (serialization.Parsable, *details.ExchangeInfo, error) { + return nil, nil, nil +} + +func (mi mockItemer) Serialize(context.Context, serialization.Parsable, string, string) ([]byte, error) { + return nil, nil +} + type ExchangeDataCollectionSuite struct { suite.Suite } @@ -137,7 +152,9 @@ func (suite *ExchangeDataCollectionSuite) TestNewCollection_state() { c := NewCollection( "u", test.curr, test.prev, - 0, api.Client{}, nil, nil, control.Options{}, + 0, + mockItemer{}, nil, + control.Options{}, false) assert.Equal(t, test.expect, c.State()) }) diff --git a/src/internal/connector/exchange/message.go b/src/internal/connector/exchange/message.go deleted file mode 100644 index 9d1e065fb..000000000 --- a/src/internal/connector/exchange/message.go +++ /dev/null @@ -1,49 +0,0 @@ -package exchange - -import ( - "time" - - "github.com/microsoftgraph/msgraph-sdk-go/models" - - "github.com/alcionai/corso/src/pkg/backup/details" -) - -func MessageInfo(msg models.Messageable, size int64) *details.ExchangeInfo { - sender := "" - subject := "" - received := time.Time{} - created := time.Time{} - modified := time.Time{} - - if msg.GetSender() != nil && - msg.GetSender().GetEmailAddress() != nil && - msg.GetSender().GetEmailAddress().GetAddress() != nil { - sender = *msg.GetSender().GetEmailAddress().GetAddress() - } - - if msg.GetSubject() != nil { - subject = *msg.GetSubject() - } - - if msg.GetReceivedDateTime() != nil { - received = *msg.GetReceivedDateTime() - } - - if msg.GetCreatedDateTime() != nil { - created = *msg.GetCreatedDateTime() - } - - if msg.GetLastModifiedDateTime() != nil { - modified = *msg.GetLastModifiedDateTime() - } - - return &details.ExchangeInfo{ - ItemType: details.ExchangeMail, - Sender: sender, - Subject: subject, - Received: received, - Created: created, - Modified: modified, - Size: size, - } -} diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index 70f2190c5..0880ad233 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -2,6 +2,7 @@ package exchange import ( "context" + "fmt" "github.com/pkg/errors" @@ -56,19 +57,16 @@ func filterContainersAndFillCollections( return err } + ibt, err := itemerByType(ac, scope.Category().PathType()) + if err != nil { + return err + } + for _, c := range resolver.Items() { if ctrlOpts.FailFast && errs != nil { return errs } - // cannot be moved out of the loop, - // else we run into state issues. - service, err := createService(qp.Credentials) - if err != nil { - errs = support.WrapAndAppend(qp.ResourceOwner, err, errs) - continue - } - cID := *c.GetId() delete(tombstones, cID) @@ -118,8 +116,7 @@ func filterContainersAndFillCollections( currPath, prevPath, scope.Category().PathType(), - ac, - service, + ibt, statusUpdater, ctrlOpts, newDelta.Reset) @@ -148,12 +145,6 @@ func filterContainersAndFillCollections( // in the `previousPath` set, but does not exist in the current container // resolver (which contains all the resource owners' current containers). for id, p := range tombstones { - service, err := createService(qp.Credentials) - if err != nil { - errs = support.WrapAndAppend(p, err, errs) - continue - } - if collections[id] != nil { errs = support.WrapAndAppend(p, errors.New("conflict: tombstone exists for a live collection"), errs) continue @@ -178,8 +169,7 @@ func filterContainersAndFillCollections( nil, // marks the collection as deleted prevPath, scope.Category().PathType(), - ac, - service, + ibt, statusUpdater, ctrlOpts, false) @@ -231,3 +221,16 @@ func pathFromPrevString(ps string) (path.Path, error) { return p, nil } + +func itemerByType(ac api.Client, category path.CategoryType) (itemer, error) { + switch category { + case path.EmailCategory: + return ac.Mail(), nil + case path.EventsCategory: + return ac.Events(), nil + case path.ContactsCategory: + return ac.Contacts(), nil + default: + return nil, fmt.Errorf("category %s not supported by getFetchIDFunc", category) + } +} diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go index 06c56ac9d..3f88f6efe 100644 --- a/src/internal/connector/exchange/service_restore.go +++ b/src/internal/connector/exchange/service_restore.go @@ -84,7 +84,10 @@ func RestoreExchangeContact( return nil, errors.New("msgraph contact post fail: REST response not received") } - return ContactInfo(contact, int64(len(bits))), nil + info := api.ContactInfo(contact) + info.Size = int64(len(bits)) + + return info, nil } // RestoreExchangeEvent restores a contact to the @bits byte @@ -153,7 +156,10 @@ func RestoreExchangeEvent( } } - return EventInfo(event, int64(len(bits))), errs + info := api.EventInfo(event) + info.Size = int64(len(bits)) + + return info, errs } // RestoreMailMessage utility function to place an exchange.Mail @@ -215,7 +221,10 @@ func RestoreMailMessage( } } - return MessageInfo(clone, int64(len(bits))), nil + info := api.MailInfo(clone) + info.Size = int64(len(bits)) + + return info, nil } // attachmentBytes is a helper to retrieve the attachment content from a models.Attachmentable From 45874abf7e814659954eb319fcf3562b22114e1e Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 17 Jan 2023 12:50:24 -0800 Subject: [PATCH 31/38] Begin persisting OneDrive/SharePoint library metdata (#2144) ## Description Start persisting the folder path maps and delta URLs for backed up OneDrive/SharePoint drives. Delta URLs are saved in a map[drive ID]deltaURL while folder IDs are in a map[driveID]map[folder ID]folder path Needs another patch to properly save the path for folders that match the selector, currently the selector comparison is only on the parent of an item Later PRs can get the new folder map by taking the map from the previous backup and making changes to it when folder deletions/moves are encountered ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2120 ## Test Plan - [x] :muscle: Manual - [x] :zap: Unit test - [ ] :green_heart: E2E --- .../connector/data_collections_test.go | 88 ++++++----- .../connector/onedrive/collections.go | 86 ++++++++++- .../connector/onedrive/collections_test.go | 137 +++++++++++++----- src/internal/connector/onedrive/drive.go | 37 ++++- src/internal/connector/onedrive/item_test.go | 9 +- .../sharepoint/data_collections_test.go | 3 +- 6 files changed, 259 insertions(+), 101 deletions(-) diff --git a/src/internal/connector/data_collections_test.go b/src/internal/connector/data_collections_test.go index cfec30173..9ee113ed2 100644 --- a/src/internal/connector/data_collections_test.go +++ b/src/internal/connector/data_collections_test.go @@ -13,6 +13,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/sharepoint" "github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -303,9 +304,7 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) SetupSuite() { tester.LogTimeOfTest(suite.T()) } -// TestCreateSharePointCollection. Ensures the proper amount of collections are created based -// on the selector. -func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateSharePointCollection() { +func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateSharePointCollection_Libraries() { ctx, flush := tester.NewContext() defer flush() @@ -316,51 +315,46 @@ func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateShar siteIDs = []string{siteID} ) - tables := []struct { - name string - sel func() selectors.Selector - comparator assert.ComparisonAssertionFunc - }{ - { - name: "SharePoint.Libraries", - comparator: assert.Equal, - sel: func() selectors.Selector { - sel := selectors.NewSharePointBackup(siteIDs) - sel.Include(sel.Libraries([]string{"foo"}, selectors.PrefixMatch())) - return sel.Selector - }, - }, - { - name: "SharePoint.Lists", - comparator: assert.Less, - sel: func() selectors.Selector { - sel := selectors.NewSharePointBackup(siteIDs) - sel.Include(sel.Lists(selectors.Any(), selectors.PrefixMatch())) + sel := selectors.NewSharePointBackup(siteIDs) + sel.Include(sel.Libraries([]string{"foo"}, selectors.PrefixMatch())) - return sel.Selector - }, - }, - } + cols, err := gc.DataCollections(ctx, sel.Selector, nil, control.Options{}) + require.NoError(t, err) + assert.Len(t, cols, 1) - for _, test := range tables { - t.Run(test.name, func(t *testing.T) { - cols, err := gc.DataCollections(ctx, test.sel(), nil, control.Options{}) - require.NoError(t, err) - test.comparator(t, 0, len(cols)) - - if test.name == "SharePoint.Lists" { - for _, collection := range cols { - t.Logf("Path: %s\n", collection.FullPath().String()) - for item := range collection.Items() { - t.Log("File: " + item.UUID()) - - bytes, err := io.ReadAll(item.ToReader()) - require.NoError(t, err) - t.Log(string(bytes)) - - } - } - } - }) + for _, collection := range cols { + t.Logf("Path: %s\n", collection.FullPath().String()) + assert.Equal(t, path.SharePointMetadataService, collection.FullPath().Service()) + } +} + +func (suite *ConnectorCreateSharePointCollectionIntegrationSuite) TestCreateSharePointCollection_Lists() { + ctx, flush := tester.NewContext() + defer flush() + + var ( + t = suite.T() + siteID = tester.M365SiteID(t) + gc = loadConnector(ctx, t, 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{}) + require.NoError(t, err) + assert.Less(t, 0, len(cols)) + + for _, collection := range cols { + t.Logf("Path: %s\n", collection.FullPath().String()) + + for item := range collection.Items() { + t.Log("File: " + item.UUID()) + + bs, err := io.ReadAll(item.ToReader()) + require.NoError(t, err) + t.Log(string(bs)) + } } } diff --git a/src/internal/connector/onedrive/collections.go b/src/internal/connector/onedrive/collections.go index d3528cb02..7bdbfc8f5 100644 --- a/src/internal/connector/onedrive/collections.go +++ b/src/internal/connector/onedrive/collections.go @@ -25,6 +25,17 @@ const ( SharePointSource ) +func (ds driveSource) toPathServiceCat() (path.ServiceType, path.CategoryType) { + switch ds { + case OneDriveSource: + return path.OneDriveService, path.FilesCategory + case SharePointSource: + return path.SharePointService, path.LibrariesCategory + default: + return path.UnknownService, path.UnknownCategory + } +} + type folderMatcher interface { IsAny() bool Matches(string) bool @@ -81,27 +92,80 @@ func (c *Collections) Get(ctx context.Context) ([]data.Collection, error) { return nil, err } + var ( + // Drive ID -> delta URL for drive + deltaURLs = map[string]string{} + // Drive ID -> folder ID -> folder path + folderPaths = map[string]map[string]string{} + ) + // Update the collection map with items from each drive for _, d := range drives { - err = collectItems(ctx, c.service, *d.GetId(), c.UpdateCollections) + driveID := *d.GetId() + + delta, paths, err := collectItems(ctx, c.service, driveID, c.UpdateCollections) if err != nil { return nil, err } + + if len(delta) > 0 { + deltaURLs[driveID] = delta + } + + if len(paths) > 0 { + folderPaths[driveID] = map[string]string{} + + for id, p := range paths { + folderPaths[driveID][id] = p + } + } } observe.Message(ctx, fmt.Sprintf("Discovered %d items to backup", c.NumItems)) - collections := make([]data.Collection, 0, len(c.CollectionMap)) + // Add an extra for the metadata collection. + collections := make([]data.Collection, 0, len(c.CollectionMap)+1) for _, coll := range c.CollectionMap { collections = append(collections, coll) } + service, category := c.source.toPathServiceCat() + metadata, err := graph.MakeMetadataCollection( + c.tenant, + c.resourceOwner, + service, + category, + []graph.MetadataCollectionEntry{ + graph.NewMetadataEntry(graph.PreviousPathFileName, folderPaths), + graph.NewMetadataEntry(graph.DeltaURLsFileName, deltaURLs), + }, + c.statusUpdater, + ) + + if err != nil { + // Technically it's safe to continue here because the logic for starting an + // incremental backup should eventually find that the metadata files are + // empty/missing and default to a full backup. + logger.Ctx(ctx).Warnw( + "making metadata collection for future incremental backups", + "error", + err, + ) + } else { + collections = append(collections, metadata) + } + return collections, nil } // UpdateCollections initializes and adds the provided drive items to Collections // A new collection is created for every drive folder (or package) -func (c *Collections) UpdateCollections(ctx context.Context, driveID string, items []models.DriveItemable) error { +func (c *Collections) UpdateCollections( + ctx context.Context, + driveID string, + items []models.DriveItemable, + paths map[string]string, +) error { for _, item := range items { if item.GetRoot() != nil { // Skip the root item @@ -131,9 +195,19 @@ func (c *Collections) UpdateCollections(ctx context.Context, driveID string, ite switch { case item.GetFolder() != nil, item.GetPackage() != nil: - // Leave this here so we don't fall into the default case. - // TODO: This is where we might create a "special file" to represent these in the backup repository - // e.g. a ".folderMetadataFile" + // Eventually, deletions of folders will be handled here so we may as well + // start off by saving the path.Path of the item instead of just the + // OneDrive parentRef or such. + folderPath, err := collectionPath.Append(*item.GetName(), false) + if err != nil { + logger.Ctx(ctx).Errorw("failed building collection path", "error", err) + return err + } + + // TODO(ashmrtn): Handle deletions by removing this entry from the map. + // TODO(ashmrtn): Handle moves by setting the collection state if the + // collection doesn't already exist/have that state. + paths[*item.GetId()] = folderPath.String() case item.GetFile() != nil: col, found := c.CollectionMap[collectionPath.String()] diff --git a/src/internal/connector/onedrive/collections_test.go b/src/internal/connector/onedrive/collections_test.go index 923c5512d..f31ae8bab 100644 --- a/src/internal/connector/onedrive/collections_test.go +++ b/src/internal/connector/onedrive/collections_test.go @@ -102,19 +102,21 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { expectedItemCount int expectedContainerCount int expectedFileCount int + expectedMetadataPaths map[string]string }{ { testCase: "Invalid item", items: []models.DriveItemable{ - driveItem("item", testBaseDrivePath, false, false, false), + driveItem("item", "item", testBaseDrivePath, false, false, false), }, - scope: anyFolder, - expect: assert.Error, + scope: anyFolder, + expect: assert.Error, + expectedMetadataPaths: map[string]string{}, }, { testCase: "Single File", items: []models.DriveItemable{ - driveItem("file", testBaseDrivePath, true, false, false), + driveItem("file", "file", testBaseDrivePath, true, false, false), }, scope: anyFolder, expect: assert.NoError, @@ -127,33 +129,51 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { expectedItemCount: 2, expectedFileCount: 1, expectedContainerCount: 1, + // Root folder is skipped since it's always present. + expectedMetadataPaths: map[string]string{}, }, { testCase: "Single Folder", items: []models.DriveItemable{ - driveItem("folder", testBaseDrivePath, false, true, false), + driveItem("folder", "folder", testBaseDrivePath, false, true, false), }, scope: anyFolder, expect: assert.NoError, expectedCollectionPaths: []string{}, + expectedMetadataPaths: map[string]string{ + "folder": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder", + )[0], + }, }, { testCase: "Single Package", items: []models.DriveItemable{ - driveItem("package", testBaseDrivePath, false, false, true), + driveItem("package", "package", testBaseDrivePath, false, false, true), }, scope: anyFolder, expect: assert.NoError, expectedCollectionPaths: []string{}, + expectedMetadataPaths: map[string]string{ + "package": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/package", + )[0], + }, }, { testCase: "1 root file, 1 folder, 1 package, 2 files, 3 collections", items: []models.DriveItemable{ - driveItem("fileInRoot", testBaseDrivePath, true, false, false), - driveItem("folder", testBaseDrivePath, false, true, false), - driveItem("package", testBaseDrivePath, false, false, true), - driveItem("fileInFolder", testBaseDrivePath+folder, true, false, false), - driveItem("fileInPackage", testBaseDrivePath+pkg, true, false, false), + driveItem("fileInRoot", "fileInRoot", testBaseDrivePath, true, false, false), + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("package", "package", testBaseDrivePath, false, false, true), + driveItem("fileInFolder", "fileInFolder", testBaseDrivePath+folder, true, false, false), + driveItem("fileInPackage", "fileInPackage", testBaseDrivePath+pkg, true, false, false), }, scope: anyFolder, expect: assert.NoError, @@ -168,18 +188,32 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { expectedItemCount: 6, expectedFileCount: 3, expectedContainerCount: 3, + expectedMetadataPaths: map[string]string{ + "folder": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder", + )[0], + "package": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/package", + )[0], + }, }, { testCase: "contains folder selector", items: []models.DriveItemable{ - driveItem("fileInRoot", testBaseDrivePath, true, false, false), - driveItem("folder", testBaseDrivePath, false, true, false), - driveItem("subfolder", testBaseDrivePath+folder, false, true, false), - driveItem("folder", testBaseDrivePath+folderSub, false, true, false), - driveItem("package", testBaseDrivePath, false, false, true), - driveItem("fileInFolder", testBaseDrivePath+folder, true, false, false), - driveItem("fileInFolder2", testBaseDrivePath+folderSub+folder, true, false, false), - driveItem("fileInPackage", testBaseDrivePath+pkg, true, false, false), + driveItem("fileInRoot", "fileInRoot", testBaseDrivePath, true, false, false), + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("subfolder", "subfolder", testBaseDrivePath+folder, false, true, false), + driveItem("folder2", "folder", testBaseDrivePath+folderSub, false, true, false), + driveItem("package", "package", testBaseDrivePath, false, false, true), + driveItem("fileInFolder", "fileInFolder", testBaseDrivePath+folder, true, false, false), + driveItem("fileInFolder2", "fileInFolder2", testBaseDrivePath+folderSub+folder, true, false, false), + driveItem("fileInFolderPackage", "fileInPackage", testBaseDrivePath+pkg, true, false, false), }, scope: (&selectors.OneDriveBackup{}).Folders([]string{"folder"})[0], expect: assert.NoError, @@ -200,18 +234,34 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { expectedItemCount: 4, expectedFileCount: 2, expectedContainerCount: 2, + // just "folder" isn't added here because the include check is done on the + // parent path since we only check later if something is a folder or not. + expectedMetadataPaths: map[string]string{ + "subfolder": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder/subfolder", + )[0], + "folder2": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder/subfolder/folder", + )[0], + }, }, { testCase: "prefix subfolder selector", items: []models.DriveItemable{ - driveItem("fileInRoot", testBaseDrivePath, true, false, false), - driveItem("folder", testBaseDrivePath, false, true, false), - driveItem("subfolder", testBaseDrivePath+folder, false, true, false), - driveItem("folder", testBaseDrivePath+folderSub, false, true, false), - driveItem("package", testBaseDrivePath, false, false, true), - driveItem("fileInFolder", testBaseDrivePath+folder, true, false, false), - driveItem("fileInFolder2", testBaseDrivePath+folderSub+folder, true, false, false), - driveItem("fileInPackage", testBaseDrivePath+pkg, true, false, false), + driveItem("fileInRoot", "fileInRoot", testBaseDrivePath, true, false, false), + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("subfolder", "subfolder", testBaseDrivePath+folder, false, true, false), + driveItem("folder", "folder", testBaseDrivePath+folderSub, false, true, false), + driveItem("package", "package", testBaseDrivePath, false, false, true), + driveItem("fileInFolder", "fileInFolder", testBaseDrivePath+folder, true, false, false), + driveItem("fileInFolder2", "fileInFolder2", testBaseDrivePath+folderSub+folder, true, false, false), + driveItem("fileInPackage", "fileInPackage", testBaseDrivePath+pkg, true, false, false), }, scope: (&selectors.OneDriveBackup{}). Folders([]string{"/folder/subfolder"}, selectors.PrefixMatch())[0], @@ -225,17 +275,25 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { expectedItemCount: 2, expectedFileCount: 1, expectedContainerCount: 1, + expectedMetadataPaths: map[string]string{ + "folder": expectedPathAsSlice( + suite.T(), + tenant, + user, + testBaseDrivePath+"/folder/subfolder/folder", + )[0], + }, }, { testCase: "match subfolder selector", items: []models.DriveItemable{ - driveItem("fileInRoot", testBaseDrivePath, true, false, false), - driveItem("folder", testBaseDrivePath, false, true, false), - driveItem("subfolder", testBaseDrivePath+folder, false, true, false), - driveItem("package", testBaseDrivePath, false, false, true), - driveItem("fileInFolder", testBaseDrivePath+folder, true, false, false), - driveItem("fileInSubfolder", testBaseDrivePath+folderSub, true, false, false), - driveItem("fileInPackage", testBaseDrivePath+pkg, true, false, false), + driveItem("fileInRoot", "fileInRoot", testBaseDrivePath, true, false, false), + driveItem("folder", "folder", testBaseDrivePath, false, true, false), + driveItem("subfolder", "subfolder", testBaseDrivePath+folder, false, true, false), + driveItem("package", "package", testBaseDrivePath, false, false, true), + driveItem("fileInFolder", "fileInFolder", testBaseDrivePath+folder, true, false, false), + driveItem("fileInSubfolder", "fileInSubfolder", testBaseDrivePath+folderSub, true, false, false), + driveItem("fileInPackage", "fileInPackage", testBaseDrivePath+pkg, true, false, false), }, scope: (&selectors.OneDriveBackup{}).Folders([]string{"folder/subfolder"})[0], expect: assert.NoError, @@ -248,6 +306,8 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { expectedItemCount: 2, expectedFileCount: 1, expectedContainerCount: 1, + // No child folders for subfolder so nothing here. + expectedMetadataPaths: map[string]string{}, }, } @@ -256,6 +316,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { ctx, flush := tester.NewContext() defer flush() + paths := map[string]string{} c := NewCollections( tenant, user, @@ -265,7 +326,7 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { nil, control.Options{}) - err := c.UpdateCollections(ctx, "driveID", tt.items) + err := c.UpdateCollections(ctx, "driveID", tt.items, paths) tt.expect(t, err) assert.Equal(t, len(tt.expectedCollectionPaths), len(c.CollectionMap), "collection paths") assert.Equal(t, tt.expectedItemCount, c.NumItems, "item count") @@ -274,14 +335,16 @@ func (suite *OneDriveCollectionsSuite) TestUpdateCollections() { for _, collPath := range tt.expectedCollectionPaths { assert.Contains(t, c.CollectionMap, collPath) } + + assert.Equal(t, tt.expectedMetadataPaths, paths) }) } } -func driveItem(name string, path string, isFile, isFolder, isPackage bool) models.DriveItemable { +func driveItem(id string, name string, path string, isFile, isFolder, isPackage bool) models.DriveItemable { item := models.NewDriveItem() item.SetName(&name) - item.SetId(&name) + item.SetId(&id) parentReference := models.NewItemReference() parentReference.SetPath(&path) diff --git a/src/internal/connector/onedrive/drive.go b/src/internal/connector/onedrive/drive.go index 36a79dca1..f063eec53 100644 --- a/src/internal/connector/onedrive/drive.go +++ b/src/internal/connector/onedrive/drive.go @@ -161,7 +161,12 @@ func userDrives(ctx context.Context, service graph.Servicer, user string) ([]mod } // itemCollector functions collect the items found in a drive -type itemCollector func(ctx context.Context, driveID string, driveItems []models.DriveItemable) error +type itemCollector func( + ctx context.Context, + driveID string, + driveItems []models.DriveItemable, + paths map[string]string, +) error // collectItems will enumerate all items in the specified drive and hand them to the // provided `collector` method @@ -170,7 +175,14 @@ func collectItems( service graph.Servicer, driveID string, collector itemCollector, -) error { +) (string, map[string]string, error) { + var ( + newDeltaURL = "" + // TODO(ashmrtn): Eventually this should probably be a parameter so we can + // take in previous paths. + paths = map[string]string{} + ) + // TODO: Specify a timestamp in the delta query // https://docs.microsoft.com/en-us/graph/api/driveitem-delta? // view=graph-rest-1.0&tabs=http#example-4-retrieving-delta-results-using-a-timestamp @@ -200,16 +212,20 @@ func collectItems( for { r, err := builder.Get(ctx, requestConfig) if err != nil { - return errors.Wrapf( + return "", nil, errors.Wrapf( err, "failed to query drive items. details: %s", support.ConnectorStackErrorTrace(err), ) } - err = collector(ctx, driveID, r.GetValue()) + err = collector(ctx, driveID, r.GetValue(), paths) if err != nil { - return err + return "", nil, err + } + + if r.GetOdataDeltaLink() != nil && len(*r.GetOdataDeltaLink()) > 0 { + newDeltaURL = *r.GetOdataDeltaLink() } // Check if there are more items @@ -222,7 +238,7 @@ func collectItems( builder = msdrives.NewItemRootDeltaRequestBuilder(*nextLink, service.Adapter()) } - return nil + return newDeltaURL, paths, nil } // getFolder will lookup the specified folder name under `parentFolderID` @@ -329,11 +345,16 @@ func GetAllFolders( folders := map[string]*Displayable{} for _, d := range drives { - err = collectItems( + _, _, err = collectItems( ctx, gs, *d.GetId(), - func(innerCtx context.Context, driveID string, items []models.DriveItemable) error { + func( + innerCtx context.Context, + driveID string, + items []models.DriveItemable, + paths map[string]string, + ) error { for _, item := range items { // Skip the root item. if item.GetRoot() != nil { diff --git a/src/internal/connector/onedrive/item_test.go b/src/internal/connector/onedrive/item_test.go index e423e65d9..d87878fc4 100644 --- a/src/internal/connector/onedrive/item_test.go +++ b/src/internal/connector/onedrive/item_test.go @@ -95,7 +95,12 @@ func (suite *ItemIntegrationSuite) TestItemReader_oneDrive() { var driveItem models.DriveItemable // This item collector tries to find "a" drive item that is a file to test the reader function - itemCollector := func(ctx context.Context, driveID string, items []models.DriveItemable) error { + itemCollector := func( + ctx context.Context, + driveID string, + items []models.DriveItemable, + paths map[string]string, + ) error { for _, item := range items { if item.GetFile() != nil { driveItem = item @@ -105,7 +110,7 @@ func (suite *ItemIntegrationSuite) TestItemReader_oneDrive() { return nil } - err := collectItems(ctx, suite, suite.userDriveID, itemCollector) + _, _, err := collectItems(ctx, suite, suite.userDriveID, itemCollector) require.NoError(suite.T(), err) // Test Requirement 2: Need a file diff --git a/src/internal/connector/sharepoint/data_collections_test.go b/src/internal/connector/sharepoint/data_collections_test.go index 9b391d1e8..f52a642ba 100644 --- a/src/internal/connector/sharepoint/data_collections_test.go +++ b/src/internal/connector/sharepoint/data_collections_test.go @@ -87,6 +87,7 @@ func (suite *SharePointLibrariesSuite) TestUpdateCollections() { ctx, flush := tester.NewContext() defer flush() + paths := map[string]string{} c := onedrive.NewCollections( tenant, site, @@ -95,7 +96,7 @@ func (suite *SharePointLibrariesSuite) TestUpdateCollections() { &MockGraphService{}, nil, control.Options{}) - err := c.UpdateCollections(ctx, "driveID", test.items) + err := c.UpdateCollections(ctx, "driveID", test.items, paths) test.expect(t, err) assert.Equal(t, len(test.expectedCollectionPaths), len(c.CollectionMap), "collection paths") assert.Equal(t, test.expectedItemCount, c.NumItems, "item count") From 48e4b651655d7927edf0a63efb26008d901709cd Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 17 Jan 2023 14:22:30 -0700 Subject: [PATCH 32/38] migrate, test manifest+meta producer (#2091) ## Description Adds mocked unit tests for produceManifestsAnd- Metadata. For cleanliness, moves that func, and any funcs called within it, to their own file within operations ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :robot: Test ## Issue(s) * #2062 ## Test Plan - [x] :zap: Unit test --- src/internal/operations/backup.go | 174 ------ src/internal/operations/backup_test.go | 63 +- src/internal/operations/manifests.go | 210 +++++++ src/internal/operations/manifests_test.go | 685 ++++++++++++++++++++++ 4 files changed, 949 insertions(+), 183 deletions(-) create mode 100644 src/internal/operations/manifests.go create mode 100644 src/internal/operations/manifests_test.go diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 3a35eb349..7f562321c 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -6,12 +6,10 @@ import ( "github.com/google/uuid" multierror "github.com/hashicorp/go-multierror" - "github.com/kopia/kopia/repo/manifest" "github.com/pkg/errors" "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector" - "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/data" D "github.com/alcionai/corso/src/internal/diagnostics" @@ -262,178 +260,6 @@ type backuper interface { ) (*kopia.BackupStats, *details.Builder, map[string]path.Path, error) } -func verifyDistinctBases(mans []*kopia.ManifestEntry) error { - var ( - errs *multierror.Error - reasons = map[string]manifest.ID{} - ) - - for _, man := range mans { - // Incomplete snapshots are used only for kopia-assisted incrementals. The - // fact that we need this check here makes it seem like this should live in - // the kopia code. However, keeping it here allows for better debugging as - // the kopia code only has access to a path builder which means it cannot - // remove the resource owner from the error/log output. That is also below - // the point where we decide if we should do a full backup or an - // incremental. - if len(man.IncompleteReason) > 0 { - continue - } - - for _, reason := range man.Reasons { - reasonKey := reason.ResourceOwner + reason.Service.String() + reason.Category.String() - - if b, ok := reasons[reasonKey]; ok { - errs = multierror.Append(errs, errors.Errorf( - "multiple base snapshots source data for %s %s. IDs: %s, %s", - reason.Service.String(), - reason.Category.String(), - b, - man.ID, - )) - - continue - } - - reasons[reasonKey] = man.ID - } - } - - return errs.ErrorOrNil() -} - -// calls kopia to retrieve prior backup manifests, metadata collections to supply backup heuristics. -func produceManifestsAndMetadata( - ctx context.Context, - kw *kopia.Wrapper, - sw *store.Wrapper, - reasons []kopia.Reason, - tenantID string, - getMetadata bool, -) ([]*kopia.ManifestEntry, []data.Collection, bool, error) { - var ( - metadataFiles = graph.AllMetadataFileNames() - collections []data.Collection - ) - - ms, err := kw.FetchPrevSnapshotManifests( - ctx, - reasons, - map[string]string{kopia.TagBackupCategory: ""}) - if err != nil { - return nil, nil, false, err - } - - if !getMetadata { - return ms, nil, false, nil - } - - // We only need to check that we have 1:1 reason:base if we're doing an - // incremental with associated metadata. This ensures that we're only sourcing - // data from a single Point-In-Time (base) for each incremental backup. - // - // TODO(ashmrtn): This may need updating if we start sourcing item backup - // details from previous snapshots when using kopia-assisted incrementals. - if err := verifyDistinctBases(ms); err != nil { - logger.Ctx(ctx).Warnw( - "base snapshot collision, falling back to full backup", - "error", - err, - ) - - return ms, nil, false, nil - } - - for _, man := range ms { - if len(man.IncompleteReason) > 0 { - continue - } - - bID, ok := man.GetTag(kopia.TagBackupID) - if !ok { - return nil, nil, false, errors.New("snapshot manifest missing backup ID") - } - - dID, _, err := sw.GetDetailsIDFromBackupID(ctx, model.StableID(bID)) - if err != nil { - // if no backup exists for any of the complete manifests, we want - // to fall back to a complete backup. - if errors.Is(err, kopia.ErrNotFound) { - logger.Ctx(ctx).Infow( - "backup missing, falling back to full backup", - "backup_id", bID) - - return ms, nil, false, nil - } - - return nil, nil, false, errors.Wrap(err, "retrieving prior backup data") - } - - // if no detailsID exists for any of the complete manifests, we want - // to fall back to a complete backup. This is a temporary prevention - // mechanism to keep backups from falling into a perpetually bad state. - // This makes an assumption that the ID points to a populated set of - // details; we aren't doing the work to look them up. - if len(dID) == 0 { - logger.Ctx(ctx).Infow( - "backup missing details ID, falling back to full backup", - "backup_id", bID) - - return ms, nil, false, nil - } - - colls, err := collectMetadata(ctx, kw, man, metadataFiles, tenantID) - if err != nil && !errors.Is(err, kopia.ErrNotFound) { - // prior metadata isn't guaranteed to exist. - // if it doesn't, we'll just have to do a - // full backup for that data. - return nil, nil, false, err - } - - collections = append(collections, colls...) - } - - return ms, collections, true, err -} - -func collectMetadata( - ctx context.Context, - r restorer, - man *kopia.ManifestEntry, - fileNames []string, - tenantID string, -) ([]data.Collection, error) { - paths := []path.Path{} - - for _, fn := range fileNames { - for _, reason := range man.Reasons { - p, err := path.Builder{}. - Append(fn). - ToServiceCategoryMetadataPath( - tenantID, - reason.ResourceOwner, - reason.Service, - reason.Category, - true) - if err != nil { - return nil, errors.Wrapf(err, "building metadata path") - } - - paths = append(paths, p) - } - } - - dcs, err := r.RestoreMultipleItems(ctx, string(man.ID), paths, nil) - if err != nil { - // Restore is best-effort and we want to keep it that way since we want to - // return as much metadata as we can to reduce the work we'll need to do. - // Just wrap the error here for better reporting/debugging. - return dcs, errors.Wrap(err, "collecting prior metadata") - } - - return dcs, nil -} - func selectorToReasons(sel selectors.Selector) []kopia.Reason { service := sel.PathService() reasons := []kopia.Reason{} diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index 90c5aa50e..32a6d1a4e 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -35,7 +35,26 @@ import ( // ----- restore producer type mockRestorer struct { - gotPaths []path.Path + gotPaths []path.Path + colls []data.Collection + collsByID map[string][]data.Collection // snapshotID: []Collection + err error + onRestore restoreFunc +} + +type restoreFunc func(id string, ps []path.Path) ([]data.Collection, error) + +func (mr *mockRestorer) buildRestoreFunc( + t *testing.T, + oid string, + ops []path.Path, +) { + mr.onRestore = func(id string, ps []path.Path) ([]data.Collection, error) { + assert.Equal(t, oid, id, "manifest id") + checkPaths(t, ops, ps) + + return mr.colls, mr.err + } } func (mr *mockRestorer) RestoreMultipleItems( @@ -46,13 +65,19 @@ func (mr *mockRestorer) RestoreMultipleItems( ) ([]data.Collection, error) { mr.gotPaths = append(mr.gotPaths, paths...) - return nil, nil + if mr.onRestore != nil { + return mr.onRestore(snapshotID, paths) + } + + if len(mr.collsByID) > 0 { + return mr.collsByID[snapshotID], mr.err + } + + return mr.colls, mr.err } -func (mr mockRestorer) checkPaths(t *testing.T, expected []path.Path) { - t.Helper() - - assert.ElementsMatch(t, expected, mr.gotPaths) +func checkPaths(t *testing.T, expected, got []path.Path) { + assert.ElementsMatch(t, expected, got) } // ----- backup producer @@ -168,6 +193,27 @@ func (mbs mockBackupStorer) Update(context.Context, model.Schema, model.Model) e // helper funcs // --------------------------------------------------------------------------- +// expects you to Append your own file +func makeMetadataBasePath( + t *testing.T, + tenant string, + service path.ServiceType, + resourceOwner string, + category path.CategoryType, +) path.Path { + t.Helper() + + p, err := path.Builder{}.ToServiceCategoryMetadataPath( + tenant, + resourceOwner, + service, + category, + false) + require.NoError(t, err) + + return p +} + func makeMetadataPath( t *testing.T, tenant string, @@ -183,8 +229,7 @@ func makeMetadataPath( resourceOwner, service, category, - true, - ) + true) require.NoError(t, err) return p @@ -635,7 +680,7 @@ func (suite *BackupOpSuite) TestBackupOperation_CollectMetadata() { _, err := collectMetadata(ctx, mr, test.inputMan, test.inputFiles, tenant) assert.NoError(t, err) - mr.checkPaths(t, test.expected) + checkPaths(t, test.expected, mr.gotPaths) }) } } diff --git a/src/internal/operations/manifests.go b/src/internal/operations/manifests.go new file mode 100644 index 000000000..fe0e4d09d --- /dev/null +++ b/src/internal/operations/manifests.go @@ -0,0 +1,210 @@ +package operations + +import ( + "context" + + multierror "github.com/hashicorp/go-multierror" + "github.com/kopia/kopia/repo/manifest" + "github.com/pkg/errors" + + "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/kopia" + "github.com/alcionai/corso/src/internal/model" + "github.com/alcionai/corso/src/pkg/backup" + "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/path" +) + +type manifestFetcher interface { + FetchPrevSnapshotManifests( + ctx context.Context, + reasons []kopia.Reason, + tags map[string]string, + ) ([]*kopia.ManifestEntry, error) +} + +type manifestRestorer interface { + manifestFetcher + restorer +} + +type getDetailsIDer interface { + GetDetailsIDFromBackupID( + ctx context.Context, + backupID model.StableID, + ) (string, *backup.Backup, error) +} + +// calls kopia to retrieve prior backup manifests, metadata collections to supply backup heuristics. +func produceManifestsAndMetadata( + ctx context.Context, + mr manifestRestorer, + gdi getDetailsIDer, + reasons []kopia.Reason, + tenantID string, + getMetadata bool, +) ([]*kopia.ManifestEntry, []data.Collection, bool, error) { + var ( + metadataFiles = graph.AllMetadataFileNames() + collections []data.Collection + ) + + ms, err := mr.FetchPrevSnapshotManifests( + ctx, + reasons, + map[string]string{kopia.TagBackupCategory: ""}) + if err != nil { + return nil, nil, false, err + } + + if !getMetadata { + return ms, nil, false, nil + } + + // We only need to check that we have 1:1 reason:base if we're doing an + // incremental with associated metadata. This ensures that we're only sourcing + // data from a single Point-In-Time (base) for each incremental backup. + // + // TODO(ashmrtn): This may need updating if we start sourcing item backup + // details from previous snapshots when using kopia-assisted incrementals. + if err := verifyDistinctBases(ms); err != nil { + logger.Ctx(ctx).Warnw( + "base snapshot collision, falling back to full backup", + "error", + err, + ) + + return ms, nil, false, nil + } + + for _, man := range ms { + if len(man.IncompleteReason) > 0 { + continue + } + + bID, ok := man.GetTag(kopia.TagBackupID) + if !ok { + return nil, nil, false, errors.New("snapshot manifest missing backup ID") + } + + dID, _, err := gdi.GetDetailsIDFromBackupID(ctx, model.StableID(bID)) + if err != nil { + // if no backup exists for any of the complete manifests, we want + // to fall back to a complete backup. + if errors.Is(err, kopia.ErrNotFound) { + logger.Ctx(ctx).Infow( + "backup missing, falling back to full backup", + "backup_id", bID) + + return ms, nil, false, nil + } + + return nil, nil, false, errors.Wrap(err, "retrieving prior backup data") + } + + // if no detailsID exists for any of the complete manifests, we want + // to fall back to a complete backup. This is a temporary prevention + // mechanism to keep backups from falling into a perpetually bad state. + // This makes an assumption that the ID points to a populated set of + // details; we aren't doing the work to look them up. + if len(dID) == 0 { + logger.Ctx(ctx).Infow( + "backup missing details ID, falling back to full backup", + "backup_id", bID) + + return ms, nil, false, nil + } + + colls, err := collectMetadata(ctx, mr, man, metadataFiles, tenantID) + if err != nil && !errors.Is(err, kopia.ErrNotFound) { + // prior metadata isn't guaranteed to exist. + // if it doesn't, we'll just have to do a + // full backup for that data. + return nil, nil, false, err + } + + collections = append(collections, colls...) + } + + return ms, collections, true, err +} + +// verifyDistinctBases is a validation checker that ensures, for a given slice +// of manifests, that each manifest's Reason (owner, service, category) is only +// included once. If a reason is duplicated by any two manifests, an error is +// returned. +func verifyDistinctBases(mans []*kopia.ManifestEntry) error { + var ( + errs *multierror.Error + reasons = map[string]manifest.ID{} + ) + + for _, man := range mans { + // Incomplete snapshots are used only for kopia-assisted incrementals. The + // fact that we need this check here makes it seem like this should live in + // the kopia code. However, keeping it here allows for better debugging as + // the kopia code only has access to a path builder which means it cannot + // remove the resource owner from the error/log output. That is also below + // the point where we decide if we should do a full backup or an incremental. + if len(man.IncompleteReason) > 0 { + continue + } + + for _, reason := range man.Reasons { + reasonKey := reason.ResourceOwner + reason.Service.String() + reason.Category.String() + + if b, ok := reasons[reasonKey]; ok { + errs = multierror.Append(errs, errors.Errorf( + "multiple base snapshots source data for %s %s. IDs: %s, %s", + reason.Service, reason.Category, b, man.ID, + )) + + continue + } + + reasons[reasonKey] = man.ID + } + } + + return errs.ErrorOrNil() +} + +// collectMetadata retrieves all metadata files associated with the manifest. +func collectMetadata( + ctx context.Context, + r restorer, + man *kopia.ManifestEntry, + fileNames []string, + tenantID string, +) ([]data.Collection, error) { + paths := []path.Path{} + + for _, fn := range fileNames { + for _, reason := range man.Reasons { + p, err := path.Builder{}. + Append(fn). + ToServiceCategoryMetadataPath( + tenantID, + reason.ResourceOwner, + reason.Service, + reason.Category, + true) + if err != nil { + return nil, errors.Wrapf(err, "building metadata path") + } + + paths = append(paths, p) + } + } + + dcs, err := r.RestoreMultipleItems(ctx, string(man.ID), paths, nil) + if err != nil { + // Restore is best-effort and we want to keep it that way since we want to + // return as much metadata as we can to reduce the work we'll need to do. + // Just wrap the error here for better reporting/debugging. + return dcs, errors.Wrap(err, "collecting prior metadata") + } + + return dcs, nil +} diff --git a/src/internal/operations/manifests_test.go b/src/internal/operations/manifests_test.go new file mode 100644 index 000000000..7cfc9ac9a --- /dev/null +++ b/src/internal/operations/manifests_test.go @@ -0,0 +1,685 @@ +package operations + +import ( + "context" + "testing" + + "github.com/kopia/kopia/repo/manifest" + "github.com/kopia/kopia/snapshot" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/kopia" + "github.com/alcionai/corso/src/internal/model" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/pkg/backup" + "github.com/alcionai/corso/src/pkg/path" +) + +// --------------------------------------------------------------------------- +// interfaces +// --------------------------------------------------------------------------- + +type mockManifestRestorer struct { + mockRestorer + mans []*kopia.ManifestEntry + mrErr error // err varname already claimed by mockRestorer +} + +func (mmr mockManifestRestorer) FetchPrevSnapshotManifests( + ctx context.Context, + reasons []kopia.Reason, + tags map[string]string, +) ([]*kopia.ManifestEntry, error) { + return mmr.mans, mmr.mrErr +} + +type mockGetDetailsIDer struct { + detailsID string + err error +} + +func (mg mockGetDetailsIDer) GetDetailsIDFromBackupID( + ctx context.Context, + backupID model.StableID, +) (string, *backup.Backup, error) { + return mg.detailsID, nil, mg.err +} + +type mockColl struct { + id string // for comparisons + p path.Path + prevP path.Path +} + +func (mc mockColl) Items() <-chan data.Stream { + return nil +} + +func (mc mockColl) FullPath() path.Path { + return mc.p +} + +func (mc mockColl) PreviousPath() path.Path { + return mc.prevP +} + +func (mc mockColl) State() data.CollectionState { + return data.NewState +} + +func (mc mockColl) DoNotMergeItems() bool { + return false +} + +// --------------------------------------------------------------------------- +// tests +// --------------------------------------------------------------------------- + +type OperationsManifestsUnitSuite struct { + suite.Suite +} + +func TestOperationsManifestsUnitSuite(t *testing.T) { + suite.Run(t, new(OperationsManifestsUnitSuite)) +} + +func (suite *OperationsManifestsUnitSuite) TestCollectMetadata() { + const ( + ro = "owner" + tid = "tenantid" + ) + + var ( + emailPath = makeMetadataBasePath( + suite.T(), + tid, + path.ExchangeService, + ro, + path.EmailCategory) + contactPath = makeMetadataBasePath( + suite.T(), + tid, + path.ExchangeService, + ro, + path.ContactsCategory) + ) + + table := []struct { + name string + manID string + reasons []kopia.Reason + fileNames []string + expectPaths func(*testing.T, []string) []path.Path + expectErr error + }{ + { + name: "single reason, single file", + manID: "single single", + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + expectPaths: func(t *testing.T, files []string) []path.Path { + ps := make([]path.Path, 0, len(files)) + + for _, f := range files { + p, err := emailPath.Append(f, true) + assert.NoError(t, err) + ps = append(ps, p) + } + + return ps + }, + fileNames: []string{"a"}, + }, + { + name: "single reason, multiple files", + manID: "single multi", + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + expectPaths: func(t *testing.T, files []string) []path.Path { + ps := make([]path.Path, 0, len(files)) + + for _, f := range files { + p, err := emailPath.Append(f, true) + assert.NoError(t, err) + ps = append(ps, p) + } + + return ps + }, + fileNames: []string{"a", "b"}, + }, + { + name: "multiple reasons, single file", + manID: "multi single", + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + expectPaths: func(t *testing.T, files []string) []path.Path { + ps := make([]path.Path, 0, len(files)) + + for _, f := range files { + p, err := emailPath.Append(f, true) + assert.NoError(t, err) + ps = append(ps, p) + p, err = contactPath.Append(f, true) + assert.NoError(t, err) + ps = append(ps, p) + } + + return ps + }, + fileNames: []string{"a"}, + }, + { + name: "multiple reasons, multiple file", + manID: "multi multi", + reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + expectPaths: func(t *testing.T, files []string) []path.Path { + ps := make([]path.Path, 0, len(files)) + + for _, f := range files { + p, err := emailPath.Append(f, true) + assert.NoError(t, err) + ps = append(ps, p) + p, err = contactPath.Append(f, true) + assert.NoError(t, err) + ps = append(ps, p) + } + + return ps + }, + fileNames: []string{"a", "b"}, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + paths := test.expectPaths(t, test.fileNames) + + mr := mockRestorer{err: test.expectErr} + mr.buildRestoreFunc(t, test.manID, paths) + + man := &kopia.ManifestEntry{ + Manifest: &snapshot.Manifest{ID: manifest.ID(test.manID)}, + Reasons: test.reasons, + } + + _, err := collectMetadata(ctx, &mr, man, test.fileNames, tid) + assert.ErrorIs(t, err, test.expectErr) + }) + } +} + +func (suite *OperationsManifestsUnitSuite) TestVerifyDistinctBases() { + ro := "resource_owner" + + table := []struct { + name string + mans []*kopia.ManifestEntry + expect assert.ErrorAssertionFunc + }{ + { + name: "one manifest, one reason", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + }, + expect: assert.NoError, + }, + { + name: "one incomplete manifest", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{IncompleteReason: "ir"}, + }, + }, + expect: assert.NoError, + }, + { + name: "one manifest, multiple reasons", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + }, + }, + expect: assert.NoError, + }, + { + name: "one manifest, duplicate reasons", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + }, + expect: assert.Error, + }, + { + name: "two manifests, non-overlapping reasons", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.ContactsCategory, + }, + }, + }, + }, + expect: assert.NoError, + }, + { + name: "two manifests, overlapping reasons", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + }, + expect: assert.Error, + }, + { + name: "two manifests, overlapping reasons, one snapshot incomplete", + mans: []*kopia.ManifestEntry{ + { + Manifest: &snapshot.Manifest{}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + { + Manifest: &snapshot.Manifest{IncompleteReason: "ir"}, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: path.EmailCategory, + }, + }, + }, + }, + expect: assert.NoError, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + err := verifyDistinctBases(test.mans) + test.expect(t, err) + }) + } +} + +func (suite *OperationsManifestsUnitSuite) TestProduceManifestsAndMetadata() { + const ( + ro = "resourceowner" + tid = "tenantid" + did = "detailsid" + ) + + makeMan := func(pct path.CategoryType, id, incmpl, bid string) *kopia.ManifestEntry { + tags := map[string]string{} + if len(bid) > 0 { + tags = map[string]string{"tag:" + kopia.TagBackupID: bid} + } + + return &kopia.ManifestEntry{ + Manifest: &snapshot.Manifest{ + ID: manifest.ID(id), + IncompleteReason: incmpl, + Tags: tags, + }, + Reasons: []kopia.Reason{ + { + ResourceOwner: ro, + Service: path.ExchangeService, + Category: pct, + }, + }, + } + } + + table := []struct { + name string + mr mockManifestRestorer + gdi mockGetDetailsIDer + reasons []kopia.Reason + getMeta bool + assertErr assert.ErrorAssertionFunc + assertB assert.BoolAssertionFunc + expectDCS []data.Collection + expectNilMans bool + }{ + { + name: "don't get metadata, no mans", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: false, + assertErr: assert.NoError, + assertB: assert.False, + expectDCS: nil, + }, + { + name: "don't get metadata", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{makeMan(path.EmailCategory, "", "", "")}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: false, + assertErr: assert.NoError, + assertB: assert.False, + expectDCS: nil, + }, + { + name: "don't get metadata, incomplete manifest", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{makeMan(path.EmailCategory, "", "ir", "")}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: false, + assertErr: assert.NoError, + assertB: assert.False, + expectDCS: nil, + }, + { + name: "fetch manifests errors", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mrErr: assert.AnError, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.Error, + assertB: assert.False, + expectDCS: nil, + }, + { + name: "verify distinct bases fails", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{ + makeMan(path.EmailCategory, "", "", ""), + makeMan(path.EmailCategory, "", "", ""), + }, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, // No error, even though verify failed. + assertB: assert.False, + expectDCS: nil, + }, + { + name: "no manifests", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: nil, + }, + { + name: "only incomplete manifests", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{ + makeMan(path.EmailCategory, "", "ir", ""), + makeMan(path.ContactsCategory, "", "ir", ""), + }, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: nil, + }, + { + name: "man missing backup id", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{collsByID: map[string][]data.Collection{ + "id": {mockColl{id: "id_coll"}}, + }}, + mans: []*kopia.ManifestEntry{makeMan(path.EmailCategory, "id", "", "")}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.Error, + assertB: assert.False, + expectNilMans: true, + }, + { + name: "backup missing details id", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{}, + mans: []*kopia.ManifestEntry{makeMan(path.EmailCategory, "", "", "bid")}, + }, + gdi: mockGetDetailsIDer{}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.False, + }, + { + name: "one complete, one incomplete", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{collsByID: map[string][]data.Collection{ + "id": {mockColl{id: "id_coll"}}, + "incmpl_id": {mockColl{id: "incmpl_id_coll"}}, + }}, + mans: []*kopia.ManifestEntry{ + makeMan(path.EmailCategory, "id", "", "bid"), + makeMan(path.EmailCategory, "incmpl_id", "ir", ""), + }, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []data.Collection{mockColl{id: "id_coll"}}, + }, + { + name: "single valid man", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{collsByID: map[string][]data.Collection{ + "id": {mockColl{id: "id_coll"}}, + }}, + mans: []*kopia.ManifestEntry{makeMan(path.EmailCategory, "id", "", "bid")}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []data.Collection{mockColl{id: "id_coll"}}, + }, + { + name: "multiple valid mans", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{collsByID: map[string][]data.Collection{ + "mail": {mockColl{id: "mail_coll"}}, + "contact": {mockColl{id: "contact_coll"}}, + }}, + mans: []*kopia.ManifestEntry{ + makeMan(path.EmailCategory, "mail", "", "bid"), + makeMan(path.ContactsCategory, "contact", "", "bid"), + }, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.NoError, + assertB: assert.True, + expectDCS: []data.Collection{ + mockColl{id: "mail_coll"}, + mockColl{id: "contact_coll"}, + }, + }, + { + name: "error collecting metadata", + mr: mockManifestRestorer{ + mockRestorer: mockRestorer{err: assert.AnError}, + mans: []*kopia.ManifestEntry{makeMan(path.EmailCategory, "", "", "bid")}, + }, + gdi: mockGetDetailsIDer{detailsID: did}, + reasons: []kopia.Reason{}, + getMeta: true, + assertErr: assert.Error, + assertB: assert.False, + expectDCS: nil, + expectNilMans: true, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + ctx, flush := tester.NewContext() + defer flush() + + mans, dcs, b, err := produceManifestsAndMetadata( + ctx, + &test.mr, + &test.gdi, + test.reasons, + tid, + test.getMeta, + ) + test.assertErr(t, err) + test.assertB(t, b) + + expectMans := test.mr.mans + if test.expectNilMans { + expectMans = nil + } + assert.Equal(t, expectMans, mans) + + expect, got := []string{}, []string{} + + for _, dc := range test.expectDCS { + mc, ok := dc.(mockColl) + assert.True(t, ok) + + expect = append(expect, mc.id) + } + + for _, dc := range dcs { + mc, ok := dc.(mockColl) + assert.True(t, ok) + + got = append(got, mc.id) + } + + assert.ElementsMatch(t, expect, got, "expected collections are present") + }) + } +} From f01c8ad8437c1735e0cd52ee1f8ad925771bcecb Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 17 Jan 2023 13:51:57 -0800 Subject: [PATCH 33/38] Add logging for bases in incremental backups (#2151) ## Description Add log statements noting which bases were used for kopia assisted incrementals and which bases were merged into the hierarchy. Also record the reasons a base was chosen. Log statements when searching for previous snapshots will be added when that code is refactored ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [x] :broom: Tech Debt/Cleanup ## Issue(s) * #2149 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/kopia/upload.go | 12 ++++++++++++ src/internal/kopia/wrapper.go | 9 +++++++++ src/internal/operations/backup.go | 24 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/internal/kopia/upload.go b/src/internal/kopia/upload.go index 6a99b9898..d9320f2a6 100644 --- a/src/internal/kopia/upload.go +++ b/src/internal/kopia/upload.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/kopia/kopia/fs" "github.com/kopia/kopia/fs/virtualfs" + "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/snapshot/snapshotfs" "github.com/pkg/errors" @@ -884,6 +885,17 @@ func inflateDirTree( return nil, errors.Wrap(err, "inflating collection tree") } + baseIDs := make([]manifest.ID, 0, len(baseSnaps)) + for _, snap := range baseSnaps { + baseIDs = append(baseIDs, snap.ID) + } + + logger.Ctx(ctx).Infow( + "merging hierarchies from base snapshots", + "snapshot_ids", + baseIDs, + ) + for _, snap := range baseSnaps { if err = inflateBaseTree(ctx, loader, snap, updatedPaths, roots); err != nil { return nil, errors.Wrap(err, "inflating base snapshot tree(s)") diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index 3e8f5b338..0976bb40e 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -178,11 +178,20 @@ func (w Wrapper) makeSnapshotWithRoot( bc = &stats.ByteCounter{} ) + snapIDs := make([]manifest.ID, 0, len(prevSnapEntries)) prevSnaps := make([]*snapshot.Manifest, 0, len(prevSnapEntries)) + for _, ent := range prevSnapEntries { prevSnaps = append(prevSnaps, ent.Manifest) + snapIDs = append(snapIDs, ent.ID) } + logger.Ctx(ctx).Infow( + "using snapshots for kopia-assisted incrementals", + "snapshot_ids", + snapIDs, + ) + err := repo.WriteSession( ctx, w.c, diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 7f562321c..89dbb340d 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -340,6 +340,8 @@ func consumeBackupDataCollections( for _, m := range mans { paths := make([]*path.Builder, 0, len(m.Reasons)) + services := map[string]struct{}{} + categories := map[string]struct{}{} for _, reason := range m.Reasons { pb, err := builderFromReason(tenantID, reason) @@ -348,12 +350,34 @@ func consumeBackupDataCollections( } paths = append(paths, pb) + services[reason.Service.String()] = struct{}{} + categories[reason.Category.String()] = struct{}{} } bases = append(bases, kopia.IncrementalBase{ Manifest: m.Manifest, SubtreePaths: paths, }) + + svcs := make([]string, 0, len(services)) + for k := range services { + svcs = append(svcs, k) + } + + cats := make([]string, 0, len(categories)) + for k := range categories { + cats = append(cats, k) + } + + logger.Ctx(ctx).Infow( + "using base for backup", + "snapshot_id", + m.ID, + "services", + svcs, + "categories", + cats, + ) } return bu.BackupCollections(ctx, bases, cs, tags, isIncremental) From 4f8f76f1eb0fb09eca9498959645aab6f1350d78 Mon Sep 17 00:00:00 2001 From: ashmrtn Date: Tue, 17 Jan 2023 15:01:54 -0800 Subject: [PATCH 34/38] Add tags to checkpoints in kopia (#2153) ## Description Tags allow searching for previous snapshots (complete or otherwise). This in turn provides the ability to use kopia-assisted incrementals in more situations as partially completed backups will be reused for their cached data. Streaming files that have not been completely uploaded are not added to a checkpoint so there is no possibility of accidentally triggering kopia-assisted incrementals thus causing us to treat a partially uploaded file in a checkpoint as a "cached" version of the complete file ## Does this PR need a docs update or release note? - [ ] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [x] :no_entry: No ## Type of change - [x] :sunflower: Feature - [ ] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * closes #1674 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- src/internal/kopia/snapshot_manager.go | 3 +++ src/internal/kopia/wrapper.go | 31 +++++++++++++++++--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/internal/kopia/snapshot_manager.go b/src/internal/kopia/snapshot_manager.go index 8fc950dd2..b3bd2a151 100644 --- a/src/internal/kopia/snapshot_manager.go +++ b/src/internal/kopia/snapshot_manager.go @@ -24,6 +24,9 @@ const ( // (permalinks) // [1] https://github.com/kopia/kopia/blob/05e729a7858a6e86cb48ba29fb53cb6045efce2b/cli/command_snapshot_create.go#L169 userTagPrefix = "tag:" + + // Tag key applied to checkpoints (but not completed snapshots) in kopia. + checkpointTagKey = "checkpoint" ) type Reason struct { diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index 0976bb40e..a27b9a590 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -192,6 +192,24 @@ func (w Wrapper) makeSnapshotWithRoot( snapIDs, ) + checkpointTagK, checkpointTagV := makeTagKV(checkpointTagKey) + + tags := map[string]string{} + checkpointTags := map[string]string{ + checkpointTagK: checkpointTagV, + } + + for k, v := range addlTags { + mk, mv := makeTagKV(k) + + if len(v) == 0 { + v = mv + } + + tags[mk] = v + checkpointTags[mk] = v + } + err := repo.WriteSession( ctx, w.c, @@ -228,6 +246,7 @@ func (w Wrapper) makeSnapshotWithRoot( u := snapshotfs.NewUploader(rw) progress.UploadProgress = u.Progress u.Progress = progress + u.CheckpointLabels = checkpointTags man, err = u.Upload(innerCtx, root, policyTree, si, prevSnaps...) if err != nil { @@ -236,17 +255,7 @@ func (w Wrapper) makeSnapshotWithRoot( return err } - man.Tags = map[string]string{} - - for k, v := range addlTags { - mk, mv := makeTagKV(k) - - if len(v) == 0 { - v = mv - } - - man.Tags[mk] = v - } + man.Tags = tags if _, err := snapshot.SaveSnapshot(innerCtx, rw, man); err != nil { err = errors.Wrap(err, "saving snapshot") From 63b77e2bf52d641d053b281d3247d8875d9c8c58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jan 2023 05:55:55 +0000 Subject: [PATCH 35/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20github.com/aw?= =?UTF-8?q?s/aws-sdk-go=20from=201.44.180=20to=201.44.181=20in=20/src=20(#?= =?UTF-8?q?2157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.44.180 to 1.44.181.
Release notes

Sourced from github.com/aws/aws-sdk-go's releases.

Release v1.44.181 (2023-01-17)

Service Client Updates

  • service/billingconductor: Updates service API and documentation
  • service/cloud9: Updates service API
    • Added minimum value to AutomaticStopTimeMinutes parameter.
  • service/imagebuilder: Updates service API and documentation
  • service/network-firewall: Updates service API and documentation
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.44.180&new-version=1.44.181)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- src/go.mod | 2 +- src/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/go.mod b/src/go.mod index 194d1bc06..0ba54fea1 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,7 +6,7 @@ replace github.com/kopia/kopia => github.com/alcionai/kopia v0.10.8-0.2023011220 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 - github.com/aws/aws-sdk-go v1.44.180 + github.com/aws/aws-sdk-go v1.44.181 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 diff --git a/src/go.sum b/src/go.sum index 277e9650f..222bf48f8 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.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= -github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.181 h1:w4OzE8bwIVo62gUTAp/uEFO2HSsUtf1pjXpSs36cluY= +github.com/aws/aws-sdk-go v1.44.181/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= From 3a37584938527862c7698a8237baadec70b1fcbc Mon Sep 17 00:00:00 2001 From: Vaibhav Kamra Date: Tue, 17 Jan 2023 22:59:29 -0800 Subject: [PATCH 36/38] Check whether the user has an exchange mailbox (#2156) ## Description This commit adds logic in discovery and backup to check whether the specified user has an exchange mailbox that is available/enabled. If so - the backup is short-circuited to succeed but with "no data" Going forward - we should be able to move the logic in the OneDrive connector that checks for a valid drive and license in here. ## Does this PR need a docs update or release note? - [x] :white_check_mark: Yes, it's included - [ ] :clock1: Yes, but in a later PR - [ ] :no_entry: No ## Type of change - [ ] :sunflower: Feature - [x] :bug: Bugfix - [ ] :world_map: Documentation - [ ] :robot: Test - [ ] :computer: CI/Deployment - [ ] :broom: Tech Debt/Cleanup ## Issue(s) * #2145 ## Test Plan - [x] :muscle: Manual - [ ] :zap: Unit test - [ ] :green_heart: E2E --- CHANGELOG.md | 4 ++ src/internal/connector/data_collections.go | 35 +++++++++++++++ src/internal/connector/discovery/discovery.go | 44 +++++++++++++++++++ src/internal/connector/graph/errors.go | 16 ++++--- 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 352b8703c..56f24db17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] (alpha) +### Fixed + +- Check if the user specified for an exchange backup operation has a mailbox. + ## [v0.1.0] (alpha) - 2023-01-13 diff --git a/src/internal/connector/data_collections.go b/src/internal/connector/data_collections.go index abd3436b3..2ba2e59a4 100644 --- a/src/internal/connector/data_collections.go +++ b/src/internal/connector/data_collections.go @@ -7,7 +7,9 @@ import ( "github.com/pkg/errors" + "github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/exchange" + "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/sharepoint" "github.com/alcionai/corso/src/internal/connector/support" @@ -15,6 +17,7 @@ import ( D "github.com/alcionai/corso/src/internal/diagnostics" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/logger" + "github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -41,6 +44,15 @@ func (gc *GraphConnector) DataCollections( return nil, err } + serviceEnabled, err := checkServiceEnabled(ctx, gc.Service, path.ServiceType(sels.Service), sels.DiscreteOwner) + if err != nil { + return nil, err + } + + if !serviceEnabled { + return []data.Collection{}, nil + } + switch sels.Service { case selectors.ServiceExchange: colls, err := exchange.DataCollections( @@ -124,6 +136,29 @@ func verifyBackupInputs(sels selectors.Selector, userPNs, siteIDs []string) erro return nil } +func checkServiceEnabled( + ctx context.Context, + gs graph.Servicer, + service path.ServiceType, + resource string, +) (bool, error) { + if service == path.SharePointService { + // No "enabled" check required for sharepoint + return true, nil + } + + _, info, err := discovery.User(ctx, gs, resource) + if err != nil { + return false, err + } + + if _, ok := info.DiscoveredServices[service]; !ok { + return false, nil + } + + return true, nil +} + // --------------------------------------------------------------------------- // OneDrive // --------------------------------------------------------------------------- diff --git a/src/internal/connector/discovery/discovery.go b/src/internal/connector/discovery/discovery.go index 17ad10eb2..a9f2266d1 100644 --- a/src/internal/connector/discovery/discovery.go +++ b/src/internal/connector/discovery/discovery.go @@ -10,6 +10,7 @@ import ( "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/pkg/path" ) const ( @@ -64,6 +65,49 @@ func Users(ctx context.Context, gs graph.Servicer, tenantID string) ([]models.Us return users, iterErrs } +type UserInfo struct { + DiscoveredServices map[path.ServiceType]struct{} +} + +func User(ctx context.Context, gs graph.Servicer, userID string) (models.Userable, *UserInfo, error) { + user, err := gs.Client().UsersById(userID).Get(ctx, nil) + if err != nil { + return nil, nil, errors.Wrapf( + err, + "retrieving resource for tenant: %s", + support.ConnectorStackErrorTrace(err), + ) + } + + // Assume all services are enabled + userInfo := &UserInfo{ + DiscoveredServices: map[path.ServiceType]struct{}{ + path.ExchangeService: {}, + path.OneDriveService: {}, + }, + } + + // Discover which services the user has enabled + + // Exchange: Query `MailFolders` + _, err = gs.Client().UsersById(userID).MailFolders().Get(ctx, nil) + if err != nil { + if !graph.IsErrExchangeMailFolderNotFound(err) { + return nil, nil, errors.Wrapf( + err, + "retrieving mail folders for tenant: %s", + support.ConnectorStackErrorTrace(err), + ) + } + + delete(userInfo.DiscoveredServices, path.ExchangeService) + } + + // TODO: OneDrive + + return user, userInfo, nil +} + // parseUser extracts information from `models.Userable` we care about func parseUser(item interface{}) (models.Userable, error) { m, ok := item.(models.Userable) diff --git a/src/internal/connector/graph/errors.go b/src/internal/connector/graph/errors.go index cf2df3556..1b0d86b85 100644 --- a/src/internal/connector/graph/errors.go +++ b/src/internal/connector/graph/errors.go @@ -15,11 +15,13 @@ import ( // --------------------------------------------------------------------------- const ( - errCodeItemNotFound = "ErrorItemNotFound" - errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" - errCodeResyncRequired = "ResyncRequired" - errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound" - errCodeSyncStateNotFound = "SyncStateNotFound" + errCodeItemNotFound = "ErrorItemNotFound" + errCodeEmailFolderNotFound = "ErrorSyncFolderNotFound" + errCodeResyncRequired = "ResyncRequired" + errCodeSyncFolderNotFound = "ErrorSyncFolderNotFound" + errCodeSyncStateNotFound = "SyncStateNotFound" + errCodeResourceNotFound = "ResourceNotFound" + errCodeMailboxNotEnabledForRESTAPI = "MailboxNotEnabledForRESTAPI" ) // The folder or item was deleted between the time we identified @@ -69,6 +71,10 @@ func asInvalidDelta(err error) bool { return errors.As(err, &e) } +func IsErrExchangeMailFolderNotFound(err error) bool { + return hasErrorCode(err, errCodeResourceNotFound, errCodeMailboxNotEnabledForRESTAPI) +} + // Timeout errors are identified for tracking the need to retry calls. // Other delay errors, like throttling, are already handled by the // graph client's built-in retries. From dc17c680749b35312686c2d610fa1aaa25ad0099 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 18 Jan 2023 09:08:00 -0500 Subject: [PATCH 37/38] Middleware: Context timeout expansion (#2048) ## Description Adds additional definitions for context timeouts for middleware usage ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :bug: Bugfix ## Issue(s) *related #2047 ## Test Plan - [x] :muscle: Manual --- src/internal/connector/graph/errors.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/internal/connector/graph/errors.go b/src/internal/connector/graph/errors.go index 1b0d86b85..86cec64bd 100644 --- a/src/internal/connector/graph/errors.go +++ b/src/internal/connector/graph/errors.go @@ -1,7 +1,9 @@ package graph import ( + "context" "net/url" + "os" "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" "github.com/pkg/errors" @@ -126,6 +128,10 @@ func hasErrorCode(err error, codes ...string) bool { // 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() From e3b6d035fb259b88f9876226a286a23e5f0dcf2f Mon Sep 17 00:00:00 2001 From: Keepers Date: Wed, 18 Jan 2023 09:43:19 -0700 Subject: [PATCH 38/38] extra loggng and error wraps (#2155) ## Description Adds info logging on all throttling responses, and some extra error handling in container resolvers. ## Does this PR need a docs update or release note? - [x] :no_entry: No ## Type of change - [x] :broom: Tech Debt/Cleanup ## Test Plan - [x] :muscle: Manual --- .../connector/exchange/contact_folder_cache.go | 12 +++++------- .../connector/exchange/event_calendar_cache.go | 8 ++++---- .../connector/exchange/mail_folder_cache.go | 10 +++++----- src/internal/connector/graph/service_helper.go | 13 +++++++++---- src/internal/connector/support/errors.go | 16 ++++++++++++++-- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/internal/connector/exchange/contact_folder_cache.go b/src/internal/connector/exchange/contact_folder_cache.go index 9ac83e665..b2c077a2e 100644 --- a/src/internal/connector/exchange/contact_folder_cache.go +++ b/src/internal/connector/exchange/contact_folder_cache.go @@ -26,15 +26,13 @@ func (cfc *contactFolderCache) populateContactRoot( ) error { f, err := cfc.getter.GetContainerByID(ctx, cfc.userID, directoryID) if err != nil { - return errors.Wrapf( - err, - "fetching root contact folder: "+support.ConnectorStackErrorTrace(err)) + return support.ConnectorStackErrorTraceWrap(err, "fetching root folder") } temp := graph.NewCacheFolder(f, path.Builder{}.Append(baseContainerPath...)) if err := cfc.addFolder(temp); err != nil { - return errors.Wrap(err, "adding cache root") + return errors.Wrap(err, "adding resolver dir") } return nil @@ -50,16 +48,16 @@ func (cfc *contactFolderCache) Populate( baseContainerPather ...string, ) error { if err := cfc.init(ctx, baseID, baseContainerPather); err != nil { - return err + return errors.Wrap(err, "initializing") } err := cfc.enumer.EnumerateContainers(ctx, cfc.userID, baseID, cfc.addFolder) if err != nil { - return err + return errors.Wrap(err, "enumerating containers") } if err := cfc.populatePaths(ctx); err != nil { - return errors.Wrap(err, "contacts resolver") + return errors.Wrap(err, "populating paths") } return nil diff --git a/src/internal/connector/exchange/event_calendar_cache.go b/src/internal/connector/exchange/event_calendar_cache.go index c383a74e6..2b4e1b22d 100644 --- a/src/internal/connector/exchange/event_calendar_cache.go +++ b/src/internal/connector/exchange/event_calendar_cache.go @@ -31,7 +31,7 @@ func (ecc *eventCalendarCache) Populate( err := ecc.enumer.EnumerateContainers(ctx, ecc.userID, "", ecc.addFolder) if err != nil { - return err + return errors.Wrap(err, "enumerating containers") } return nil @@ -41,20 +41,20 @@ func (ecc *eventCalendarCache) Populate( // @returns error iff the required values are not accessible. func (ecc *eventCalendarCache) AddToCache(ctx context.Context, f graph.Container) error { if err := checkIDAndName(f); err != nil { - return errors.Wrap(err, "adding cache folder") + return errors.Wrap(err, "validating container") } temp := graph.NewCacheFolder(f, path.Builder{}.Append(*f.GetDisplayName())) if err := ecc.addFolder(temp); err != nil { - return errors.Wrap(err, "adding cache folder") + return errors.Wrap(err, "adding container") } // Populate the path for this entry so calls to PathInCache succeed no matter // when they're made. _, err := ecc.IDToPath(ctx, *f.GetId()) if err != nil { - return errors.Wrap(err, "adding cache entry") + return errors.Wrap(err, "setting path to container id") } return nil diff --git a/src/internal/connector/exchange/mail_folder_cache.go b/src/internal/connector/exchange/mail_folder_cache.go index a2b92593c..06d4b1285 100644 --- a/src/internal/connector/exchange/mail_folder_cache.go +++ b/src/internal/connector/exchange/mail_folder_cache.go @@ -35,7 +35,7 @@ func (mc *mailFolderCache) populateMailRoot( f, err := mc.getter.GetContainerByID(ctx, mc.userID, fldr) if err != nil { - return errors.Wrap(err, "fetching root folder"+support.ConnectorStackErrorTrace(err)) + return support.ConnectorStackErrorTraceWrap(err, "fetching root folder") } if fldr == DefaultMailFolder { @@ -44,7 +44,7 @@ func (mc *mailFolderCache) populateMailRoot( temp := graph.NewCacheFolder(f, path.Builder{}.Append(directory)) if err := mc.addFolder(temp); err != nil { - return errors.Wrap(err, "initializing mail resolver") + return errors.Wrap(err, "adding resolver dir") } } @@ -62,16 +62,16 @@ func (mc *mailFolderCache) Populate( baseContainerPath ...string, ) error { if err := mc.init(ctx); err != nil { - return err + return errors.Wrap(err, "initializing") } err := mc.enumer.EnumerateContainers(ctx, mc.userID, "", mc.addFolder) if err != nil { - return err + return errors.Wrap(err, "enumerating containers") } if err := mc.populatePaths(ctx); err != nil { - return errors.Wrap(err, "mail resolver") + return errors.Wrap(err, "populating paths") } return nil diff --git a/src/internal/connector/graph/service_helper.go b/src/internal/connector/graph/service_helper.go index db39fcb34..76ff54ad4 100644 --- a/src/internal/connector/graph/service_helper.go +++ b/src/internal/connector/graph/service_helper.go @@ -1,7 +1,7 @@ package graph import ( - nethttp "net/http" + "net/http" "net/http/httputil" "os" "strings" @@ -47,7 +47,7 @@ func CreateAdapter(tenant, client, secret string) (*msgraphsdk.GraphRequestAdapt } // CreateHTTPClient creates the httpClient with middlewares and timeout configured -func CreateHTTPClient() *nethttp.Client { +func CreateHTTPClient() *http.Client { clientOptions := msgraphsdk.GetDefaultClientOptions() middlewares := msgraphgocore.GetDefaultMiddlewaresWithOptions(&clientOptions) middlewares = append(middlewares, &LoggingMiddleware{}) @@ -67,8 +67,8 @@ type LoggingMiddleware struct{} func (handler *LoggingMiddleware) Intercept( pipeline khttp.Pipeline, middlewareIndex int, - req *nethttp.Request, -) (*nethttp.Response, error) { + req *http.Request, +) (*http.Response, error) { var ( ctx = req.Context() resp, err = pipeline.Next(req, middlewareIndex) @@ -82,6 +82,11 @@ func (handler *LoggingMiddleware) Intercept( 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 logger.DebugAPI || os.Getenv(logGraphRequestsEnvKey) != "" { respDump, _ := httputil.DumpResponse(resp, true) diff --git a/src/internal/connector/support/errors.go b/src/internal/connector/support/errors.go index 4289e5058..8f73ea8fa 100644 --- a/src/internal/connector/support/errors.go +++ b/src/internal/connector/support/errors.go @@ -89,8 +89,20 @@ func concatenateStringFromPointers(orig string, pointers []*string) string { return orig } -// ConnectorStackErrorTrace is a helper function that wraps the -// stack trace for oDataError types from querying the M365 back store. +// ConnectorStackErrorTraceWrap is a helper function that wraps the +// stack trace for oDataErrors (if the error has one) onto the prefix. +// If no stack trace is found, wraps the error with only the prefix. +func ConnectorStackErrorTraceWrap(e error, prefix string) error { + cset := ConnectorStackErrorTrace(e) + if len(cset) > 0 { + return errors.Wrap(e, prefix+": "+cset) + } + + return errors.Wrap(e, prefix) +} + +// ConnectorStackErrorTracew is a helper function that extracts +// the stack trace for oDataErrors, if the error has one. func ConnectorStackErrorTrace(e error) string { eMessage := ""