Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4bb22f498 | ||
|
|
6a1dbd388b | ||
|
|
f3721e9e5d |
13
CHANGELOG.md
13
CHANGELOG.md
@ -12,6 +12,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Document Corso's fault-tolerance and restartability features
|
- Document Corso's fault-tolerance and restartability features
|
||||||
- Add retries on timeouts and status code 500 for Exchange
|
- Add retries on timeouts and status code 500 for Exchange
|
||||||
- Increase page size preference for delta requests for Exchange to reduce number of roundtrips
|
- Increase page size preference for delta requests for Exchange to reduce number of roundtrips
|
||||||
|
- OneDrive file/folder permissions can now be backed up and restored
|
||||||
|
- Add `--restore-permissions` flag to toggle restoration of OneDrive permissions
|
||||||
|
- Add versions to backups so that we can understand/handle older backup formats
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Backing up a calendar that has the same name as the default calendar
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
- When the same user has permissions to a file and the containing
|
||||||
|
folder, we only restore folder level permissions for the user and no
|
||||||
|
separate file only permission is restored.
|
||||||
|
- Link shares are not restored
|
||||||
|
|
||||||
## [v0.2.0] (alpha) - 2023-1-29
|
## [v0.2.0] (alpha) - 2023-1-29
|
||||||
|
|
||||||
|
|||||||
@ -501,10 +501,11 @@ func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() {
|
|||||||
directoryCaches = make(map[path.CategoryType]graph.ContainerResolver)
|
directoryCaches = make(map[path.CategoryType]graph.ContainerResolver)
|
||||||
folderName = tester.DefaultTestRestoreDestination().ContainerName
|
folderName = tester.DefaultTestRestoreDestination().ContainerName
|
||||||
tests = []struct {
|
tests = []struct {
|
||||||
name string
|
name string
|
||||||
pathFunc1 func(t *testing.T) path.Path
|
pathFunc1 func(t *testing.T) path.Path
|
||||||
pathFunc2 func(t *testing.T) path.Path
|
pathFunc2 func(t *testing.T) path.Path
|
||||||
category path.CategoryType
|
category path.CategoryType
|
||||||
|
folderPrefix string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Mail Cache Test",
|
name: "Mail Cache Test",
|
||||||
@ -587,6 +588,7 @@ func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return aPath
|
return aPath
|
||||||
},
|
},
|
||||||
|
folderPrefix: calendarOthersFolder,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -617,8 +619,9 @@ func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() {
|
|||||||
_, err = resolver.IDToPath(ctx, secondID)
|
_, err = resolver.IDToPath(ctx, secondID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, ok := resolver.PathInCache(folderName)
|
p := stdpath.Join(test.folderPrefix, folderName)
|
||||||
require.True(t, ok)
|
_, ok := resolver.PathInCache(p)
|
||||||
|
require.True(t, ok, "looking for path in cache: %s", p)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -537,9 +537,9 @@ func (suite *DataCollectionsIntegrationSuite) TestEventsSerializationRegression(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Birthday Calendar",
|
name: "Birthday Calendar",
|
||||||
expected: "Birthdays",
|
expected: calendarOthersFolder + "/Birthdays",
|
||||||
scope: selectors.NewExchangeBackup(users).EventCalendars(
|
scope: selectors.NewExchangeBackup(users).EventCalendars(
|
||||||
[]string{"Birthdays"},
|
[]string{calendarOthersFolder + "/Birthdays"},
|
||||||
selectors.PrefixMatch(),
|
selectors.PrefixMatch(),
|
||||||
)[0],
|
)[0],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,7 +64,15 @@ func (ecc *eventCalendarCache) Populate(
|
|||||||
return errors.Wrap(err, "initializing")
|
return errors.Wrap(err, "initializing")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ecc.enumer.EnumerateContainers(ctx, ecc.userID, "", ecc.addFolder)
|
err := ecc.enumer.EnumerateContainers(
|
||||||
|
ctx,
|
||||||
|
ecc.userID,
|
||||||
|
"",
|
||||||
|
func(cf graph.CacheFolder) error {
|
||||||
|
cf.SetPath(path.Builder{}.Append(calendarOthersFolder, *cf.GetDisplayName()))
|
||||||
|
return ecc.addFolder(cf)
|
||||||
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "enumerating containers")
|
return errors.Wrap(err, "enumerating containers")
|
||||||
}
|
}
|
||||||
@ -83,7 +91,7 @@ func (ecc *eventCalendarCache) AddToCache(ctx context.Context, f graph.Container
|
|||||||
return errors.Wrap(err, "validating container")
|
return errors.Wrap(err, "validating container")
|
||||||
}
|
}
|
||||||
|
|
||||||
temp := graph.NewCacheFolder(f, path.Builder{}.Append(*f.GetDisplayName()))
|
temp := graph.NewCacheFolder(f, path.Builder{}.Append(calendarOthersFolder, *f.GetDisplayName()))
|
||||||
|
|
||||||
if err := ecc.addFolder(temp); err != nil {
|
if err := ecc.addFolder(temp); err != nil {
|
||||||
return errors.Wrap(err, "adding container")
|
return errors.Wrap(err, "adding container")
|
||||||
|
|||||||
@ -38,4 +38,5 @@ const (
|
|||||||
rootFolderAlias = "msgfolderroot"
|
rootFolderAlias = "msgfolderroot"
|
||||||
DefaultContactFolder = "Contacts"
|
DefaultContactFolder = "Contacts"
|
||||||
DefaultCalendar = "Calendar"
|
DefaultCalendar = "Calendar"
|
||||||
|
calendarOthersFolder = "Other Calendars"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -637,7 +637,11 @@ func establishEventsRestoreLocation(
|
|||||||
user string,
|
user string,
|
||||||
isNewCache bool,
|
isNewCache bool,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
cached, ok := ecc.PathInCache(folders[0])
|
// Need to prefix with the "Other Calendars" folder so lookup happens properly.
|
||||||
|
cached, ok := ecc.PathInCache(path.Builder{}.Append(
|
||||||
|
calendarOthersFolder,
|
||||||
|
folders[0],
|
||||||
|
).String())
|
||||||
if ok {
|
if ok {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package operations
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -106,7 +107,13 @@ type detailsWriter interface {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Run begins a synchronous backup operation.
|
// Run begins a synchronous backup operation.
|
||||||
func (op *BackupOperation) Run(ctx context.Context) error {
|
func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
ctx, end := D.Span(ctx, "operations:backup:run")
|
ctx, end := D.Span(ctx, "operations:backup:run")
|
||||||
defer func() {
|
defer func() {
|
||||||
end()
|
end()
|
||||||
@ -160,6 +167,12 @@ func (op *BackupOperation) do(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
// persist operation results to the model store on exit
|
// persist operation results to the model store on exit
|
||||||
defer func() {
|
defer func() {
|
||||||
|
// panic recovery here prevents additional errors in op.persistResults()
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = op.persistResults(startTime, &opStats)
|
err = op.persistResults(startTime, &opStats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
op.Errors.Fail(errors.Wrap(err, "persisting backup results"))
|
op.Errors.Fail(errors.Wrap(err, "persisting backup results"))
|
||||||
@ -189,6 +202,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) {
|
|||||||
op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics"))
|
op.Errors.Fail(errors.Wrap(err, "collecting manifest heuristics"))
|
||||||
opStats.readErr = op.Errors.Err()
|
opStats.readErr = op.Errors.Err()
|
||||||
|
|
||||||
|
logger.Ctx(ctx).With("err", err).Errorw("producing manifests and metadata", clues.InErr(err).Slice()...)
|
||||||
|
|
||||||
return opStats.readErr
|
return opStats.readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +212,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) {
|
|||||||
op.Errors.Fail(errors.Wrap(err, "connecting to m365"))
|
op.Errors.Fail(errors.Wrap(err, "connecting to m365"))
|
||||||
opStats.readErr = op.Errors.Err()
|
opStats.readErr = op.Errors.Err()
|
||||||
|
|
||||||
|
logger.Ctx(ctx).With("err", err).Errorw("connectng to m365", clues.InErr(err).Slice()...)
|
||||||
|
|
||||||
return opStats.readErr
|
return opStats.readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +222,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) {
|
|||||||
op.Errors.Fail(errors.Wrap(err, "retrieving data to backup"))
|
op.Errors.Fail(errors.Wrap(err, "retrieving data to backup"))
|
||||||
opStats.readErr = op.Errors.Err()
|
opStats.readErr = op.Errors.Err()
|
||||||
|
|
||||||
|
logger.Ctx(ctx).With("err", err).Errorw("producing backup data collections", clues.InErr(err).Slice()...)
|
||||||
|
|
||||||
return opStats.readErr
|
return opStats.readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +242,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) {
|
|||||||
op.Errors.Fail(errors.Wrap(err, "backing up service data"))
|
op.Errors.Fail(errors.Wrap(err, "backing up service data"))
|
||||||
opStats.writeErr = op.Errors.Err()
|
opStats.writeErr = op.Errors.Err()
|
||||||
|
|
||||||
|
logger.Ctx(ctx).With("err", err).Errorw("persisting collection backups", clues.InErr(err).Slice()...)
|
||||||
|
|
||||||
return opStats.writeErr
|
return opStats.writeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +258,8 @@ func (op *BackupOperation) do(ctx context.Context) (err error) {
|
|||||||
op.Errors.Fail(errors.Wrap(err, "merging backup details"))
|
op.Errors.Fail(errors.Wrap(err, "merging backup details"))
|
||||||
opStats.writeErr = op.Errors.Err()
|
opStats.writeErr = op.Errors.Err()
|
||||||
|
|
||||||
|
logger.Ctx(ctx).With("err", err).Errorw("merging details", clues.InErr(err).Slice()...)
|
||||||
|
|
||||||
return opStats.writeErr
|
return opStats.writeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,24 +451,22 @@ func consumeBackupDataCollections(
|
|||||||
cs,
|
cs,
|
||||||
nil,
|
nil,
|
||||||
tags,
|
tags,
|
||||||
isIncremental,
|
isIncremental)
|
||||||
)
|
if err != nil {
|
||||||
|
if kopiaStats == nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, nil, errors.Wrapf(
|
||||||
|
err,
|
||||||
|
"kopia snapshot failed with %v catastrophic errors and %v ignored errors",
|
||||||
|
kopiaStats.ErrorCount, kopiaStats.IgnoredErrorCount)
|
||||||
|
}
|
||||||
|
|
||||||
if kopiaStats.ErrorCount > 0 || kopiaStats.IgnoredErrorCount > 0 {
|
if kopiaStats.ErrorCount > 0 || kopiaStats.IgnoredErrorCount > 0 {
|
||||||
if err != nil {
|
err = errors.Errorf(
|
||||||
err = errors.Wrapf(
|
"kopia snapshot failed with %v catastrophic errors and %v ignored errors",
|
||||||
err,
|
kopiaStats.ErrorCount, kopiaStats.IgnoredErrorCount)
|
||||||
"kopia snapshot failed with %v catastrophic errors and %v ignored errors",
|
|
||||||
kopiaStats.ErrorCount,
|
|
||||||
kopiaStats.IgnoredErrorCount,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
err = errors.Errorf(
|
|
||||||
"kopia snapshot failed with %v catastrophic errors and %v ignored errors",
|
|
||||||
kopiaStats.ErrorCount,
|
|
||||||
kopiaStats.IgnoredErrorCount,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return kopiaStats, deets, itemsSourcedFromBase, err
|
return kopiaStats, deets, itemsSourcedFromBase, err
|
||||||
@ -589,15 +610,21 @@ func (op *BackupOperation) persistResults(
|
|||||||
opStats.writeErr)
|
opStats.writeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
op.Results.BytesRead = opStats.k.TotalHashedBytes
|
||||||
|
op.Results.BytesUploaded = opStats.k.TotalUploadedBytes
|
||||||
|
op.Results.ItemsWritten = opStats.k.TotalFileCount
|
||||||
|
op.Results.ResourceOwners = opStats.resourceCount
|
||||||
|
|
||||||
|
if opStats.gc == nil {
|
||||||
|
op.Status = Failed
|
||||||
|
return errors.New("data population never completed")
|
||||||
|
}
|
||||||
|
|
||||||
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
|
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
|
||||||
op.Status = NoData
|
op.Status = NoData
|
||||||
}
|
}
|
||||||
|
|
||||||
op.Results.BytesRead = opStats.k.TotalHashedBytes
|
|
||||||
op.Results.BytesUploaded = opStats.k.TotalUploadedBytes
|
|
||||||
op.Results.ItemsRead = opStats.gc.Successful
|
op.Results.ItemsRead = opStats.gc.Successful
|
||||||
op.Results.ItemsWritten = opStats.k.TotalFileCount
|
|
||||||
op.Results.ResourceOwners = opStats.resourceCount
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package operations
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
@ -106,6 +107,12 @@ type restorer interface {
|
|||||||
|
|
||||||
// Run begins a synchronous restore operation.
|
// Run begins a synchronous restore operation.
|
||||||
func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.Details, err error) {
|
func (op *RestoreOperation) Run(ctx context.Context) (restoreDetails *details.Details, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
ctx, end := D.Span(ctx, "operations:restore:run")
|
ctx, end := D.Span(ctx, "operations:restore:run")
|
||||||
defer func() {
|
defer func() {
|
||||||
end()
|
end()
|
||||||
@ -143,6 +150,12 @@ func (op *RestoreOperation) do(ctx context.Context) (restoreDetails *details.Det
|
|||||||
)
|
)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
// panic recovery here prevents additional errors in op.persistResults()
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = clues.Wrap(r.(error), "panic recovery").WithClues(ctx).With("stacktrace", debug.Stack())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = op.persistResults(ctx, startTime, &opStats)
|
err = op.persistResults(ctx, startTime, &opStats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -250,14 +263,20 @@ func (op *RestoreOperation) persistResults(
|
|||||||
opStats.writeErr)
|
opStats.writeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
op.Results.BytesRead = opStats.bytesRead.NumBytes
|
||||||
|
op.Results.ItemsRead = len(opStats.cs) // TODO: file count, not collection count
|
||||||
|
op.Results.ResourceOwners = opStats.resourceCount
|
||||||
|
|
||||||
|
if opStats.gc == nil {
|
||||||
|
op.Status = Failed
|
||||||
|
return errors.New("data restoration never completed")
|
||||||
|
}
|
||||||
|
|
||||||
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
|
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
|
||||||
op.Status = NoData
|
op.Status = NoData
|
||||||
}
|
}
|
||||||
|
|
||||||
op.Results.BytesRead = opStats.bytesRead.NumBytes
|
|
||||||
op.Results.ItemsRead = len(opStats.cs) // TODO: file count, not collection count
|
|
||||||
op.Results.ItemsWritten = opStats.gc.Successful
|
op.Results.ItemsWritten = opStats.gc.Successful
|
||||||
op.Results.ResourceOwners = opStats.resourceCount
|
|
||||||
|
|
||||||
dur := op.Results.CompletedAt.Sub(op.Results.StartedAt)
|
dur := op.Results.CompletedAt.Sub(op.Results.StartedAt)
|
||||||
|
|
||||||
|
|||||||
@ -87,11 +87,12 @@ func (e *Errors) Fail(err error) *Errors {
|
|||||||
// setErr handles setting errors.err. Sync locking gets
|
// setErr handles setting errors.err. Sync locking gets
|
||||||
// handled upstream of this call.
|
// handled upstream of this call.
|
||||||
func (e *Errors) setErr(err error) *Errors {
|
func (e *Errors) setErr(err error) *Errors {
|
||||||
if e.err != nil {
|
if e.err == nil {
|
||||||
return e.addErr(err)
|
e.err = err
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
e.err = err
|
e.errs = append(e.errs, err)
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,8 @@ func (suite *FaultErrorsUnitSuite) TestErr() {
|
|||||||
suite.T().Run(test.name, func(t *testing.T) {
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
n := fault.New(test.failFast)
|
n := fault.New(test.failFast)
|
||||||
require.NotNil(t, n)
|
require.NotNil(t, n)
|
||||||
|
require.NoError(t, n.Err())
|
||||||
|
require.Empty(t, n.Errs())
|
||||||
|
|
||||||
e := n.Fail(test.fail)
|
e := n.Fail(test.fail)
|
||||||
require.NotNil(t, e)
|
require.NotNil(t, e)
|
||||||
@ -90,6 +92,8 @@ func (suite *FaultErrorsUnitSuite) TestFail() {
|
|||||||
|
|
||||||
n := fault.New(false)
|
n := fault.New(false)
|
||||||
require.NotNil(t, n)
|
require.NotNil(t, n)
|
||||||
|
require.NoError(t, n.Err())
|
||||||
|
require.Empty(t, n.Errs())
|
||||||
|
|
||||||
n.Fail(assert.AnError)
|
n.Fail(assert.AnError)
|
||||||
assert.Error(t, n.Err())
|
assert.Error(t, n.Err())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user