diff --git a/internal/api/account.go b/internal/api/account.go index 6ac54dfe3..2fc5c9b47 100644 --- a/internal/api/account.go +++ b/internal/api/account.go @@ -170,7 +170,7 @@ func ShareWithAccount(router *gin.RouterGroup) { } for _, file := range files { - dstFileName := path.Join(dst, file.ShareFileName()) + dstFileName := path.Join(dst, file.ShareBase()) fileShare := entity.NewFileShare(file.ID, m.ID, dstFileName) entity.FirstOrCreateFileShare(fileShare) } diff --git a/internal/api/download.go b/internal/api/download.go index 8b49e4e4b..8e3af8150 100644 --- a/internal/api/download.go +++ b/internal/api/download.go @@ -2,8 +2,11 @@ package api import ( "fmt" + "github.com/photoprism/photoprism/internal/entity" "net/http" + "github.com/photoprism/photoprism/internal/service" + "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/query" "github.com/photoprism/photoprism/pkg/fs" @@ -48,9 +51,31 @@ func GetDownload(router *gin.RouterGroup) { return } - downloadFileName := f.ShareFileName() + name := entity.DownloadNameFile - c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) + switch c.Query("name") { + case "file": + name = entity.DownloadNameFile + case "share": + name = entity.DownloadNameShare + case "original": + name = entity.DownloadNameOriginal + default: + name = service.Config().Settings().Download.Name + } + + var downloadName string + + switch name { + case entity.DownloadNameFile: + downloadName = f.Base() + case entity.DownloadNameOriginal: + downloadName = f.OriginalBase() + default: + downloadName = f.ShareBase() + } + + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadName)) c.File(fileName) }) diff --git a/internal/api/photo.go b/internal/api/photo.go index 715e75bdb..d79e559fc 100644 --- a/internal/api/photo.go +++ b/internal/api/photo.go @@ -144,7 +144,7 @@ func GetPhotoDownload(router *gin.RouterGroup) { return } - downloadFileName := f.ShareFileName() + downloadFileName := f.ShareBase() c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName)) diff --git a/internal/api/thumbs.go b/internal/api/thumbs.go index efc4db276..e457db652 100644 --- a/internal/api/thumbs.go +++ b/internal/api/thumbs.go @@ -159,13 +159,13 @@ func GetThumb(router *gin.RouterGroup) { } // Cache thumbnail filename. - if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil { + if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareBase()}); err == nil { logError("thumbnail", cache.Set(cacheKey, cached)) log.Debugf("cached %s [%s]", cacheKey, time.Since(start)) } if c.Query("download") != "" { - c.FileAttachment(thumbnail, f.ShareFileName()) + c.FileAttachment(thumbnail, f.ShareBase()) } else { c.File(thumbnail) } @@ -272,13 +272,13 @@ func AlbumThumb(router *gin.RouterGroup) { return } - if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil { + if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareBase()}); err == nil { logError("album-thumbnail", cache.Set(cacheKey, cached)) log.Debugf("cached %s [%s]", cacheKey, time.Since(start)) } if c.Query("download") != "" { - c.FileAttachment(thumbnail, f.ShareFileName()) + c.FileAttachment(thumbnail, f.ShareBase()) } else { c.File(thumbnail) } @@ -387,13 +387,13 @@ func LabelThumb(router *gin.RouterGroup) { return } - if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareFileName()}); err == nil { + if cached, err := json.Marshal(ThumbCache{thumbnail, f.ShareBase()}); err == nil { logError("label-thumbnail", cache.Set(cacheKey, cached)) log.Debugf("cached %s [%s]", cacheKey, time.Since(start)) } if c.Query("download") != "" { - c.FileAttachment(thumbnail, f.ShareFileName()) + c.FileAttachment(thumbnail, f.ShareBase()) } else { c.File(thumbnail) } diff --git a/internal/api/video.go b/internal/api/video.go index 6c54ebde8..57e74f515 100644 --- a/internal/api/video.go +++ b/internal/api/video.go @@ -90,7 +90,7 @@ func GetVideo(router *gin.RouterGroup) { c.Header("Content-Type", `video/mp4; codecs="avc1"`) if c.Query("download") != "" { - c.FileAttachment(fileName, f.ShareFileName()) + c.FileAttachment(fileName, f.ShareBase()) } else { c.File(fileName) } diff --git a/internal/api/zip.go b/internal/api/zip.go index 03255cf81..5c85265a9 100644 --- a/internal/api/zip.go +++ b/internal/api/zip.go @@ -88,7 +88,7 @@ func CreateZip(router *gin.RouterGroup) { for _, f := range files { fileName := photoprism.FileName(f.FileRoot, f.FileName) - fileAlias := f.ShareFileName() + fileAlias := f.ShareBase() if fs.FileExists(fileName) { if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil { diff --git a/internal/config/settings.go b/internal/config/settings.go index 2e6c0c9eb..664cbf2a2 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -5,6 +5,8 @@ import ( "io/ioutil" "os" + "github.com/photoprism/photoprism/internal/entity" + "github.com/photoprism/photoprism/internal/i18n" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" @@ -73,11 +75,16 @@ type StackSettings struct { Name bool `json:"name" yaml:"name"` } -// ShareSettings represents photo sharing settings. +// ShareSettings represents content sharing settings. type ShareSettings struct { Title string `json:"title" yaml:"title"` } +// DownloadSettings represents content download settings. +type DownloadSettings struct { + Name entity.DownloadName `json:"name" yaml:"name"` +} + // Settings represents user settings for Web UI, indexing, and import. type Settings struct { UI UISettings `json:"ui" yaml:"ui"` @@ -88,6 +95,7 @@ type Settings struct { Index IndexSettings `json:"index" yaml:"index"` Stack StackSettings `json:"stack" yaml:"stack"` Share ShareSettings `json:"share" yaml:"share"` + Download DownloadSettings `json:"download" yaml:"download"` } // NewSettings creates a new Settings instance. @@ -96,7 +104,7 @@ func NewSettings() *Settings { UI: UISettings{ Scrollbar: true, Theme: "default", - Language: "en", + Language: i18n.Default.Locale(), }, Templates: TemplateSettings{ Default: "index.tmpl", @@ -122,11 +130,11 @@ func NewSettings() *Settings { Logs: true, }, Import: ImportSettings{ - Path: "/", + Path: entity.RootPath, Move: false, }, Index: IndexSettings{ - Path: "/", + Path: entity.RootPath, Rescan: false, Convert: true, }, @@ -135,6 +143,12 @@ func NewSettings() *Settings { Meta: true, Name: false, }, + Share: ShareSettings{ + Title: "", + }, + Download: DownloadSettings{ + Name: entity.DownloadNameDefault, + }, } } diff --git a/internal/config/testdata/settings.yml b/internal/config/testdata/settings.yml index 6919d8574..c035bd020 100755 --- a/internal/config/testdata/settings.yml +++ b/internal/config/testdata/settings.yml @@ -36,3 +36,5 @@ stack: name: false share: title: "" +download: + name: file diff --git a/internal/entity/file.go b/internal/entity/file.go index 293add743..1913b105c 100644 --- a/internal/entity/file.go +++ b/internal/entity/file.go @@ -2,6 +2,7 @@ package entity import ( "fmt" + "path/filepath" "strings" "time" @@ -14,6 +15,15 @@ import ( "github.com/ulule/deepcopier" ) +type DownloadName string + +const ( + DownloadNameFile DownloadName = "file" + DownloadNameOriginal DownloadName = "original" + DownloadNameShare DownloadName = "share" + DownloadNameDefault = DownloadNameFile +) + type Files []File // File represents an image or sidecar file that belongs to a photo. @@ -98,8 +108,26 @@ func (m *File) BeforeCreate(scope *gorm.Scope) error { return scope.SetColumn("FileUID", rnd.PPID('f')) } -// ShareFileName returns a meaningful file name useful for sharing. -func (m *File) ShareFileName() string { +// Base returns the file name without path. +func (m *File) Base() string { + if m.FileName == "" { + return m.ShareBase() + } + + return filepath.Base(m.FileName) +} + +// OriginalBase returns the original file name without path. +func (m *File) OriginalBase() string { + if m.OriginalName == "" { + return m.Base() + } + + return filepath.Base(m.OriginalName) +} + +// ShareBase returns a meaningful file name useful for sharing. +func (m *File) ShareBase() string { photo := m.RelatedPhoto() if photo == nil { diff --git a/internal/entity/file_test.go b/internal/entity/file_test.go index 33ea1867c..9e2f6150a 100644 --- a/internal/entity/file_test.go +++ b/internal/entity/file_test.go @@ -29,7 +29,7 @@ func TestFile_ShareFileName(t *testing.T) { photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"} file := &File{Photo: photo, FileType: "jpg", FileUID: "foobar345678765", FileHash: "e98eb86480a72bd585d228a709f0622f90e86cbc"} - filename := file.ShareFileName() + filename := file.ShareBase() assert.Contains(t, filename, "20190115-000000-Berlin-Morning-Mood") assert.Contains(t, filename, fs.JpegExt) @@ -38,14 +38,14 @@ func TestFile_ShareFileName(t *testing.T) { photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: ""} file := &File{Photo: photo, FileType: "jpg", PhotoUID: "123", FileUID: "foobar345678765", FileHash: "e98eb86480a72bd585d228a709f0622f90e86cbc"} - filename := file.ShareFileName() + filename := file.ShareBase() assert.Equal(t, filename, "e98eb86480a72bd585d228a709f0622f90e86cbc.jpg") }) t.Run("photo without photo", func(t *testing.T) { file := &File{Photo: nil, FileType: "jpg", FileUID: "foobar345678765", FileHash: "e98eb86480a72bd585d228a709f0622f90e86cbc"} - filename := file.ShareFileName() + filename := file.ShareBase() assert.Equal(t, "e98eb86480a72bd585d228a709f0622f90e86cbc.jpg", filename) }) @@ -54,14 +54,14 @@ func TestFile_ShareFileName(t *testing.T) { file := &File{Photo: photo, FileType: "jpg", FileUID: "foobar345678765", FileHash: "e98"} - filename := file.ShareFileName() + filename := file.ShareBase() assert.NotContains(t, filename, "20190115-000000-Berlin-Morning-Mood") }) t.Run("no file uid", func(t *testing.T) { file := &File{Photo: nil, FileType: "jpg", FileHash: "e98ijhyt"} - filename := file.ShareFileName() + filename := file.ShareBase() assert.Equal(t, filename, "e98ijhyt.jpg") }) diff --git a/internal/i18n/locales.go b/internal/i18n/locales.go index bf207d9e5..2cb03a59b 100644 --- a/internal/i18n/locales.go +++ b/internal/i18n/locales.go @@ -38,3 +38,7 @@ func SetLocale(loc string) { gotext.Configure(localeDir, string(locale), "default") } + +func (l Locale) Locale() string { + return string(l) +}