diff --git a/internal/manager/api_impl/interfaces.go b/internal/manager/api_impl/interfaces.go index d118f6c8..60087b73 100644 --- a/internal/manager/api_impl/interfaces.go +++ b/internal/manager/api_impl/interfaces.go @@ -131,6 +131,9 @@ type LastRendered interface { // ThumbSpecs returns the thumbnail specifications. ThumbSpecs() []last_rendered.Thumbspec + + // JobHasImage returns true only if the job actually has a last-rendered image. + JobHasImage(jobUUID string) bool } // LocalStorage handles the storage organisation of local files like the last-rendered images. diff --git a/internal/manager/api_impl/jobs.go b/internal/manager/api_impl/jobs.go index 85fb985b..813653ff 100644 --- a/internal/manager/api_impl/jobs.go +++ b/internal/manager/api_impl/jobs.go @@ -317,6 +317,10 @@ func (f *Flamenco) FetchJobLastRenderedInfo(e echo.Context, jobID string) error return sendAPIError(e, http.StatusBadRequest, "job ID should be a UUID") } + if !f.lastRender.JobHasImage(jobID) { + return e.NoContent(http.StatusNoContent) + } + logger := requestLogger(e) info, err := f.lastRenderedInfoForJob(logger, jobID) if err != nil { diff --git a/internal/manager/api_impl/jobs_test.go b/internal/manager/api_impl/jobs_test.go index 4a4c0317..3e2c5fb6 100644 --- a/internal/manager/api_impl/jobs_test.go +++ b/internal/manager/api_impl/jobs_test.go @@ -10,6 +10,7 @@ import ( "testing" "git.blender.org/flamenco/internal/manager/job_compilers" + "git.blender.org/flamenco/internal/manager/last_rendered" "git.blender.org/flamenco/internal/manager/persistence" "git.blender.org/flamenco/pkg/api" "github.com/golang/mock/gomock" @@ -312,3 +313,43 @@ func TestFetchTaskLogTail(t *testing.T) { assert.NoError(t, err) assertResponseNoContent(t, echoCtx) } + +func TestFetchJobLastRenderedInfo(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mf := newMockedFlamenco(mockCtrl) + + jobID := "18a9b096-d77e-438c-9be2-74397038298b" + + { + // Last-rendered image has been processed. + mf.lastRender.EXPECT().JobHasImage(jobID).Return(true) + mf.lastRender.EXPECT().PathForJob(jobID).Return("/absolute/path/to/local/job/dir") + mf.localStorage.EXPECT().RelPath("/absolute/path/to/local/job/dir").Return("relative/path", nil) + mf.lastRender.EXPECT().ThumbSpecs().Return([]last_rendered.Thumbspec{ + {Filename: "das grosses potaat.jpg"}, + {Filename: "invisibru.jpg"}, + }) + + echoCtx := mf.prepareMockedRequest(nil) + err := mf.flamenco.FetchJobLastRenderedInfo(echoCtx, jobID) + assert.NoError(t, err) + + expectBody := api.JobLastRenderedImageInfo{ + Base: "/job-files/relative/path", + Suffixes: []string{"das grosses potaat.jpg", "invisibru.jpg"}, + } + assertResponseJSON(t, echoCtx, http.StatusOK, expectBody) + } + + { + // No last-rendered image exists. + mf.lastRender.EXPECT().JobHasImage(jobID).Return(false) + + echoCtx := mf.prepareMockedRequest(nil) + err := mf.flamenco.FetchJobLastRenderedInfo(echoCtx, jobID) + assert.NoError(t, err) + assertResponseNoContent(t, echoCtx) + } +} diff --git a/internal/manager/api_impl/mocks/api_impl_mock.gen.go b/internal/manager/api_impl/mocks/api_impl_mock.gen.go index 0145bb24..4667696f 100644 --- a/internal/manager/api_impl/mocks/api_impl_mock.gen.go +++ b/internal/manager/api_impl/mocks/api_impl_mock.gen.go @@ -867,6 +867,20 @@ func (m *MockLastRendered) EXPECT() *MockLastRenderedMockRecorder { return m.recorder } +// JobHasImage mocks base method. +func (m *MockLastRendered) JobHasImage(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "JobHasImage", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// JobHasImage indicates an expected call of JobHasImage. +func (mr *MockLastRenderedMockRecorder) JobHasImage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JobHasImage", reflect.TypeOf((*MockLastRendered)(nil).JobHasImage), arg0) +} + // PathForJob mocks base method. func (m *MockLastRendered) PathForJob(arg0 string) string { m.ctrl.T.Helper() diff --git a/internal/manager/last_rendered/last_rendered.go b/internal/manager/last_rendered/last_rendered.go index 1f0fb01c..7ea83cbb 100644 --- a/internal/manager/last_rendered/last_rendered.go +++ b/internal/manager/last_rendered/last_rendered.go @@ -5,6 +5,8 @@ package last_rendered import ( "context" "errors" + "io/fs" + "os" "path/filepath" "github.com/rs/zerolog" @@ -110,6 +112,26 @@ func (lrp *LastRenderedProcessor) PathForJob(jobUUID string) string { return lrp.storage.ForJob(jobUUID) } +// JobHasImage returns true only if the job actually has a last-rendered image. +// Only the lowest-resolution image is tested for. Since images are processed in +// order, existence of the last one should imply existence of all of them. +func (lrp *LastRenderedProcessor) JobHasImage(jobUUID string) bool { + dirPath := lrp.PathForJob(jobUUID) + filename := thumbnails[len(thumbnails)-1].Filename + path := filepath.Join(dirPath, filename) + + _, err := os.Stat(path) + switch { + case err == nil: + return true + case errors.Is(err, fs.ErrNotExist): + return false + default: + log.Warn().Err(err).Str("path", path).Msg("last-rendered: unexpected error checking file for existence") + return false + } +} + // ThumbSpecs returns the thumbnail specifications. func (lrp *LastRenderedProcessor) ThumbSpecs() []Thumbspec { // Return a copy so modification of the returned slice won't affect the global diff --git a/web/app/public/nothing-rendered-yet.svg b/web/app/public/nothing-rendered-yet.svg new file mode 100644 index 00000000..e13a318b --- /dev/null +++ b/web/app/public/nothing-rendered-yet.svg @@ -0,0 +1,67 @@ + + + + + + + + image/svg+xml + + + + + + + Nothing rendered yet... + + diff --git a/web/app/src/components/jobs/LastRenderedImage.vue b/web/app/src/components/jobs/LastRenderedImage.vue index 26134fbf..5cd062c5 100644 --- a/web/app/src/components/jobs/LastRenderedImage.vue +++ b/web/app/src/components/jobs/LastRenderedImage.vue @@ -1,11 +1,15 @@ @@ -75,4 +88,8 @@ defineExpose({ height: 112px; float: right; } + +.lastRendered.nothingRenderedYet { + outline: thin dotted var(--color-text-hint); +}