Handle photo not found case in API routes (#1201)

* Handle media not found issue

* temporary run workflows for master pushes only; tests

* revert temp workflow changes

---------

Co-authored-by: Konstantin Koval
This commit is contained in:
Kostiantyn
2025-05-10 17:45:32 +03:00
committed by GitHub
parent 36f3924b4f
commit 27a0b082d1
3 changed files with 130 additions and 1 deletions

View File

@@ -26,6 +26,11 @@ func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) {
}
media := mediaURL.Media
if media == nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 - Media not found"))
return
}
if success, response, status, err := authenticateMedia(media, db, r); !success {
if err != nil {
@@ -46,7 +51,7 @@ func RegisterPhotoRoutes(db *gorm.DB, router *mux.Router) {
if _, err := os.Stat(cachedPath); os.IsNotExist((err)) {
// err := db.Transaction(func(tx *gorm.DB) error {
if err = scanner.ProcessSingleMedia(db, media); err != nil {
if err = scanner.ProcessSingleMediaFunc(db, media); err != nil {
log.Printf("ERROR: processing image not found in cache (%s): %s\n", cachedPath, err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(internalServerError))

122
api/routes/photos_test.go Normal file
View File

@@ -0,0 +1,122 @@
package routes
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"testing"
"time"
"github.com/gorilla/mux"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/scanner"
"github.com/photoview/photoview/api/test_utils"
"github.com/photoview/photoview/api/utils"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
func TestPhotoRoutes(t *testing.T) {
db := test_utils.DatabaseTest(t)
user, err := models.RegisterUser(db, "testuser", nil, false)
assert.NoError(t, err)
album := models.Album{Title: "test_album", Path: "/photos"}
assert.NoError(t, db.Model(&user).Association("Albums").Append(&album))
media := models.Media{
Title: "test_media",
Path: "/photos/test_image.jpg",
AlbumID: album.ID,
DateShot: time.Now(),
Type: models.MediaTypePhoto,
}
assert.NoError(t, db.Save(&media).Error)
mediaURL := models.MediaURL{
MediaID: media.ID,
Media: &media,
MediaName: "test_image.jpg",
Width: 1000,
Height: 800,
Purpose: models.PhotoThumbnail,
ContentType: "image/jpeg",
FileSize: 1024,
}
assert.NoError(t, db.Save(&mediaURL).Error)
tempDir := t.TempDir()
orig := utils.MediaCachePath()
utils.ConfigureTestCache(tempDir)
defer utils.ConfigureTestCache(orig)
router := mux.NewRouter()
RegisterPhotoRoutes(db, router)
// -- Test cases --
// Non-existent media_name => 404 (no auth required)
t.Run("media not found", func(t *testing.T) {
req := httptest.NewRequest("GET", "/does_not_exist.jpg", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusNotFound, rec.Code)
assert.Equal(t, "404 - Media not found", rec.Body.String())
})
// Missing auth => 403
t.Run("auth failure", func(t *testing.T) {
req := httptest.NewRequest("GET", "/test_image.jpg", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
assert.Contains(t, rec.Body.String(), "unauthorized")
})
// Cache miss + scan error => 500
t.Run("scan failure yields 500", func(t *testing.T) {
req := httptest.NewRequest("GET", "/test_image.jpg", nil)
ctx := auth.AddUserToContext(req.Context(), user)
req = req.WithContext(ctx)
// ensure no cached file
cachedPath, err := mediaURL.CachedPath()
assert.NoError(t, err)
os.Remove(cachedPath)
// mock scan to fail
origScan := scanner.ProcessSingleMediaFunc
scanner.ProcessSingleMediaFunc = func(db *gorm.DB, m *models.Media) error {
return fmt.Errorf("scan error")
}
defer func() { scanner.ProcessSingleMediaFunc = origScan }()
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "server error")
})
// Cache hit => 200 with correct body and headers
t.Run("cache hit serves file", func(t *testing.T) {
req := httptest.NewRequest("GET", "/test_image.jpg", nil)
ctx := auth.AddUserToContext(req.Context(), user)
req = req.WithContext(ctx)
// pre-create cached file
cachedPath, err := mediaURL.CachedPath()
assert.NoError(t, err)
assert.NoError(t, os.MkdirAll(path.Dir(cachedPath), 0755))
content := []byte("cached-binary")
assert.NoError(t, os.WriteFile(cachedPath, content, 0644))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "cached-binary", rec.Body.String())
assert.Equal(t, "private, max-age=86400, immutable", rec.Header().Get("Cache-Control"))
})
}

View File

@@ -14,6 +14,8 @@ import (
"gorm.io/gorm"
)
var ProcessSingleMediaFunc = ProcessSingleMedia
func ScanMedia(tx *gorm.DB, mediaPath string, albumId int, cache *scanner_cache.AlbumScannerCache) (*models.Media, bool, error) {
mediaName := path.Base(mediaPath)