diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 949fe889..220982ef 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -487,6 +487,11 @@ func (a *serverApp) run(ctx context.Context) error { go a.devAuth.Run(context.Background()) // dev oauth2 server on :8084 } + // staging images resubmit after restart of the app + if e := a.dataService.ResubmitStagingImages(a.Sites); e != nil { + log.Printf("[WARN] failed to resubmit comments with staging images, %s", e) + } + go a.imageService.Cleanup(ctx) // pictures cleanup for staging images a.restSrv.Run(a.Port) diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index 4a429d6a..a3d65fd9 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -200,6 +200,29 @@ func (s *DataStore) DeleteUserDetail(siteID string, userID string, detail engine }) } +// ResubmitStagingImages retrieves timestamp of the oldest image in staging and +// calls s.submitImages on all comments newer than it +func (s *DataStore) ResubmitStagingImages(sites []string) error { + info, err := s.ImageService.Info() + if err != nil { + return err + } + ts := info.FirstStagingImageTS + if ts.IsZero() { + return nil + } + result := new(multierror.Error) + for _, site := range sites { + locator := store.Locator{SiteID: site} + comments, err := s.FindSince(locator, "time", store.User{}, ts) + result = multierror.Append(result, errors.Wrapf(err, "problem finding comments for site %s", site)) + for _, c := range comments { + s.submitImages(c.Locator, c.ID) + } + } + return result.ErrorOrNil() +} + // submitImages initiated delayed commit of all images from the comment uploaded to remark42 func (s *DataStore) submitImages(locator store.Locator, commentID string) { diff --git a/backend/app/store/service/service_test.go b/backend/app/store/service/service_test.go index 76195d41..1a494bb9 100644 --- a/backend/app/store/service/service_test.go +++ b/backend/app/store/service/service_test.go @@ -1,6 +1,7 @@ package service import ( + "context" "fmt" "io/ioutil" "math/rand" @@ -1302,6 +1303,105 @@ func TestService_submitImages(t *testing.T) { time.Sleep(250 * time.Millisecond) } +func TestService_ResubmitStagingImages(t *testing.T) { + mockStore := image.MockStore{} + imgSvc := image.NewService(&mockStore, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + }) + defer imgSvc.Close(context.TODO()) + + eng, teardown := prepStoreEngine(t) + defer teardown() + b := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvc} + + // create comment with three images without preparing it properly + comment := store.Comment{ + ID: "id-0", + Text: `startrails_01.jpg
+ cat.png
+ boat.png`, + Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), + Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: store.User{ID: "user1", Name: "user name"}, + } + _, err := b.Engine.Create(comment) + require.NoError(t, err) + + // resubmit single comment with three images, of which two are in staging storage + mockStore.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil) + err = b.ResubmitStagingImages([]string{"radio-t"}) + assert.NoError(t, err) + + // wait for Submit goroutine to commit image + mockStore.On("Commit", "dev_user/bqf122eq9r8ad657n3ng").Once().Return(nil) + mockStore.On("Commit", "dev_user/bqf321eq9r8ad657n3ng").Once().Return(nil) + time.Sleep(time.Millisecond * 100) + + mockStore.AssertNumberOfCalls(t, "Info", 1) + mockStore.AssertNumberOfCalls(t, "Commit", 2) + + // empty answer + mockStoreEmpty := image.MockStore{} + imgSvcEmpty := image.NewService(&mockStoreEmpty, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + }) + defer imgSvcEmpty.Close(context.TODO()) + bEmpty := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcEmpty} + + // resubmit receive empty timestamp and should do nothing + mockStoreEmpty.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}}, nil) + err = bEmpty.ResubmitStagingImages([]string{"radio-t", "non_existent"}) + assert.NoError(t, err) + + mockStoreEmpty.AssertNumberOfCalls(t, "Info", 1) + + // error from image storage + mockStoreError := image.MockStore{} + imgSvcError := image.NewService(&mockStoreError, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + }) + defer imgSvcError.Close(context.TODO()) + bError := DataStore{Engine: eng, EditDuration: 10 * time.Millisecond, ImageService: imgSvcError} + + // resubmit will receive error from image storage and should return it + mockStoreError.On("Info").Once().Return(image.StoreInfo{}, errors.New("mock_err")) + err = bError.ResubmitStagingImages([]string{"radio-t"}) + assert.EqualError(t, err, "mock_err") + + mockStoreError.AssertNumberOfCalls(t, "Info", 1) +} + +func TestService_ResubmitStagingImages_EngineError(t *testing.T) { + mockStore := image.MockStore{} + imgSvc := image.NewService(&mockStore, + image.ServiceParams{ + EditDuration: 10 * time.Millisecond, + ImageAPI: "http://127.0.0.1:8080/api/v1/picture/", + }) + defer imgSvc.Close(context.TODO()) + + engineMock := engine.MockInterface{} + site1Req := engine.FindRequest{Locator: store.Locator{SiteID: "site1", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)} + site2Req := engine.FindRequest{Locator: store.Locator{SiteID: "site2", URL: ""}, Sort: "time", Since: time.Time{}.Add(time.Second)} + engineMock.On("Find", site1Req).Return(nil, nil) + engineMock.On("Find", site2Req).Return(nil, errors.New("mockError")) + b := DataStore{Engine: &engineMock, EditDuration: 10 * time.Millisecond, ImageService: imgSvc} + + // One call without error and one with error + mockStore.On("Info").Once().Return(image.StoreInfo{FirstStagingImageTS: time.Time{}.Add(time.Second)}, nil) + err := b.ResubmitStagingImages([]string{"site1", "site2"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "problem finding comments for site site2: mockError") + + mockStore.AssertNumberOfCalls(t, "Info", 1) +} + func TestService_alterComment(t *testing.T) { engineMock := engine.MockInterface{}