2020-02-23 18:00:08 +01:00
|
|
|
package scanner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-05-14 14:35:08 +02:00
|
|
|
"github.com/pkg/errors"
|
2020-02-23 18:00:08 +01:00
|
|
|
"github.com/viktorstrate/photoview/api/graphql/models"
|
|
|
|
"github.com/viktorstrate/photoview/api/utils"
|
|
|
|
|
|
|
|
// Image decoders
|
|
|
|
_ "image/gif"
|
|
|
|
_ "image/png"
|
|
|
|
|
2020-03-02 16:32:24 +01:00
|
|
|
_ "golang.org/x/image/bmp"
|
|
|
|
_ "golang.org/x/image/tiff"
|
2020-02-23 18:00:08 +01:00
|
|
|
_ "golang.org/x/image/webp"
|
|
|
|
)
|
|
|
|
|
2020-02-26 21:23:13 +01:00
|
|
|
// Higher order function used to check if PhotoURL for a given PhotoPurpose exists
|
2020-02-23 18:00:08 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-02-26 21:23:13 +01:00
|
|
|
func ProcessPhoto(tx *sql.Tx, photo *models.Photo) error {
|
2020-02-23 18:00:08 +01:00
|
|
|
|
|
|
|
log.Printf("Processing photo: %s\n", photo.Path)
|
|
|
|
|
2020-05-14 14:35:08 +02:00
|
|
|
imageData := EncodeImageData{
|
2020-03-12 12:30:55 +01:00
|
|
|
photo: photo,
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
|
2020-05-14 14:35:08 +02:00
|
|
|
photoUrlFromDB, err := makePhotoURLChecker(tx, photo.PhotoID)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// original photo url
|
2020-05-14 14:35:08 +02:00
|
|
|
origURL, err := photoUrlFromDB(models.PhotoOriginal)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thumbnail
|
2020-05-14 14:35:08 +02:00
|
|
|
thumbURL, err := photoUrlFromDB(models.PhotoThumbnail)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
2020-05-14 14:35:08 +02:00
|
|
|
return errors.Wrap(err, "error processing thumbnail")
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Highres
|
2020-05-14 14:35:08 +02:00
|
|
|
highResURL, err := photoUrlFromDB(models.PhotoHighRes)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
2020-05-14 14:35:08 +02:00
|
|
|
return errors.Wrap(err, "error processing highres")
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure photo cache directory exists
|
|
|
|
photoCachePath, err := makePhotoCacheDir(photo)
|
|
|
|
if err != nil {
|
2020-05-14 14:35:08 +02:00
|
|
|
return errors.Wrap(err, "cache directory error")
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate high res jpeg
|
2020-05-15 16:36:02 +02:00
|
|
|
var photoDimensions *PhotoDimensions
|
|
|
|
var baseImagePath string = photo.Path
|
|
|
|
|
2020-02-23 18:00:08 +01:00
|
|
|
if highResURL == nil {
|
2020-02-26 21:23:13 +01:00
|
|
|
|
|
|
|
contentType, err := imageData.ContentType()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
if !contentType.isWebCompatible() {
|
|
|
|
highres_name := fmt.Sprintf("highres_%s_%s", path.Base(photo.Path), utils.GenerateToken())
|
2020-02-23 18:00:08 +01:00
|
|
|
highres_name = strings.ReplaceAll(highres_name, ".", "_")
|
|
|
|
highres_name = strings.ReplaceAll(highres_name, " ", "_")
|
|
|
|
highres_name = highres_name + ".jpg"
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
baseImagePath = path.Join(*photoCachePath, highres_name)
|
|
|
|
|
|
|
|
err = imageData.EncodeHighRes(tx, baseImagePath)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
2020-05-15 16:36:02 +02:00
|
|
|
return errors.Wrap(err, "creating high-res cached image")
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
photoDimensions, err = GetPhotoDimensions(baseImagePath)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
2020-05-15 16:36:02 +02:00
|
|
|
return err
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
|
2020-05-15 16:36:02 +02:00
|
|
|
photo.PhotoID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
2020-05-15 16:36:02 +02:00
|
|
|
log.Printf("Could not insert highres photo url: %d, %s\n", photo.PhotoID, path.Base(photo.Path))
|
2020-02-23 18:00:08 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2020-05-14 14:35:08 +02:00
|
|
|
} else {
|
|
|
|
// Verify that highres photo still exists in cache
|
2020-05-15 16:36:02 +02:00
|
|
|
baseImagePath = path.Join(*photoCachePath, highResURL.PhotoName)
|
2020-02-23 18:00:08 +01:00
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
if _, err := os.Stat(baseImagePath); os.IsNotExist(err) {
|
2020-02-23 18:00:08 +01:00
|
|
|
fmt.Printf("High-res photo found in database but not in cache, re-encoding photo to cache: %s\n", highResURL.PhotoName)
|
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
err = imageData.EncodeHighRes(tx, baseImagePath)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "creating high-res cached image")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save original photo to database
|
|
|
|
if origURL == nil {
|
|
|
|
// Make sure photo dimensions is set
|
|
|
|
if photoDimensions == nil {
|
|
|
|
photoDimensions, err = GetPhotoDimensions(baseImagePath)
|
2020-05-15 15:23:21 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-15 16:36:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if err = saveOriginalPhotoToDB(tx, photo, imageData, photoDimensions); err != nil {
|
|
|
|
return errors.Wrap(err, "saving original photo to database")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save thumbnail to cache
|
|
|
|
if thumbURL == nil {
|
|
|
|
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", path.Base(photo.Path), utils.GenerateToken())
|
|
|
|
thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
|
|
|
|
thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
|
|
|
|
thumbnail_name = thumbnail_name + ".jpg"
|
|
|
|
|
|
|
|
// thumbnailImage, err := imageData.ThumbnailImage(tx)
|
|
|
|
// if err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
2020-05-15 15:23:21 +02:00
|
|
|
|
2020-05-15 16:36:02 +02:00
|
|
|
thumbOutputPath := path.Join(*photoCachePath, thumbnail_name)
|
|
|
|
|
|
|
|
thumbSize, err := EncodeThumbnail(baseImagePath, thumbOutputPath)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "could not create thumbnail cached image")
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.PhotoID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Verify that thumbnail photo still exists in cache
|
|
|
|
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)
|
|
|
|
|
|
|
|
_, err := EncodeThumbnail(baseImagePath, thumbPath)
|
2020-02-23 18:00:08 +01:00
|
|
|
if err != nil {
|
2020-05-15 16:36:02 +02:00
|
|
|
return errors.Wrap(err, "could not create thumbnail cached image")
|
2020-02-23 18:00:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2020-05-15 16:36:02 +02:00
|
|
|
|
|
|
|
func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Photo, imageData EncodeImageData, photoDimensions *PhotoDimensions) error {
|
|
|
|
photoName := path.Base(photo.Path)
|
|
|
|
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
|
|
|
|
photoBaseExt := path.Ext(photoName)
|
|
|
|
|
|
|
|
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
|
|
|
|
original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
|
|
|
|
|
|
|
|
contentType, err := imageData.ContentType()
|
|
|
|
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, photoDimensions.Width, photoDimensions.Height, models.PhotoOriginal, contentType)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Could not insert original photo url: %d, %s\n", photo.PhotoID, photoName)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|