Work on photo serving and processing
This commit is contained in:
parent
9a8701ecd0
commit
d50b8034d1
|
@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS photo_url (
|
|||
width int NOT NULL,
|
||||
height int NOT NULL,
|
||||
purpose varchar(64) NOT NULL,
|
||||
content_type varchar(64) NOT NULL,
|
||||
|
||||
PRIMARY KEY (url_id),
|
||||
FOREIGN KEY (photo_id) REFERENCES photo(photo_id)
|
||||
|
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
@ -27,7 +28,8 @@ type PhotoURL struct {
|
|||
PhotoName string
|
||||
Width int
|
||||
Height int
|
||||
purpose PhotoPurpose
|
||||
Purpose PhotoPurpose
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func (p *Photo) ID() string {
|
||||
|
@ -59,5 +61,15 @@ func NewPhotosFromRows(rows *sql.Rows) ([]*Photo, error) {
|
|||
}
|
||||
|
||||
func (p *PhotoURL) URL() string {
|
||||
return "URL:" + p.PhotoName
|
||||
return fmt.Sprintf("/photo/%s", p.PhotoName)
|
||||
}
|
||||
|
||||
func NewPhotoURLFromRow(row *sql.Row) (*PhotoURL, error) {
|
||||
url := PhotoURL{}
|
||||
|
||||
if err := row.Scan(&url.UrlID, &url.PhotoId, &url.PhotoName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &url, nil
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package resolvers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
api "github.com/viktorstrate/photoview/api/graphql"
|
||||
"github.com/viktorstrate/photoview/api/graphql/auth"
|
||||
|
@ -9,7 +10,17 @@ import (
|
|||
)
|
||||
|
||||
func (r *queryResolver) MyPhotos(ctx context.Context) ([]*models.Photo, error) {
|
||||
panic("Not implemented")
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
rows, err := r.Database.Query("SELECT photo.* FROM photo, album WHERE photo.album_id = album.album_id AND album.owner_id = ?", user.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewPhotosFromRows(rows)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Photo(ctx context.Context, id string) (*models.Photo, error) {
|
||||
|
@ -45,27 +56,25 @@ func (r *photoResolver) HighRes(ctx context.Context, obj *models.Photo) (*models
|
|||
}
|
||||
|
||||
func (r *photoResolver) Original(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
|
||||
panic("not implemented")
|
||||
// row := r.Database.QueryRow("SELECT photo_url.* FROM photo, photo_url WHERE photo.photo_id = ? AND photo.original_url = photo_url.url_id", obj.PhotoID)
|
||||
row := r.Database.QueryRow("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?", obj.PhotoID, models.PhotoOriginal)
|
||||
|
||||
// var photoUrl models.PhotoURL
|
||||
// if err := row.Scan(&photoUrl.UrlID, &photoUrl.Token, &photoUrl.Width, &photoUrl.Height); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
url, err := models.NewPhotoURLFromRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return &photoUrl, nil
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (r *photoResolver) Thumbnail(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
|
||||
panic("not implemented")
|
||||
// row := r.Database.QueryRow("SELECT photo_url.* FROM photo, photo_url WHERE photo.photo_id = ? AND photo.thumbnail_url = photo_url.url_id", obj.PhotoID)
|
||||
row := r.Database.QueryRow("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?", obj.PhotoID, models.PhotoThumbnail)
|
||||
|
||||
// var photoUrl models.PhotoURL
|
||||
// if err := row.Scan(&photoUrl.UrlID, &photoUrl.Token, &photoUrl.Width, &photoUrl.Height); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
url, err := models.NewPhotoURLFromRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return &photoUrl, nil
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.Album, error) {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func ImageRoutes() chi.Router {
|
||||
router := chi.NewRouter()
|
||||
router.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||
image_name := chi.URLParam(r, "name")
|
||||
w.Write([]byte(fmt.Sprintf("Image: %s", image_name)))
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
)
|
||||
|
||||
func PhotoRoutes(db *sql.DB) chi.Router {
|
||||
router := chi.NewRouter()
|
||||
router.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||
image_name := chi.URLParam(r, "name")
|
||||
|
||||
row := db.QueryRow("SELECT photo_url.purpose, photo.path, 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)
|
||||
|
||||
var purpose models.PhotoPurpose
|
||||
var path string
|
||||
var content_type string
|
||||
var album_id int
|
||||
|
||||
if err := row.Scan(&purpose, &path, &album_id, &content_type); err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("404"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", content_type)
|
||||
|
||||
var file *os.File
|
||||
|
||||
if purpose == models.PhotoThumbnail {
|
||||
var err error
|
||||
file, err = os.Open(fmt.Sprintf("./image-cache/%d/%s", album_id, image_name))
|
||||
if err != nil {
|
||||
w.Write([]byte("Error: " + err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if purpose == models.PhotoOriginal {
|
||||
var err error
|
||||
file, err = os.Open(path)
|
||||
if err != nil {
|
||||
w.Write([]byte("Error: " + err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if stats, err := file.Stat(); err == nil {
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stats.Size()))
|
||||
}
|
||||
|
||||
io.Copy(w, file)
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
|
@ -18,13 +18,14 @@ import (
|
|||
// Image decoders
|
||||
_ "golang.org/x/image/bmp"
|
||||
// _ "golang.org/x/image/tiff"
|
||||
_ "github.com/nf/cr2"
|
||||
_ "golang.org/x/image/webp"
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
|
||||
_ "github.com/nf/cr2"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
func ProcessImage(tx *sql.Tx, photoPath string, albumId int) error {
|
||||
func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string) error {
|
||||
|
||||
log.Printf("Processing image: %s\n", photoPath)
|
||||
|
||||
|
@ -52,19 +53,25 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
thumbFile, err := os.Open(photoPath)
|
||||
photo_file, err := os.Open(photoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer thumbFile.Close()
|
||||
defer photo_file.Close()
|
||||
|
||||
image, _, err := image.Decode(thumbFile)
|
||||
image, _, err := image.Decode(photo_file)
|
||||
if err != nil {
|
||||
log.Println("ERROR: decoding image")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose) VALUES (?, ?, ?, ?, ?)", photo_id, photoName, image.Bounds().Max.X, image.Bounds().Max.Y, models.PhotoOriginal)
|
||||
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
|
||||
photoBaseExt := path.Ext(photoName)
|
||||
|
||||
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, 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
|
||||
|
@ -87,26 +94,24 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
// Generate image token name
|
||||
thumbnailToken := generateToken()
|
||||
|
||||
// Save thumbnail as jpg
|
||||
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, thumbnailToken)
|
||||
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, generateToken())
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
|
||||
thumbnail_name = thumbnail_name + ".jpg"
|
||||
|
||||
thumbFile, err = os.Create(path.Join(albumCachePath, thumbnail_name))
|
||||
photo_file, err = os.Create(path.Join(albumCachePath, thumbnail_name))
|
||||
if err != nil {
|
||||
log.Println("ERROR: Could not make thumbnail file")
|
||||
return err
|
||||
}
|
||||
defer thumbFile.Close()
|
||||
defer photo_file.Close()
|
||||
|
||||
jpeg.Encode(thumbFile, thumbnailImage, &jpeg.Options{Quality: 70})
|
||||
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) VALUES (?, ?, ?, ?, ?)", photo_id, thumbnail_name, thumbSize.X, thumbSize.Y, models.PhotoThumbnail)
|
||||
_, 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
|
||||
}
|
||||
|
|
|
@ -13,6 +13,22 @@ import (
|
|||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
)
|
||||
|
||||
type scanner_cache map[string]interface{}
|
||||
|
||||
func (cache *scanner_cache) insert_photo_type(path string, content_type string) {
|
||||
(*cache)["photo_type/"+path] = content_type
|
||||
}
|
||||
|
||||
func (cache *scanner_cache) get_photo_type(path string) *string {
|
||||
result := (*cache)["photo_type/"+path]
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
photo_type := result.(string)
|
||||
return &photo_type
|
||||
}
|
||||
|
||||
func ScanUser(database *sql.DB, userId string) error {
|
||||
|
||||
row := database.QueryRow("SELECT * FROM user WHERE user_id = ?", userId)
|
||||
|
@ -29,6 +45,8 @@ func ScanUser(database *sql.DB, userId string) error {
|
|||
}
|
||||
|
||||
func scan(database *sql.DB, user *models.User) {
|
||||
scanner_cache := make(scanner_cache)
|
||||
|
||||
type scanInfo struct {
|
||||
path string
|
||||
parentId *int
|
||||
|
@ -89,14 +107,22 @@ func scan(database *sql.DB, user *models.User) {
|
|||
for _, item := range dirContent {
|
||||
photoPath := path.Join(albumPath, item.Name())
|
||||
|
||||
if !item.IsDir() && isPathImage(photoPath) {
|
||||
if !item.IsDir() && isPathImage(photoPath, &scanner_cache) {
|
||||
tx, err := database.Begin()
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not begin database transaction for image %s: %s\n", photoPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ProcessImage(tx, photoPath, albumId); err != nil {
|
||||
content_type := scanner_cache.get_photo_type(photoPath)
|
||||
if content_type == nil {
|
||||
log.Println("Content type not found from cache")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Content type: %s\n", *content_type)
|
||||
|
||||
if err := ProcessImage(tx, photoPath, albumId, *content_type); err != nil {
|
||||
log.Printf("ERROR: processing image %s: %s", photoPath, err)
|
||||
tx.Rollback()
|
||||
return
|
||||
|
@ -110,7 +136,7 @@ func scan(database *sql.DB, user *models.User) {
|
|||
for _, item := range dirContent {
|
||||
subalbumPath := path.Join(albumPath, item.Name())
|
||||
|
||||
if item.IsDir() && directoryContainsPhotos(subalbumPath) {
|
||||
if item.IsDir() && directoryContainsPhotos(subalbumPath, &scanner_cache) {
|
||||
scanQueue.PushBack(scanInfo{
|
||||
path: subalbumPath,
|
||||
parentId: &albumId,
|
||||
|
@ -122,7 +148,7 @@ func scan(database *sql.DB, user *models.User) {
|
|||
log.Println("Done scanning")
|
||||
}
|
||||
|
||||
func directoryContainsPhotos(rootPath string) bool {
|
||||
func directoryContainsPhotos(rootPath string, cache *scanner_cache) bool {
|
||||
scanQueue := list.New()
|
||||
scanQueue.PushBack(rootPath)
|
||||
|
||||
|
@ -142,7 +168,7 @@ func directoryContainsPhotos(rootPath string) bool {
|
|||
if fileInfo.IsDir() {
|
||||
scanQueue.PushBack(filePath)
|
||||
} else {
|
||||
if isPathImage(filePath) {
|
||||
if isPathImage(filePath, cache) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +188,11 @@ var supported_mimetypes = [...]string{
|
|||
"image/bmp",
|
||||
}
|
||||
|
||||
func isPathImage(path string) bool {
|
||||
func isPathImage(path string, cache *scanner_cache) bool {
|
||||
if cache.get_photo_type(path) != nil {
|
||||
log.Printf("Image cache hit: %s\n", path)
|
||||
return true
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("Could not open file %s: %s\n", path, err)
|
||||
|
@ -183,6 +213,7 @@ func isPathImage(path string) bool {
|
|||
|
||||
for _, supported_mime := range supported_mimetypes {
|
||||
if supported_mime == imgType.MIME.Value {
|
||||
cache.insert_photo_type(path, supported_mime)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ func main() {
|
|||
router.Handle("/", handler.Playground("GraphQL playground", "/graphql"))
|
||||
router.Handle("/graphql", handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig)))
|
||||
|
||||
router.Mount("/image", routes.ImageRoutes())
|
||||
router.Mount("/photo", routes.PhotoRoutes(db))
|
||||
|
||||
log.Printf("🚀 Graphql playground ready at http://localhost:%s/", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, router))
|
||||
|
|
Loading…
Reference in New Issue