Update attachments for events which drift from series master (#3644)

This updates any changes to attachments for individual event instances
for the ones that differ from the series master.<!-- PR description-->

---

#### Does this PR need a docs update or release note?

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* fixes https://github.com/alcionai/corso/issues/2835

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [ ]  Unit test
- [x] 💚 E2E
This commit is contained in:
Abin Simon 2023-06-22 19:07:05 +05:30 committed by GitHub
parent c8ae50cb2e
commit b0305a5319
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 338 additions and 77 deletions

View File

@ -25,9 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Do not display all the items that we restored at the end if there are more than 15. You can override this with `--verbose`.
### Known Issues
- Changes to attachments in instances of recurring events compared to the series master aren't restored
## [v0.8.0] (beta) - 2023-05-15
### Added

View File

@ -116,7 +116,7 @@ func handleExchangeCalendarEventFactory(cmd *cobra.Command, args []string) error
User, subject, body, body,
exchMock.NoOriginalStartDate, now, now,
exchMock.NoRecurrence, exchMock.NoAttendees,
false, exchMock.NoCancelledOccurrences,
exchMock.NoAttachments, exchMock.NoCancelledOccurrences,
exchMock.NoExceptionOccurrences)
},
control.Defaults(),

View File

@ -1,6 +1,7 @@
package exchange
import (
"bytes"
"context"
"fmt"
"strings"
@ -15,6 +16,7 @@ import (
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
@ -117,7 +119,15 @@ func (h eventRestoreHandler) restore(
}
// Fix up event instances in case we have a recurring event
err = updateRecurringEvents(ctx, h.ac, userID, destinationID, ptr.Val(item.GetId()), event)
err = updateRecurringEvents(
ctx,
h.ac,
userID,
destinationID,
ptr.Val(item.GetId()),
event,
errs,
)
if err != nil {
return nil, clues.Stack(err)
}
@ -133,6 +143,7 @@ func updateRecurringEvents(
ac api.Events,
userID, containerID, itemID string,
event models.Eventable,
errs *fault.Bus,
) error {
if event.GetRecurrence() == nil {
return nil
@ -149,7 +160,7 @@ func updateRecurringEvents(
return clues.Wrap(err, "update cancelled occurrences")
}
err = updateExceptionOccurrences(ctx, ac, userID, itemID, exceptionOccurrences)
err = updateExceptionOccurrences(ctx, ac, userID, containerID, itemID, exceptionOccurrences, errs)
if err != nil {
return clues.Wrap(err, "update exception occurrences")
}
@ -164,8 +175,10 @@ func updateExceptionOccurrences(
ctx context.Context,
ac api.Events,
userID string,
containerID string,
itemID string,
exceptionOccurrences any,
errs *fault.Bus,
) error {
if exceptionOccurrences == nil {
return nil
@ -193,9 +206,11 @@ func updateExceptionOccurrences(
startStr := dttm.FormatTo(start, dttm.DateOnly)
endStr := dttm.FormatTo(start.Add(24*time.Hour), dttm.DateOnly)
ictx := clues.Add(ctx, "event_instance_id", ptr.Val(evt.GetId()), "event_instance_date", start)
// Get all instances on the day of the instance which should
// just the one we need to modify
evts, err := ac.GetItemInstances(ctx, userID, itemID, startStr, endStr)
instances, err := ac.GetItemInstances(ictx, userID, itemID, startStr, endStr)
if err != nil {
return clues.Wrap(err, "getting instances")
}
@ -203,27 +218,141 @@ func updateExceptionOccurrences(
// Since the min recurrence interval is 1 day and we are
// querying for only a single day worth of instances, we
// should not have more than one instance here.
if len(evts) != 1 {
if len(instances) != 1 {
return clues.New("invalid number of instances for modified").
With("instances_count", len(evts), "search_start", startStr, "search_end", endStr)
With("instances_count", len(instances), "search_start", startStr, "search_end", endStr)
}
evt = toEventSimplified(evt)
// TODO(meain): Update attachments (might have to diff the
// attachments using ids and delete or add). We will have
// to get the id of the existing attachments, diff them
// with what we need a then create/delete items kinda like
// permissions
_, err = ac.PatchItem(ctx, userID, ptr.Val(evts[0].GetId()), evt)
_, err = ac.PatchItem(ictx, userID, ptr.Val(instances[0].GetId()), evt)
if err != nil {
return clues.Wrap(err, "updating event instance")
}
// We are creating event again from map as `toEventSimplified`
// removed the attachments and creating a clone from start of
// the event is non-trivial
evt, err = api.EventFromMap(instance)
if err != nil {
return clues.Wrap(err, "parsing event instance")
}
err = updateAttachments(ictx, ac, userID, containerID, ptr.Val(instances[0].GetId()), evt, errs)
if err != nil {
return clues.Wrap(err, "updating event instance attachments")
}
}
return nil
}
// updateAttachments updates the attachments of an event to match what
// is present in the backed up event. Ideally we could make use of the
// id of the series master event's attachments to see if we had
// added/removed any attachments, but as soon an event is modified,
// the id changes which makes the ids unusable. In this function, we
// use the name and content bytes to detect the changes. This function
// can be used to update the attachments of any event irrespective of
// whether they are event instances of a series master although for
// newer event, since we probably won't already have any events it
// would be better use Post[Small|Large]Attachment.
func updateAttachments(
ctx context.Context,
client api.Events,
userID, containerID, eventID string,
event models.Eventable,
errs *fault.Bus,
) error {
el := errs.Local()
attachments, err := client.GetAttachments(ctx, false, userID, eventID)
if err != nil {
return clues.Wrap(err, "getting attachments")
}
// Delete attachments that are not present in the backup but are
// present in the event(ones that were automatically inherited
// from series master).
for _, att := range attachments {
if el.Failure() != nil {
return el.Failure()
}
name := ptr.Val(att.GetName())
id := ptr.Val(att.GetId())
content, err := api.GetAttachmentContent(att)
if err != nil {
return clues.Wrap(err, "getting attachment").With("attachment_id", id)
}
found := false
for _, nAtt := range event.GetAttachments() {
nName := ptr.Val(nAtt.GetName())
nContent, err := api.GetAttachmentContent(nAtt)
if err != nil {
return clues.Wrap(err, "getting attachment").With("attachment_id", ptr.Val(nAtt.GetId()))
}
if name == nName && bytes.Equal(content, nContent) {
found = true
break
}
}
if !found {
err = client.DeleteAttachment(ctx, userID, containerID, eventID, id)
if err != nil {
logger.CtxErr(ctx, err).With("attachment_name", name).Info("attachment delete failed")
el.AddRecoverable(ctx, clues.Wrap(err, "deleting event attachment").
WithClues(ctx).With("attachment_name", name))
}
}
}
// Upload missing(attachments that are present in the individual
// instance but not in the series master event) attachments
for _, att := range event.GetAttachments() {
name := ptr.Val(att.GetName())
id := ptr.Val(att.GetId())
content, err := api.GetAttachmentContent(att)
if err != nil {
return clues.Wrap(err, "getting attachment").With("attachment_id", id)
}
found := false
for _, nAtt := range attachments {
nName := ptr.Val(nAtt.GetName())
bContent, err := api.GetAttachmentContent(nAtt)
if err != nil {
return clues.Wrap(err, "getting attachment").With("attachment_id", ptr.Val(nAtt.GetId()))
}
// Max size allowed for an outlook attachment is 150MB
if name == nName && bytes.Equal(content, bContent) {
found = true
break
}
}
if !found {
err = uploadAttachment(ctx, client, userID, containerID, eventID, att)
if err != nil {
return clues.Wrap(err, "uploading attachment").
With("attachment_id", id)
}
}
}
return el.Failure()
}
// updateCancelledOccurrences get the cancelled occurrences which is a
// list of strings of the format "<id>.<date>", parses the date out of
// that and uses the to get the event instance at that date to delete.
@ -266,7 +395,7 @@ func updateCancelledOccurrences(
// Get all instances on the day of the instance which should
// just the one we need to modify
evts, err := ac.GetItemInstances(ctx, userID, itemID, startStr, endStr)
instances, err := ac.GetItemInstances(ctx, userID, itemID, startStr, endStr)
if err != nil {
return clues.Wrap(err, "getting instances")
}
@ -274,12 +403,12 @@ func updateCancelledOccurrences(
// Since the min recurrence interval is 1 day and we are
// querying for only a single day worth of instances, we
// should not have more than one instance here.
if len(evts) != 1 {
if len(instances) != 1 {
return clues.New("invalid number of instances for cancelled").
With("instances_count", len(evts), "search_start", startStr, "search_end", endStr)
With("instances_count", len(instances), "search_start", startStr, "search_end", endStr)
}
err = ac.DeleteItem(ctx, userID, ptr.Val(evts[0].GetId()))
err = ac.DeleteItem(ctx, userID, ptr.Val(instances[0].GetId()))
if err != nil {
return clues.Wrap(err, "deleting event instance")
}

View File

@ -23,7 +23,7 @@ import (
// 10. attendees
//nolint:lll
const (
var (
eventTmpl = `{
"categories":[],
"changeKey":"0hATW1CAfUS+njw3hdxSGAAAJIxNug==",
@ -98,8 +98,10 @@ const (
defaultEventBody = "This meeting is to review the latest Tailspin Toys project proposal.<br>\\r\\nBut why not eat some sushi while were at it? :)"
defaultEventBodyPreview = "This meeting is to review the latest Tailspin Toys project proposal.\\r\\nBut why not eat some sushi while were at it? :)"
defaultEventOrganizer = "foobar@8qzvrj.onmicrosoft.com"
eventAttachment = "\"attachments\":[{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAACLjfLQAAABEgAQAHoI0xBbBBVEh6bFMU78ZUo=\",\"@odata.type\":\"#microsoft.graph.fileAttachment\"," +
"\"@odata.mediaContentType\":\"application/octet-stream\",\"contentType\":\"application/octet-stream\",\"isInline\":false,\"lastModifiedDateTime\":\"2022-10-26T15:19:42Z\",\"name\":\"database.db\",\"size\":11418," +
NoAttachments = ""
eventAttachmentFormat = "{\"id\":\"AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwBGAAAAAADCNgjhM9QmQYWNcI7hCpPrBwDSEBNbUIB9RL6ePDeF3FIYAAAAAAENAADSEBNbUIB9RL6ePDeF3FIYAACLjfLQAAABEgAQAHoI0xBbBBVEh6bFMU78ZUo=\",\"@odata.type\":\"#microsoft.graph.fileAttachment\"," +
"\"@odata.mediaContentType\":\"application/octet-stream\",\"contentType\":\"application/octet-stream\",\"isInline\":false,\"lastModifiedDateTime\":\"2022-10-26T15:19:42Z\",\"name\":\"%s\",\"size\":11418," +
"\"contentBytes\":\"U1FMaXRlIGZvcm1hdCAzAAQAAQEAQCAgAAAATQAAAAsAAAAEAAAACAAAAAsAAAAEAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNAC3mBw0DZwACAg8AAxUCDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCAwMHFxUVAYNpdGFibGVkYXRhZGF0YQJDUkVBVEUgVEFCTEUgZGF0YSAoCiAgICAgICAgIGlkIGludGVnZXIgcHJpbWFyeSBrZXkgYXV0b2luY3JlbWVudCwKICAgICAgICAgbWVhbiB0ZXh0IG5vdCBudWxsLAogICAgICAgICBtYXggdGV4dCBub3QgbnVsbCwKICAgICAgICAgbWluIHRleHQgbm90IG51bGwsCiAgICAgICAgIGRhdGEgdGV" +
@ -146,7 +148,8 @@ const (
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}],"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}"
defaultEventAttachments = "\"attachments\":[" + fmt.Sprintf(eventAttachmentFormat, "database.db") + "],"
originalStartDateFormat = `"originalStart": "%s",`
NoOriginalStartDate = ``
@ -226,37 +229,43 @@ func EventBytes(subject string) []byte {
}
func EventWithSubjectBytes(subject string) []byte {
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime := dttm.Format(at)
endTime := dttm.Format(at.Add(30 * time.Minute))
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
endTime = dttm.Format(at.Add(30 * time.Minute))
)
return EventWith(
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, endTime, NoRecurrence, NoAttendees,
false, NoCancelledOccurrences, NoExceptionOccurrences,
NoAttachments, NoCancelledOccurrences, NoExceptionOccurrences,
)
}
func EventWithAttachment(subject string) []byte {
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime := dttm.Format(at)
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
)
return EventWith(
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, NoRecurrence, NoAttendees,
true, NoCancelledOccurrences, NoExceptionOccurrences,
defaultEventAttachments, NoCancelledOccurrences, NoExceptionOccurrences,
)
}
func EventWithRecurrenceBytes(subject, recurrenceTimeZone string) []byte {
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime := dttm.Format(at)
timeSlice := strings.Split(atTime, "T")
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
timeSlice = strings.Split(atTime, "T")
)
recurrence := string(fmt.Sprintf(
recurrenceTmpl,
@ -270,16 +279,18 @@ func EventWithRecurrenceBytes(subject, recurrenceTimeZone string) []byte {
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
true, NoCancelledOccurrences, NoExceptionOccurrences,
NoAttachments, NoCancelledOccurrences, NoExceptionOccurrences,
)
}
func EventWithRecurrenceAndCancellationBytes(subject string) []byte {
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime := dttm.Format(at)
timeSlice := strings.Split(atTime, "T")
nextYear := tomorrow.AddDate(1, 0, 0)
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
timeSlice = strings.Split(atTime, "T")
nextYear = tomorrow.AddDate(1, 0, 0)
)
recurrence := string(fmt.Sprintf(
recurrenceTmpl,
@ -296,17 +307,19 @@ func EventWithRecurrenceAndCancellationBytes(subject string) []byte {
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
true, cancelledOccurrences, NoExceptionOccurrences,
defaultEventAttachments, cancelledOccurrences, NoExceptionOccurrences,
)
}
func EventWithRecurrenceAndExceptionBytes(subject string) []byte {
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime := dttm.Format(at)
timeSlice := strings.Split(atTime, "T")
newTime := dttm.Format(tomorrow.AddDate(0, 0, 1))
originalStartDate := dttm.FormatTo(at, dttm.TabularOutput)
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
timeSlice = strings.Split(atTime, "T")
newTime = dttm.Format(tomorrow.AddDate(0, 0, 1))
originalStartDate = dttm.FormatTo(at, dttm.TabularOutput)
)
recurrence := string(fmt.Sprintf(
recurrenceTmpl,
@ -321,7 +334,43 @@ func EventWithRecurrenceAndExceptionBytes(subject string) []byte {
defaultEventBody, defaultEventBodyPreview,
fmt.Sprintf(originalStartDateFormat, originalStartDate),
newTime, newTime, NoRecurrence, attendeesTmpl,
false, NoCancelledOccurrences, NoExceptionOccurrences,
NoAttachments, NoCancelledOccurrences, NoExceptionOccurrences,
)
exceptionOccurrences := fmt.Sprintf(exceptionOccurrencesFormat, exceptionEvent)
return EventWith(
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
defaultEventAttachments, NoCancelledOccurrences, exceptionOccurrences,
)
}
func EventWithRecurrenceAndExceptionAndAttachmentBytes(subject string) []byte {
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
timeSlice = strings.Split(atTime, "T")
newTime = dttm.Format(tomorrow.AddDate(0, 0, 1))
originalStartDate = dttm.FormatTo(at, dttm.TabularOutput)
)
recurrence := string(fmt.Sprintf(
recurrenceTmpl,
strconv.Itoa(int(at.Month())),
strconv.Itoa(at.Day()),
timeSlice[0],
`"UTC"`,
))
exceptionEvent := EventWith(
defaultEventOrganizer, subject+"(modified)",
defaultEventBody, defaultEventBodyPreview,
fmt.Sprintf(originalStartDateFormat, originalStartDate),
newTime, newTime, NoRecurrence, attendeesTmpl,
"\"attachments\":["+fmt.Sprintf(eventAttachmentFormat, "exception-database.db")+"],",
NoCancelledOccurrences, NoExceptionOccurrences,
)
exceptionOccurrences := fmt.Sprintf(
exceptionOccurrencesFormat,
@ -332,20 +381,22 @@ func EventWithRecurrenceAndExceptionBytes(subject string) []byte {
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, recurrence, attendeesTmpl,
true, NoCancelledOccurrences, exceptionOccurrences,
defaultEventAttachments, NoCancelledOccurrences, exceptionOccurrences,
)
}
func EventWithAttendeesBytes(subject string) []byte {
tomorrow := time.Now().UTC().AddDate(0, 0, 1)
at := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime := dttm.Format(at)
var (
tomorrow = time.Now().UTC().AddDate(0, 0, 1)
at = time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), tomorrow.Hour(), 0, 0, 0, time.UTC)
atTime = dttm.Format(at)
)
return EventWith(
defaultEventOrganizer, subject,
defaultEventBody, defaultEventBodyPreview,
NoOriginalStartDate, atTime, atTime, NoRecurrence, attendeesTmpl,
true, NoCancelledOccurrences, NoExceptionOccurrences,
defaultEventAttachments, NoCancelledOccurrences, NoExceptionOccurrences,
)
}
@ -357,13 +408,9 @@ func EventWithAttendeesBytes(subject string) []byte {
func EventWith(
organizer, subject, body, bodyPreview,
originalStartDate, startDateTime, endDateTime, recurrence, attendees string,
hasAttachments bool, cancelledOccurrences, exceptionOccurrences string,
attachments string, cancelledOccurrences, exceptionOccurrences string,
) []byte {
var attachments string
if hasAttachments {
attachments = eventAttachment
}
hasAttachments := len(attachments) > 0
startDateTime = strings.TrimSuffix(startDateTime, "Z")
endDateTime = strings.TrimSuffix(endDateTime, "Z")

View File

@ -124,6 +124,10 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
name: "Test exceptionOccurrences",
bytes: exchMock.EventWithRecurrenceAndExceptionBytes(subject),
},
{
name: "Test exceptionOccurrences with different attachments",
bytes: exchMock.EventWithRecurrenceAndExceptionAndAttachmentBytes(subject),
},
}
for _, test := range tests {
@ -369,3 +373,70 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
})
}
}
func (suite *RestoreIntgSuite) TestRestoreAndBackupEvent_recurringInstancesWithAttachments() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
var (
userID = tester.M365UserID(t)
subject = testdata.DefaultRestoreConfig("event").Location
handler = newEventRestoreHandler(suite.ac)
)
calendar, err := handler.ac.CreateContainer(ctx, userID, subject, "")
require.NoError(t, err, clues.ToCore(err))
calendarID := ptr.Val(calendar.GetId())
bytes := exchMock.EventWithRecurrenceAndExceptionAndAttachmentBytes("Reoccurring event restore and backup test")
info, err := handler.restore(
ctx,
bytes,
userID, calendarID,
fault.New(true))
require.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, info, "event item info")
ec, err := handler.ac.Stable.
Client().
Users().
ByUserId(userID).
Calendars().
ByCalendarId(calendarID).
Events().
Get(ctx, nil)
require.NoError(t, err, clues.ToCore(err))
evts := ec.GetValue()
assert.Len(t, evts, 1, "count of events")
sp, info, err := suite.ac.Events().GetItem(ctx, userID, ptr.Val(evts[0].GetId()), false, fault.New(true))
require.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, info, "event item info")
body, err := suite.ac.Events().Serialize(ctx, sp, userID, ptr.Val(evts[0].GetId()))
require.NoError(t, err, clues.ToCore(err))
event, err := api.BytesToEventable(body)
require.NoError(t, err, clues.ToCore(err))
assert.NotNil(t, event.GetRecurrence(), "recurrence")
eo := event.GetAdditionalData()["exceptionOccurrences"]
assert.NotNil(t, eo, "exceptionOccurrences")
assert.NotEqual(
t,
ptr.Val(event.GetSubject()),
ptr.Val(eo.([]any)[0].(map[string]any)["subject"].(*string)),
"name equal")
atts := eo.([]any)[0].(map[string]any)["attachments"]
assert.NotEqual(
t,
ptr.Val(event.GetAttachments()[0].GetName()),
ptr.Val(atts.([]any)[0].(map[string]any)["name"].(*string)),
"attachment name equal")
}

View File

@ -815,7 +815,7 @@ func testExchangeContinuousBackups(suite *BackupOpIntegrationSuite, toggles cont
suite.user, subject, body, body,
exchMock.NoOriginalStartDate, now, now,
exchMock.NoRecurrence, exchMock.NoAttendees,
false, exchMock.NoCancelledOccurrences,
exchMock.NoAttachments, exchMock.NoCancelledOccurrences,
exchMock.NoExceptionOccurrences)
}

View File

@ -241,9 +241,12 @@ func (c Events) GetItem(
return nil, nil, clues.Wrap(err, "fixup exception occurrences")
}
attachments, err := c.getAttachments(ctx, event, immutableIDs, userID, itemID)
if err != nil {
return nil, nil, err
var attachments []models.Attachmentable
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) {
attachments, err = c.GetAttachments(ctx, immutableIDs, userID, itemID)
if err != nil {
return nil, nil, err
}
}
event.SetAttachments(attachments)
@ -286,10 +289,14 @@ func fixupExceptionOccurrences(
// OPTIMIZATION: We don't have to store any of the
// attachments that carry over from the original
attachments, err := client.getAttachments(ctx, evt, immutableIDs, userID, ptr.Val(evt.GetId()))
if err != nil {
return clues.Wrap(err, "getting exception attachments").
With("exception_event_id", ptr.Val(evt.GetId()))
var attachments []models.Attachmentable
if ptr.Val(event.GetHasAttachments()) || HasAttachments(event.GetBody()) {
attachments, err = client.GetAttachments(ctx, immutableIDs, userID, ptr.Val(evt.GetId()))
if err != nil {
return clues.Wrap(err, "getting event instance attachments").
With("event_instance_id", ptr.Val(evt.GetId()))
}
}
// This odd roundabout way of doing this is required as
@ -370,17 +377,12 @@ func parseableToMap(att serialization.Parsable) (map[string]any, error) {
return item, nil
}
func (c Events) getAttachments(
func (c Events) GetAttachments(
ctx context.Context,
event models.Eventable,
immutableIDs bool,
userID string,
itemID string,
) ([]models.Attachmentable, error) {
if !ptr.Val(event.GetHasAttachments()) && !HasAttachments(event.GetBody()) {
return nil, nil
}
config := &users.ItemEventsItemAttachmentsRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemEventsItemAttachmentsRequestBuilderGetQueryParameters{
Expand: []string{"microsoft.graph.itemattachment/item"},
@ -403,6 +405,23 @@ func (c Events) getAttachments(
return attached.GetValue(), nil
}
func (c Events) DeleteAttachment(
ctx context.Context,
userID, calendarID, eventID, attachmentID string,
) error {
return c.Stable.
Client().
Users().
ByUserId(userID).
Calendars().
ByCalendarId(calendarID).
Events().
ByEventId(eventID).
Attachments().
ByAttachmentId(attachmentID).
Delete(ctx, nil)
}
func (c Events) GetItemInstances(
ctx context.Context,
userID, itemID string,

View File

@ -26,5 +26,3 @@ included in backup and restore.
* SharePoint document library data can't be restored after the library has been deleted.
* Sharing information of items in OneDrive/SharePoint using sharing links aren't backed up and restored.
* Changes to attachments in instances of recurring events compared to the series master aren't restored