diff --git a/api/routes/photos.go b/api/routes/photos.go
index 080d3681..245119bb 100644
--- a/api/routes/photos.go
+++ b/api/routes/photos.go
@@ -22,23 +22,29 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
image_name := mux.Vars(r)["name"]
- row := db.QueryRow("SELECT photo_url.purpose, photo.path, photo.photo_id, photo.album_id, photo_url.content_type FROM photo_url, photo WHERE photo_url.photo_name = ? AND photo_url.photo_id = photo.photo_id", image_name)
+ row := db.QueryRow("SELECT photo_url.purpose, photo_url.content_type, photo_url.photo_id FROM photo_url, photo WHERE photo_url.photo_name = ? AND photo_url.photo_id = photo.photo_id", image_name)
var purpose models.PhotoPurpose
- var photoPath string
var content_type string
- var album_id int
var photo_id int
- if err := row.Scan(&purpose, &photoPath, &photo_id, &album_id, &content_type); err != nil {
+ if err := row.Scan(&purpose, &content_type, &photo_id); err != nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404"))
return
}
+ row = db.QueryRow("SELECT * FROM photo WHERE photo_id = ?", photo_id)
+ photo, err := models.NewPhotoFromRow(row)
+ if err != nil {
+ log.Printf("WARN: %s", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("internal server error"))
+ }
+
user := auth.UserFromContext(r.Context())
if user != nil {
- row := db.QueryRow("SELECT owner_id FROM album WHERE album.album_id = ?", album_id)
+ row := db.QueryRow("SELECT owner_id FROM album WHERE album.album_id = ?", photo.AlbumId)
var owner_id int
if err := row.Scan(&owner_id); err != nil {
@@ -72,7 +78,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
return
}
- if shareToken.AlbumID != nil && album_id != *shareToken.AlbumID {
+ if shareToken.AlbumID != nil && photo.AlbumId != *shareToken.AlbumID {
// Check child albums
row := db.QueryRow(`
WITH recursive child_albums AS (
@@ -81,7 +87,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
SELECT child.* FROM album child JOIN child_albums parent ON parent.album_id = child.parent_album
)
SELECT * FROM child_albums WHERE album_id = ?
- `, *shareToken.AlbumID, album_id)
+ `, *shareToken.AlbumID, photo.AlbumId)
_, err := models.NewAlbumFromRow(row)
if err != nil {
@@ -105,23 +111,47 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) {
}
- var file *os.File
+ var cachedPath string
+ var file *os.File = nil
if purpose == models.PhotoThumbnail || purpose == models.PhotoHighRes {
- var err error
- file, err = os.Open(path.Join(scanner.PhotoCache(), strconv.Itoa(album_id), strconv.Itoa(photo_id), image_name))
- if err != nil {
- w.Write([]byte("Error: " + err.Error()))
- return
- }
+ cachedPath = path.Join(scanner.PhotoCache(), strconv.Itoa(photo.AlbumId), strconv.Itoa(photo_id), image_name)
}
if purpose == models.PhotoOriginal {
- var err error
- file, err = os.Open(photoPath)
- if err != nil {
- w.Write([]byte("Error: " + err.Error()))
- return
+ cachedPath = photo.Path
+ }
+
+ file, err = os.Open(cachedPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ tx, err := db.Begin()
+ if err != nil {
+ log.Printf("ERROR: %s\n", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("internal server error"))
+ return
+ }
+
+ err = scanner.ProcessPhoto(tx, photo, &content_type)
+ if err != nil {
+ log.Printf("ERROR: processing image not found in cache: %s\n", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("internal server error"))
+ tx.Rollback()
+ return
+ }
+
+ file, err = os.Open(cachedPath)
+ if err != nil {
+ log.Printf("ERROR: after reprocessing image not found in cache: %s\n", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("internal server error"))
+ tx.Rollback()
+ return
+ }
+
+ tx.Commit()
}
}
diff --git a/api/scanner/scanner.go b/api/scanner/album_scanner.go
similarity index 99%
rename from api/scanner/scanner.go
rename to api/scanner/album_scanner.go
index 2ed0adf9..af5bdd84 100644
--- a/api/scanner/scanner.go
+++ b/api/scanner/album_scanner.go
@@ -166,7 +166,7 @@ func scan(database *sql.DB, user *models.User) {
continue
}
- if err := ProcessImage(tx, photoPath, albumId, *content_type); err != nil {
+ if err := ScanPhoto(tx, photoPath, albumId, content_type); err != nil {
ScannerError("processing image %s: %s", photoPath, err)
tx.Rollback()
continue
diff --git a/api/scanner/photo_scanner.go b/api/scanner/photo_scanner.go
new file mode 100644
index 00000000..078f0071
--- /dev/null
+++ b/api/scanner/photo_scanner.go
@@ -0,0 +1,49 @@
+package scanner
+
+import (
+ "database/sql"
+ "github.com/viktorstrate/photoview/api/graphql/models"
+ "log"
+ "path"
+)
+
+func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string) error {
+
+ log.Printf("Scanning image: %s\n", photoPath)
+
+ photoName := path.Base(photoPath)
+
+ // Check if image already exists
+ row := tx.QueryRow("SELECT (photo_id) FROM photo WHERE path = ?", photoPath)
+ var photo_id int64
+ if err := row.Scan(&photo_id); err != sql.ErrNoRows {
+ if err == nil {
+ log.Printf("Image already scanned: %s\n", photoPath)
+ return nil
+ } else {
+ return err
+ }
+ }
+
+ result, err := tx.Exec("INSERT INTO photo (title, path, album_id) VALUES (?, ?, ?)", photoName, photoPath, albumId)
+ if err != nil {
+ log.Printf("ERROR: Could not insert photo into database")
+ return err
+ }
+ photo_id, err = result.LastInsertId()
+ if err != nil {
+ return err
+ }
+
+ row = tx.QueryRow("SELECT * FROM photo WHERE photo_id = ?", photo_id)
+ photo, err := models.NewPhotoFromRow(row)
+ if err != nil {
+ return err
+ }
+
+ if err := ProcessPhoto(tx, photo, content_type); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/api/scanner/process_image.go b/api/scanner/process_image.go
deleted file mode 100644
index e28fcf73..00000000
--- a/api/scanner/process_image.go
+++ /dev/null
@@ -1,163 +0,0 @@
-package scanner
-
-import (
- "database/sql"
- "fmt"
- "image"
- "image/jpeg"
- "log"
- "os"
- "path"
- "strconv"
- "strings"
-
- "github.com/nfnt/resize"
- "github.com/viktorstrate/photoview/api/graphql/models"
- "github.com/viktorstrate/photoview/api/utils"
-
- // Image decoders
- _ "golang.org/x/image/bmp"
- // _ "golang.org/x/image/tiff"
- _ "image/gif"
- _ "image/png"
-
- _ "github.com/nf/cr2"
- _ "golang.org/x/image/webp"
-)
-
-func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string) error {
-
- // log.Printf("Processing image: %s\n", photoPath)
-
- photoName := path.Base(photoPath)
-
- // Check if image already exists
- row := tx.QueryRow("SELECT (photo_id) FROM photo WHERE path = ?", photoPath)
- var photo_id int64
- if err := row.Scan(&photo_id); err != sql.ErrNoRows {
- if err == nil {
- log.Printf("Image already processed: %s\n", photoPath)
- return nil
- } else {
- return err
- }
- }
-
- result, err := tx.Exec("INSERT INTO photo (title, path, album_id) VALUES (?, ?, ?)", photoName, photoPath, albumId)
- if err != nil {
- log.Printf("ERROR: Could not insert photo into database")
- return err
- }
- photo_id, err = result.LastInsertId()
- if err != nil {
- return err
- }
-
- photo_file, err := os.Open(photoPath)
- if err != nil {
- return err
- }
- defer photo_file.Close()
-
- image, _, err := image.Decode(photo_file)
- if err != nil {
- log.Println("ERROR: decoding image")
- return err
- }
-
- photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
- photoBaseExt := path.Ext(photoName)
-
- // original photo url
- original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
- original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
-
- _, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo_id, original_image_name, image.Bounds().Max.X, image.Bounds().Max.Y, models.PhotoOriginal, content_type)
- if err != nil {
- log.Printf("Could not insert original photo url: %d, %s\n", photo_id, photoName)
- return err
- }
-
- // Thumbnail
- thumbnailImage := resize.Thumbnail(1024, 1024, image, resize.Bilinear)
-
- if _, err := os.Stat(PhotoCache()); os.IsNotExist(err) {
- if err := os.Mkdir(PhotoCache(), os.ModePerm); err != nil {
- log.Println("ERROR: Could not make image cache directory")
- return err
- }
- }
-
- // Make album cache dir
- albumCachePath := path.Join(PhotoCache(), strconv.Itoa(albumId))
- if _, err := os.Stat(albumCachePath); os.IsNotExist(err) {
- if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil {
- log.Println("ERROR: Could not make album image cache directory")
- return err
- }
- }
-
- // Make photo cache dir
- photoCachePath := path.Join(albumCachePath, strconv.Itoa(int(photo_id)))
- if _, err := os.Stat(photoCachePath); os.IsNotExist(err) {
- if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil {
- log.Println("ERROR: Could not make photo image cache directory")
- return err
- }
- }
-
- // Save thumbnail as jpg
- thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, utils.GenerateToken())
- thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
- thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
- thumbnail_name = thumbnail_name + ".jpg"
-
- photo_file, err = os.Create(path.Join(photoCachePath, thumbnail_name))
- if err != nil {
- log.Println("ERROR: Could not make thumbnail file")
- return err
- }
- defer photo_file.Close()
-
- jpeg.Encode(photo_file, thumbnailImage, &jpeg.Options{Quality: 70})
-
- thumbSize := thumbnailImage.Bounds().Max
- _, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo_id, thumbnail_name, thumbSize.X, thumbSize.Y, models.PhotoThumbnail, "image/jpeg")
- if err != nil {
- return err
- }
-
- // high res
- original_web_safe := false
- for _, web_mime := range WebMimetypes {
- if content_type == web_mime {
- original_web_safe = true
- break
- }
- }
-
- // Generate high res jpeg
- if !original_web_safe {
- highres_name := fmt.Sprintf("highres_%s_%s", photoName, utils.GenerateToken())
- highres_name = strings.ReplaceAll(highres_name, ".", "_")
- highres_name = strings.ReplaceAll(highres_name, " ", "_")
- highres_name = highres_name + ".jpg"
-
- photo_file, err = os.Create(path.Join(photoCachePath, highres_name))
- if err != nil {
- log.Println("ERROR: Could not make highres file")
- return err
- }
- defer photo_file.Close()
-
- jpeg.Encode(photo_file, image, &jpeg.Options{Quality: 70})
-
- _, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo_id, highres_name, image.Bounds().Max.X, image.Bounds().Max.Y, models.PhotoHighRes, "image/jpeg")
- if err != nil {
- log.Printf("Could not insert highres photo url: %d, %s\n", photo_id, photoName)
- return err
- }
- }
-
- return nil
-}
diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go
new file mode 100644
index 00000000..fb7acd2c
--- /dev/null
+++ b/api/scanner/process_photo.go
@@ -0,0 +1,289 @@
+package scanner
+
+import (
+ "database/sql"
+ "fmt"
+ "image"
+ "image/jpeg"
+ "log"
+ "os"
+ "path"
+ "strconv"
+ "strings"
+
+ "github.com/nfnt/resize"
+ "github.com/viktorstrate/photoview/api/graphql/models"
+ "github.com/viktorstrate/photoview/api/utils"
+
+ // Image decoders
+ _ "golang.org/x/image/bmp"
+ // _ "golang.org/x/image/tiff"
+ _ "image/gif"
+ _ "image/png"
+
+ _ "github.com/nf/cr2"
+ _ "golang.org/x/image/webp"
+)
+
+func makePhotoURLChecker(tx *sql.Tx, photoID int) (func(purpose models.PhotoPurpose) (*models.PhotoURL, error), error) {
+ photoURLExistsStmt, err := tx.Prepare("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?")
+ if err != nil {
+ return nil, err
+ }
+
+ return func(purpose models.PhotoPurpose) (*models.PhotoURL, error) {
+ row := photoURLExistsStmt.QueryRow(photoID, purpose)
+ photoURL, err := models.NewPhotoURLFromRow(row)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return photoURL, nil
+ }, nil
+}
+
+func ProcessPhoto(tx *sql.Tx, photo *models.Photo, content_type *string) error {
+
+ log.Printf("Processing photo: %s\n", photo.Path)
+
+ imageData := processImageData{
+ photoPath: photo.Path,
+ }
+
+ photoName := path.Base(photo.Path)
+
+ photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
+ photoBaseExt := path.Ext(photoName)
+
+ photoChecker, err := makePhotoURLChecker(tx, photo.PhotoID)
+ if err != nil {
+ return err
+ }
+
+ // original photo url
+ origURL, err := photoChecker(models.PhotoOriginal)
+ if err != nil {
+ return err
+ }
+
+ if origURL == nil {
+ original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
+ original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
+
+ photoImage, err := imageData.PhotoImage()
+ if err != nil {
+ return err
+ }
+
+ _, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, original_image_name, photoImage.Bounds().Max.X, photoImage.Bounds().Max.Y, models.PhotoOriginal, content_type)
+ if err != nil {
+ log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
+ return err
+ }
+ }
+
+ // Thumbnail
+ thumbURL, err := photoChecker(models.PhotoThumbnail)
+ if err != nil {
+ return err
+ }
+
+ // Highres
+ highResURL, err := photoChecker(models.PhotoHighRes)
+ if err != nil {
+ return err
+ }
+
+ // Make sure photo cache directory exists
+ photoCachePath, err := makePhotoCacheDir(photo)
+ if err != nil {
+ return err
+ }
+
+ // Save thumbnail to cache
+ if thumbURL == nil {
+ thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, utils.GenerateToken())
+ thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
+ thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
+ thumbnail_name = thumbnail_name + ".jpg"
+
+ thumbnailImage, err := imageData.ThumbnailImage()
+ if err != nil {
+ return err
+ }
+
+ err = encodeImageJPEG(path.Join(*photoCachePath, thumbnail_name), thumbnailImage, &jpeg.Options{Quality: 70})
+ if err != nil {
+ log.Println("ERROR: creating high-res cached image")
+ return err
+ }
+
+ thumbSize := thumbnailImage.Bounds().Max
+ _, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, thumbnail_name, thumbSize.X, thumbSize.Y, models.PhotoThumbnail, "image/jpeg")
+ if err != nil {
+ return err
+ }
+ } else if thumbURL != nil {
+ thumbPath := path.Join(*photoCachePath, thumbURL.PhotoName)
+
+ if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
+ fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.PhotoName)
+
+ thumbnailImage, err := imageData.ThumbnailImage()
+ if err != nil {
+ return err
+ }
+
+ err = encodeImageJPEG(thumbPath, thumbnailImage, &jpeg.Options{Quality: 70})
+ if err != nil {
+ log.Println("ERROR: creating thumbnail cached image")
+ return err
+ }
+ }
+ }
+
+ // high res
+ original_web_safe := false
+ for _, web_mime := range WebMimetypes {
+ if *content_type == web_mime {
+ original_web_safe = true
+ break
+ }
+ }
+
+ // Generate high res jpeg
+ if highResURL == nil {
+ if !original_web_safe {
+ highres_name := fmt.Sprintf("highres_%s_%s", photoName, utils.GenerateToken())
+ highres_name = strings.ReplaceAll(highres_name, ".", "_")
+ highres_name = strings.ReplaceAll(highres_name, " ", "_")
+ highres_name = highres_name + ".jpg"
+
+ photoImage, err := imageData.PhotoImage()
+ if err != nil {
+ return err
+ }
+
+ err = encodeImageJPEG(path.Join(*photoCachePath, highres_name), photoImage, &jpeg.Options{Quality: 70})
+ if err != nil {
+ log.Println("ERROR: creating high-res cached image")
+ return err
+ }
+
+ _, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
+ photo.PhotoID, highres_name, photoImage.Bounds().Max.X, photoImage.Bounds().Max.Y, models.PhotoHighRes, "image/jpeg")
+ if err != nil {
+ log.Printf("Could not insert highres photo url: %d, %s\n", photo.PhotoID, photoName)
+ return err
+ }
+ }
+ } else if highResURL != nil {
+ highResPath := path.Join(*photoCachePath, highResURL.PhotoName)
+
+ if _, err := os.Stat(highResPath); os.IsNotExist(err) {
+ fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.PhotoName)
+
+ photoImage, err := imageData.PhotoImage()
+ if err != nil {
+ return err
+ }
+
+ err = encodeImageJPEG(highResPath, photoImage, &jpeg.Options{Quality: 70})
+ if err != nil {
+ log.Println("ERROR: creating high-res cached image")
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func makePhotoCacheDir(photo *models.Photo) (*string, error) {
+
+ // Make root cache dir if not exists
+ if _, err := os.Stat(PhotoCache()); os.IsNotExist(err) {
+ if err := os.Mkdir(PhotoCache(), os.ModePerm); err != nil {
+ log.Println("ERROR: Could not make root image cache directory")
+ return nil, err
+ }
+ }
+
+ // Make album cache dir if not exists
+ albumCachePath := path.Join(PhotoCache(), strconv.Itoa(photo.AlbumId))
+ if _, err := os.Stat(albumCachePath); os.IsNotExist(err) {
+ if err := os.Mkdir(albumCachePath, os.ModePerm); err != nil {
+ log.Println("ERROR: Could not make album image cache directory")
+ return nil, err
+ }
+ }
+
+ // Make photo cache dir if not exists
+ photoCachePath := path.Join(albumCachePath, strconv.Itoa(photo.PhotoID))
+ if _, err := os.Stat(photoCachePath); os.IsNotExist(err) {
+ if err := os.Mkdir(photoCachePath, os.ModePerm); err != nil {
+ log.Println("ERROR: Could not make photo image cache directory")
+ return nil, err
+ }
+ }
+
+ return &photoCachePath, nil
+}
+
+func encodeImageJPEG(photoPath string, photoImage image.Image, jpegOptions *jpeg.Options) error {
+ photo_file, err := os.Create(photoPath)
+ if err != nil {
+ log.Printf("ERROR: Could not create file: %s\n", photoPath)
+ return err
+ }
+ defer photo_file.Close()
+
+ err = jpeg.Encode(photo_file, photoImage, jpegOptions)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type processImageData struct {
+ photoPath string
+ _photoImage image.Image
+ _thumbnailImage image.Image
+}
+
+func (img *processImageData) PhotoImage() (image.Image, error) {
+ if img._photoImage != nil {
+ return img._photoImage, nil
+ }
+
+ photoFile, err := os.Open(img.photoPath)
+ if err != nil {
+ return nil, err
+ }
+ defer photoFile.Close()
+
+ photoImg, _, err := image.Decode(photoFile)
+ if err != nil {
+ log.Println("ERROR: decoding image")
+ return nil, err
+ }
+
+ img._photoImage = photoImg
+ return img._photoImage, nil
+}
+
+func (img *processImageData) ThumbnailImage() (image.Image, error) {
+ photoImage, err := img.PhotoImage()
+ if err != nil {
+ return nil, err
+ }
+
+ thumbImage := resize.Thumbnail(1024, 1024, photoImage, resize.Bilinear)
+ img._thumbnailImage = thumbImage
+
+ return img._thumbnailImage, nil
+}
diff --git a/ui/src/components/messages/Messages.js b/ui/src/components/messages/Messages.js
index 54ce148c..a2f9bab2 100644
--- a/ui/src/components/messages/Messages.js
+++ b/ui/src/components/messages/Messages.js
@@ -124,7 +124,9 @@ const Messages = () => {
)
})}
-
+ {localStorage.getItem('token') && (
+
+ )}
)
}