diff --git a/frontend/src/common/gallery.js b/frontend/src/common/gallery.js index 1947a844d..f32874487 100644 --- a/frontend/src/common/gallery.js +++ b/frontend/src/common/gallery.js @@ -12,7 +12,11 @@ class Gallery { } createPhotoSizes(photo) { - const result = {}; + const result = { + title: photo.PhotoTitle, + download_url: photo.getDownloadUrl(), + }; + const thumbs = window.appConfig.thumbnails; for (let i = 0; i < thumbs.length; i++) { @@ -22,7 +26,6 @@ class Gallery { src: photo.getThumbnailUrl(thumbs[i].Name), w: size.width, h: size.height, - title: photo.PhotoTitle, }; } @@ -55,24 +58,25 @@ class Gallery { const shareButtons = [ - {id:"download", label:"Download image", url:"foo", download:true}, + {id: "download", label: "Download image", url: "{{raw_image_url}}", download: true}, ]; const options = { index: index, history: false, - preload: [1,1], + preload: [1, 1], focus: true, modal: true, closeEl: true, captionEl: true, fullscreenEl: true, zoomEl: true, - shareEl: false, + shareEl: true, shareButtons: shareButtons, counterEl: false, arrowEl: true, preloaderEl: true, + getImageURLForShare: function() { return gallery.currItem.download_url}, }; let photosWithSizes = this.photosWithSizes(); @@ -115,7 +119,6 @@ class Gallery { item.src = item[nextSize].src; item.w = item[nextSize].w; item.h = item[nextSize].h; - item.title = item[nextSize].title; previousSize = nextSize; }); diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index 9ec656846..84f7e9a92 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -38,6 +38,10 @@ class Photo extends Abstract { return "/api/v1/thumbnails/" + this.FileHash + "/" + type; } + getDownloadUrl() { + return "/api/v1/download/" + this.FileHash; + } + getThumbnailSrcset() { const result = []; diff --git a/internal/api/download.go b/internal/api/download.go new file mode 100644 index 000000000..badc53b51 --- /dev/null +++ b/internal/api/download.go @@ -0,0 +1,48 @@ +package api + +import ( + "fmt" + + "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/util" + log "github.com/sirupsen/logrus" + + "github.com/gin-gonic/gin" + "github.com/photoprism/photoprism/internal/photoprism" +) + +// GET /api/v1/download/:hash +// +// Parameters: +// hash: string The file hash as returned by the search API +func GetDownload(router *gin.RouterGroup, conf *config.Config) { + router.GET("/download/:hash", func(c *gin.Context) { + fileHash := c.Param("hash") + + search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db()) + file, err := search.FindFileByHash(fileHash) + + if err != nil { + c.AbortWithStatusJSON(404, gin.H{"error": err.Error()}) + return + } + + fileName := fmt.Sprintf("%s/%s", conf.OriginalsPath(), file.FileName) + + if !util.Exists(fileName) { + log.Errorf("could not find original: %s", fileHash) + c.Data(404, "image/svg+xml", photoIconSvg) + + // Set missing flag so that the file doesn't show up in search results anymore + file.FileMissing = true + conf.Db().Save(&file) + return + } + + downloadFileName := file.DownloadFileName(conf.Db()) + + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) + + c.File(fileName) + }) +} diff --git a/internal/api/svg.go b/internal/api/svg.go new file mode 100644 index 000000000..f52be2dd7 --- /dev/null +++ b/internal/api/svg.go @@ -0,0 +1,7 @@ +package api + +var photoIconSvg = []byte(` + + + +`) diff --git a/internal/api/thumbnails.go b/internal/api/thumbnails.go index 929328f9c..1d97c99d6 100644 --- a/internal/api/thumbnails.go +++ b/internal/api/thumbnails.go @@ -11,12 +11,6 @@ import ( "github.com/photoprism/photoprism/internal/photoprism" ) -var photoIconSvg = []byte(` - - - -`) - // GET /api/v1/thumbnails/:hash/:type // // Parameters: diff --git a/internal/models/file.go b/internal/models/file.go index 52f4852a9..37051e63a 100644 --- a/internal/models/file.go +++ b/internal/models/file.go @@ -1,28 +1,44 @@ package models import ( + "fmt" + + "github.com/gosimple/slug" "github.com/jinzhu/gorm" ) // An image or sidecar file that belongs to a photo type File struct { gorm.Model - Photo *Photo - PhotoID uint - FilePrimary bool - FileMissing bool - FileDuplicate bool - FileName string `gorm:"type:varchar(512);index"` // max 3072 bytes / 4 bytes for utf8mb4 = 768 chars - FileType string `gorm:"type:varchar(32)"` - FileMime string `gorm:"type:varchar(64)"` - FileWidth int - FileHeight int - FileOrientation int - FileAspectRatio float64 - FileMainColor string - FileColors string - FileLuminance string - FileSaturation uint - FileHash string `gorm:"type:varchar(128);unique_index"` - FileNotes string `gorm:"type:text"` + Photo *Photo + PhotoID uint + FilePrimary bool + FileMissing bool + FileDuplicate bool + FileName string `gorm:"type:varchar(512);index"` // max 3072 bytes / 4 bytes for utf8mb4 = 768 chars + FileOriginalName string + FileType string `gorm:"type:varchar(32)"` + FileMime string `gorm:"type:varchar(64)"` + FileWidth int + FileHeight int + FileOrientation int + FileAspectRatio float64 + FileMainColor string + FileColors string + FileLuminance string + FileSaturation uint + FileHash string `gorm:"type:varchar(128);unique_index"` + FileNotes string `gorm:"type:text"` +} + +func (f *File) DownloadFileName(db *gorm.DB) string { + var photo Photo + + db.Model(f).Related(&photo) + + name := slug.MakeLang(photo.PhotoTitle, "en") + + result := fmt.Sprintf("%s.%s", name, f.FileType) + + return result } diff --git a/internal/server/routes.go b/internal/server/routes.go index 37e550b1a..f00f166b1 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -20,6 +20,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { { api.GetPhotos(v1, conf) api.GetThumbnail(v1, conf) + api.GetDownload(v1, conf) api.LikePhoto(v1, conf) api.DislikePhoto(v1, conf) }