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:
parent
c8ae50cb2e
commit
b0305a5319
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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 we’re at it? :)"
|
||||
defaultEventBodyPreview = "This meeting is to review the latest Tailspin Toys project proposal.\\r\\nBut why not eat some sushi while we’re 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")
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user