diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c34306e..e7bcf0df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ - Support Ed25519 in the optional OIDC id_token signature validation ([#7252](https://github.com/pocketbase/pocketbase/issues/7252); thanks @shynome). -- Added `tests.ApiScenario.DisableTestAppCleanup` optional field to skip the auto test app cleanup and leave it up to the developers ([#7267](https://github.com/pocketbase/pocketbase/discussions/7267)). +- Added `ApiScenario.DisableTestAppCleanup` optional field to skip the auto test app cleanup and leave it up to the developers ([#7267](https://github.com/pocketbase/pocketbase/discussions/7267)). + +- Added `FileDownloadRequestEvent.ThumbError` field that will be populated in case of a thumb generation failure (e.g. unsupported format, timing out, etc.), allow developers to reject the fallback and/or supply their own custom thumb generation ([#7268](https://github.com/pocketbase/pocketbase/discussions/7268)). ## v0.30.4 diff --git a/apis/file.go b/apis/file.go index 5c6ec211..bbc4bb41 100644 --- a/apis/file.go +++ b/apis/file.go @@ -142,8 +142,14 @@ func (api *fileApi) download(e *core.RequestEvent) error { defer fsys.Close() originalPath := baseFilesPath + "/" + filename - servedPath := originalPath - servedName := filename + + event := new(core.FileDownloadRequestEvent) + event.RequestEvent = e + event.Collection = collection + event.Record = record + event.FileField = fileField + event.ServedPath = originalPath + event.ServedName = filename // check for valid thumb size param thumbSize := e.Request.URL.Query().Get("thumb") @@ -157,34 +163,31 @@ func (api *fileApi) download(e *core.RequestEvent) error { // check if it is an image if list.ExistInSlice(oAttrs.ContentType, imageContentTypes) { // add thumb size as file suffix - servedName = thumbSize + "_" + filename - servedPath = baseFilesPath + "/thumbs_" + filename + "/" + servedName + event.ServedName = thumbSize + "_" + filename + event.ServedPath = baseFilesPath + "/thumbs_" + filename + "/" + event.ServedName // create a new thumb if it doesn't exist - if exists, _ := fsys.Exists(servedPath); !exists { - if err := api.createThumb(e, fsys, originalPath, servedPath, thumbSize); err != nil { + if exists, _ := fsys.Exists(event.ServedPath); !exists { + if err := api.createThumb(e, fsys, originalPath, event.ServedPath, thumbSize); err != nil { e.App.Logger().Warn( - "Fallback to original - failed to create thumb "+servedName, + "Fallback to original - failed to create thumb "+event.ServedName, slog.Any("error", err), slog.String("original", originalPath), - slog.String("thumb", servedPath), + slog.String("thumb", event.ServedPath), ) // fallback to the original - servedName = filename - servedPath = originalPath + event.ThumbError = err + event.ServedName = filename + event.ServedPath = originalPath } } } } - event := new(core.FileDownloadRequestEvent) - event.RequestEvent = e - event.Collection = collection - event.Record = record - event.FileField = fileField - event.ServedPath = servedPath - event.ServedName = servedName + if thumbSize != "" && event.ThumbError == nil && event.ServedPath == originalPath { + event.ThumbError = fmt.Errorf("the thumb size %q is not supported", thumbSize) + } // clickjacking shouldn't be a concern when serving uploaded files, // so it safe to unset the global X-Frame-Options to allow files embedding diff --git a/apis/file_test.go b/apis/file_test.go index a57c2cc6..daee78f1 100644 --- a/apis/file_test.go +++ b/apis/file_test.go @@ -181,9 +181,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - missing thumb (should fallback to the original)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=999x999", + Name: "existing image - missing thumb (should fallback to the original)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=999x999", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError == nil { + t.Fatal("Expected thumb error, got nil") + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testImg)}, ExpectedEvents: map[string]int{ @@ -192,9 +200,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - existing thumb (crop center)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50", + Name: "existing image - existing thumb (crop center)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError != nil { + t.Fatalf("Expected no thumb error, got %v", e.ThumbError) + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testThumbCropCenter)}, ExpectedEvents: map[string]int{ @@ -203,9 +219,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - existing thumb (crop top)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50t", + Name: "existing image - existing thumb (crop top)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50t", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError != nil { + t.Fatalf("Expected no thumb error, got %v", e.ThumbError) + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testThumbCropTop)}, ExpectedEvents: map[string]int{ @@ -214,9 +238,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - existing thumb (crop bottom)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50b", + Name: "existing image - existing thumb (crop bottom)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50b", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError != nil { + t.Fatalf("Expected no thumb error, got %v", e.ThumbError) + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testThumbCropBottom)}, ExpectedEvents: map[string]int{ @@ -225,9 +257,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - existing thumb (fit)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50f", + Name: "existing image - existing thumb (fit)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x50f", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError != nil { + t.Fatalf("Expected no thumb error, got %v", e.ThumbError) + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testThumbFit)}, ExpectedEvents: map[string]int{ @@ -236,9 +276,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - existing thumb (zero width)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=0x50", + Name: "existing image - existing thumb (zero width)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=0x50", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError != nil { + t.Fatalf("Expected no thumb error, got %v", e.ThumbError) + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testThumbZeroWidth)}, ExpectedEvents: map[string]int{ @@ -247,9 +295,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing image - existing thumb (zero height)", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x0", + Name: "existing image - existing thumb (zero height)", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/4q1xlclmfloku33/300_1SEi6Q6U72.png?thumb=70x0", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError != nil { + t.Fatalf("Expected no thumb error, got %v", e.ThumbError) + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testThumbZeroHeight)}, ExpectedEvents: map[string]int{ @@ -258,9 +314,17 @@ func TestFileDownload(t *testing.T) { }, }, { - Name: "existing non image file - thumb parameter should be ignored", - Method: http.MethodGet, - URL: "/api/files/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt?thumb=100x100", + Name: "existing non image file - thumb parameter should be ignored", + Method: http.MethodGet, + URL: "/api/files/_pb_users_auth_/oap640cot4yru2s/test_kfd2wYLxkz.txt?thumb=100x100", + BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) { + app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error { + if e.ThumbError == nil { + t.Fatal("Expected thumb error, got nil") + } + return e.Next() + }) + }, ExpectedStatus: 200, ExpectedContent: []string{string(testFile)}, ExpectedEvents: map[string]int{ diff --git a/core/events.go b/core/events.go index 5f54f583..4a03af62 100644 --- a/core/events.go +++ b/core/events.go @@ -384,6 +384,13 @@ type FileDownloadRequestEvent struct { FileField *FileField ServedPath string ServedName string + + // ThumbError indicates the a thumb wasn't able to be generated + // (e.g. because it didn't satisfy the support image formats or it timed out). + // + // Note that PocketBase fallbacks to the original file in case of a thumb error, + // but developers can check the field and provide their own custom thumb generation if necessary. + ThumbError error } // -------------------------------------------------------------------